From: jenkins-bot Date: Thu, 9 Jun 2016 22:13:00 +0000 (+0000) Subject: Merge "Revert "Map dummy language codes in sites"" X-Git-Tag: 1.31.0-rc.0~6658 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/exercices/modifier.php?a=commitdiff_plain;h=a33815b27b8e1dba390a1f73fa9328b5a0e20612;hp=e0661f825d6846b4f396fa88b2c495116b45baea;p=lhc%2Fweb%2Fwiklou.git Merge "Revert "Map dummy language codes in sites"" --- diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000000..6b94db6722 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,26 @@ +{ + "rules": { + "color-hex-case": [ "lower" ], + "color-hex-length": [ "short" ], + "color-named": [ "never" ], + "color-no-invalid-hex": true, + + "declaration-bang-space-after": [ "never" ], + "declaration-bang-space-before": [ "always" ], + "declaration-colon-space-after": [ "always" ], + "declaration-colon-space-before": [ "never" ], + + "font-family-name-quotes": [ "single-unless-keyword" ], + "font-weight-notation": [ "named-where-possible" ], + + "function-calc-no-unspaced-operator": true, + "function-comma-newline-after": "never-multi-line", + "function-comma-newline-before": "never-multi-line", + "function-comma-space-after": [ "always" ], + "function-comma-space-before": [ "never" ], + "function-parentheses-newline-inside": [ "never-multi-line" ], + "function-parentheses-space-inside": [ "always" ], + "function-url-quotes": [ "none" ], + "function-whitespace-after": [ "always" ], + } +} diff --git a/CREDITS b/CREDITS index a54bd90b53..dca597ef0d 100644 --- a/CREDITS +++ b/CREDITS @@ -1,6 +1,6 @@ {{int:version-credits-summary}} diff --git a/Gruntfile.js b/Gruntfile.js index 354f0483b6..a08db5c780 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2,6 +2,7 @@ module.exports = function ( grunt ) { grunt.loadNpmTasks( 'grunt-contrib-copy' ); grunt.loadNpmTasks( 'grunt-contrib-jshint' ); + grunt.loadNpmTasks( 'grunt-stylelint' ); grunt.loadNpmTasks( 'grunt-contrib-watch' ); grunt.loadNpmTasks( 'grunt-banana-checker' ); grunt.loadNpmTasks( 'grunt-jscs' ); @@ -39,9 +40,15 @@ module.exports = function ( grunt ) { api: 'includes/api/i18n/', installer: 'includes/installer/i18n/' }, + stylelint: { + options: { + syntax: 'less' + }, + src: '{resources/src/*,mw-config/**}/*.{css,less}' + }, watch: { files: [ - '.js*', + '.{stylelintrc,jscsrc,jshintignore,jshintrc}', '**/*', '!{docs,extensions,node_modules,skins,vendor}/**' ], @@ -96,7 +103,7 @@ module.exports = function ( grunt ) { return !!( process.env.MW_SERVER && process.env.MW_SCRIPT_PATH ); } ); - grunt.registerTask( 'lint', [ 'jshint', 'jscs', 'jsonlint', 'banana' ] ); + grunt.registerTask( 'lint', [ 'jshint', 'jscs', 'jsonlint', 'banana', 'stylelint' ] ); grunt.registerTask( 'qunit', [ 'assert-mw-env', 'karma:main' ] ); grunt.registerTask( 'test', [ 'lint' ] ); diff --git a/HISTORY b/HISTORY index e57d346316..e833154827 100644 --- a/HISTORY +++ b/HISTORY @@ -1,4 +1,4 @@ -Change notes from older releases. For current info see RELEASE-NOTES-1.27. +Change notes from older releases. For current info see RELEASE-NOTES-1.28. = MediaWiki 1.26 = diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index 7c50e4fd9f..3aa5d7cce8 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -11,7 +11,7 @@ HHVM 3.1. Additionally, the following PHP extensions are required: * ctype * iconv * json -* mbstring +* mbstring (new requirement in 1.27) * xml The following PHP extensions are strongly recommended: * openssl @@ -536,6 +536,8 @@ changes to languages because of Phabricator reports. * User::isPasswordReminderThrottled() was deprecated. * Bot-oriented parameters to Special:UserLogin (wpCookieCheck, wpSkipCookieCheck) were removed. +* Installer can now be customized without patching MediaWiki code, see + mw-config/overrides/README for details. == Compatibility == diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28 index e3654869c4..0e423d296c 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -9,40 +9,38 @@ production. * The load.php entry point now enforces the existing policy of not allowing access to session data, which includes the session user and the session user's language. If such access is attempted, an exception will be thrown. +* The number of internal PBKDF2 iterations used to derive the session secret + is configurable via $wgSessionPbkdf2Iterations. +* Upload dialog's file upload log comment can now be configured separately for + local and foreign uploads. === New features in 1.28 === * User::isBot() method for checking if an account is a bot role account. * Added a new hook, 'UserIsBot', to aid in determining if a user is a bot. - === External library changes in 1.28 === ==== Upgraded external libraries ==== - +* Updated es5-shim from v4.1.5 to v4.5.8 ==== New external libraries ==== - ==== Removed and replaced external libraries ==== - === Bug fixes in 1.28 === - === Action API changes in 1.28 === - === Action API internal changes in 1.28 === - === Languages updated in 1.28 === MediaWiki supports over 350 languages. Many localisations are updated regularly. Below only new and removed languages are listed, as well as changes to languages because of Phabricator reports. -=== Other changes in 1.27 === - +=== Other changes in 1.28 === +* (T128697) Improved handling of large diffs. == Compatibility == diff --git a/autoload.php b/autoload.php index f79bace67f..f40cc89434 100644 --- a/autoload.php +++ b/autoload.php @@ -164,6 +164,7 @@ $wgAutoloadLocalClasses = [ 'BacklinkJobUtils' => __DIR__ . '/includes/jobqueue/utils/BacklinkJobUtils.php', 'BackupDumper' => __DIR__ . '/maintenance/backup.inc', 'BackupReader' => __DIR__ . '/maintenance/importDump.php', + 'BadRequestError' => __DIR__ . '/includes/exception/BadRequestError.php', 'BadTitleError' => __DIR__ . '/includes/exception/BadTitleError.php', 'BagOStuff' => __DIR__ . '/includes/libs/objectcache/BagOStuff.php', 'BaseDump' => __DIR__ . '/maintenance/backupPrefetch.inc', @@ -352,7 +353,7 @@ $wgAutoloadLocalClasses = [ 'DerivativeResourceLoaderContext' => __DIR__ . '/includes/resourceloader/DerivativeResourceLoaderContext.php', 'DescribeFileOp' => __DIR__ . '/includes/filebackend/FileOp.php', 'Diff' => __DIR__ . '/includes/diff/DairikiDiff.php', - 'DiffEngine' => __DIR__ . '/includes/diff/DairikiDiff.php', + 'DiffEngine' => __DIR__ . '/includes/diff/DiffEngine.php', 'DiffFormatter' => __DIR__ . '/includes/diff/DiffFormatter.php', 'DiffHistoryBlob' => __DIR__ . '/includes/HistoryBlob.php', 'DiffOp' => __DIR__ . '/includes/diff/DairikiDiff.php', @@ -556,6 +557,7 @@ $wgAutoloadLocalClasses = [ 'HistoryPager' => __DIR__ . '/includes/actions/HistoryAction.php', 'Hooks' => __DIR__ . '/includes/Hooks.php', 'Html' => __DIR__ . '/includes/Html.php', + 'HtmlArmor' => __DIR__ . '/includes/libs/HtmlArmor.php', 'HtmlFormatter' => __DIR__ . '/includes/HtmlFormatter.php', 'Http' => __DIR__ . '/includes/HttpFunctions.php', 'HttpError' => __DIR__ . '/includes/exception/HttpError.php', @@ -601,7 +603,7 @@ $wgAutoloadLocalClasses = [ 'InitSiteStats' => __DIR__ . '/maintenance/initSiteStats.php', 'InstallDocFormatter' => __DIR__ . '/includes/installer/InstallDocFormatter.php', 'Installer' => __DIR__ . '/includes/installer/Installer.php', - 'InstallerOverrides' => __DIR__ . '/mw-config/overrides.php', + 'InstallerOverrides' => __DIR__ . '/includes/installer/InstallerOverrides.php', 'InstallerSessionProvider' => __DIR__ . '/includes/installer/InstallerSessionProvider.php', 'Interwiki' => __DIR__ . '/includes/interwiki/Interwiki.php', 'InvalidPassword' => __DIR__ . '/includes/password/InvalidPassword.php', @@ -772,13 +774,13 @@ $wgAutoloadLocalClasses = [ 'MagicWord' => __DIR__ . '/includes/MagicWord.php', 'MagicWordArray' => __DIR__ . '/includes/MagicWordArray.php', 'MailAddress' => __DIR__ . '/includes/mail/MailAddress.php', + 'MainConfigDependency' => __DIR__ . '/includes/cache/CacheDependency.php', 'Maintenance' => __DIR__ . '/maintenance/Maintenance.php', 'MaintenanceFormatInstallDoc' => __DIR__ . '/maintenance/formatInstallDoc.php', 'MakeTestEdits' => __DIR__ . '/maintenance/makeTestEdits.php', 'MalformedTitleException' => __DIR__ . '/includes/title/MalformedTitleException.php', 'ManualLogEntry' => __DIR__ . '/includes/logging/LogEntry.php', 'MapCacheLRU' => __DIR__ . '/includes/libs/MapCacheLRU.php', - 'MappedDiff' => __DIR__ . '/includes/diff/DairikiDiff.php', 'MappedIterator' => __DIR__ . '/includes/libs/MappedIterator.php', 'MarkpatrolledAction' => __DIR__ . '/includes/actions/MarkpatrolledAction.php', 'McTest' => __DIR__ . '/maintenance/mctest.php', @@ -828,8 +830,14 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Auth\\Throttler' => __DIR__ . '/includes/auth/Throttler.php', 'MediaWiki\\Auth\\UserDataAuthenticationRequest' => __DIR__ . '/includes/auth/UserDataAuthenticationRequest.php', 'MediaWiki\\Auth\\UsernameAuthenticationRequest' => __DIR__ . '/includes/auth/UsernameAuthenticationRequest.php', + 'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php', + 'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php', + 'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php', + 'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php', 'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php', 'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php', + 'MediaWiki\\Linker\\LinkRenderer' => __DIR__ . '/includes/linker/LinkRenderer.php', + 'MediaWiki\\Linker\\LinkRendererFactory' => __DIR__ . '/includes/linker/LinkRendererFactory.php', 'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php', 'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php', 'MediaWiki\\Logger\\LegacySpi' => __DIR__ . '/includes/debug/logger/LegacySpi.php', @@ -850,6 +858,7 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php', 'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php', 'MediaWiki\\Services\\NoSuchServiceException' => __DIR__ . '/includes/Services/NoSuchServiceException.php', + 'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/Services/SalvageableService.php', 'MediaWiki\\Services\\ServiceAlreadyDefinedException' => __DIR__ . '/includes/Services/ServiceAlreadyDefinedException.php', 'MediaWiki\\Services\\ServiceContainer' => __DIR__ . '/includes/Services/ServiceContainer.php', 'MediaWiki\\Services\\ServiceDisabledException' => __DIR__ . '/includes/Services/ServiceDisabledException.php', @@ -927,7 +936,6 @@ $wgAutoloadLocalClasses = [ 'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php', 'MutableContext' => __DIR__ . '/includes/context/MutableContext.php', 'MwSql' => __DIR__ . '/maintenance/sql.php', - 'MyLocalSettingsGenerator' => __DIR__ . '/mw-config/overrides.php', 'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', 'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php', @@ -1086,7 +1094,7 @@ $wgAutoloadLocalClasses = [ 'RCFeedFormatter' => __DIR__ . '/includes/rcfeed/RCFeedFormatter.php', 'RSSFeed' => __DIR__ . '/includes/Feed.php', 'RandomPage' => __DIR__ . '/includes/specials/SpecialRandompage.php', - 'RangeDifference' => __DIR__ . '/includes/diff/WikiDiff3.php', + 'RangeDifference' => __DIR__ . '/includes/diff/DiffEngine.php', 'RawAction' => __DIR__ . '/includes/actions/RawAction.php', 'RawMessage' => __DIR__ . '/includes/Message.php', 'ReadOnlyError' => __DIR__ . '/includes/exception/ReadOnlyError.php', @@ -1146,7 +1154,6 @@ $wgAutoloadLocalClasses = [ 'ResourceLoaderUploadDialogModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUploadDialogModule.php', 'ResourceLoaderUserCSSPrefsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php', 'ResourceLoaderUserDefaultsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserDefaultsModule.php', - 'ResourceLoaderUserGroupsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserGroupsModule.php', 'ResourceLoaderUserModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserModule.php', 'ResourceLoaderUserOptionsModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserOptionsModule.php', 'ResourceLoaderUserTokensModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderUserTokensModule.php', @@ -1191,6 +1198,7 @@ $wgAutoloadLocalClasses = [ 'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php', 'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php', 'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.php', + 'SearchApi' => __DIR__ . '/includes/api/SearchApi.php', 'SearchDatabase' => __DIR__ . '/includes/search/SearchDatabase.php', 'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php', 'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php', @@ -1504,7 +1512,6 @@ $wgAutoloadLocalClasses = [ 'WebRequestUpload' => __DIR__ . '/includes/WebRequestUpload.php', 'WebResponse' => __DIR__ . '/includes/WebResponse.php', 'WikiCategoryPage' => __DIR__ . '/includes/page/WikiCategoryPage.php', - 'WikiDiff3' => __DIR__ . '/includes/diff/WikiDiff3.php', 'WikiExporter' => __DIR__ . '/includes/export/WikiExporter.php', 'WikiFilePage' => __DIR__ . '/includes/page/WikiFilePage.php', 'WikiImporter' => __DIR__ . '/includes/import/WikiImporter.php', @@ -1517,7 +1524,7 @@ $wgAutoloadLocalClasses = [ 'WikitextContentHandler' => __DIR__ . '/includes/content/WikitextContentHandler.php', 'WinCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/WinCacheBagOStuff.php', 'WithoutInterwikiPage' => __DIR__ . '/includes/specials/SpecialWithoutinterwiki.php', - 'WordLevelDiff' => __DIR__ . '/includes/diff/DairikiDiff.php', + 'WordLevelDiff' => __DIR__ . '/includes/diff/WordLevelDiff.php', 'WrapOldPasswords' => __DIR__ . '/maintenance/wrapOldPasswords.php', 'XCFHandler' => __DIR__ . '/includes/media/XCF.php', 'XCacheBagOStuff' => __DIR__ . '/includes/libs/objectcache/XCacheBagOStuff.php', diff --git a/composer.json b/composer.json index ef85ec4652..a2614490c0 100644 --- a/composer.json +++ b/composer.json @@ -25,13 +25,13 @@ "ext-xml": "*", "liuggio/statsd-php-client": "1.0.18", "mediawiki/at-ease": "1.1.0", - "oojs/oojs-ui": "0.17.2", + "oojs/oojs-ui": "0.17.4", "oyejorge/less.php": "1.7.0.10", "php": ">=5.5.9", "psr/log": "1.0.0", "wikimedia/assert": "0.2.2", "wikimedia/base-convert": "1.0.1", - "wikimedia/cdb": "1.4.0", + "wikimedia/cdb": "1.4.1", "wikimedia/cldr-plural-rule-parser": "1.0.0", "wikimedia/composer-merge-plugin": "1.3.1", "wikimedia/html-formatter": "1.0.1", @@ -46,7 +46,7 @@ "require-dev": { "jakub-onderka/php-parallel-lint": "0.9.2", "justinrainbow/json-schema": "~1.3", - "mediawiki/mediawiki-codesniffer": "0.7.1", + "mediawiki/mediawiki-codesniffer": "0.7.2", "monolog/monolog": "~1.18.2", "nikic/php-parser": "1.4.1", "nmred/kafka-php": "0.1.5", diff --git a/docs/hooks.txt b/docs/hooks.txt index f6527866a9..c91354d2ff 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -1783,7 +1783,8 @@ $title: The page's Title. $out: The output page. $cssClassName: CSS class name of the language selector. -'LinkBegin': Used when generating internal and interwiki links in +'LinkBegin': DEPRECATED! Use HtmlPageLinkRendererBegin instead. +Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. @@ -1800,7 +1801,8 @@ $target: the Title that the link is pointing to &$options: array of options. Can include 'known', 'broken', 'noclasses'. &$ret: the value to return if your hook returns false. -'LinkEnd': Used when generating internal and interwiki links in Linker::link(), +'LinkEnd': DEPRECATED! Use HtmlPageLinkRendererEnd hook instead +Used when generating internal and interwiki links in Linker::link(), just before the function returns a value. If you return true, an element with HTML attributes $attribs and contents $html will be returned. If you return false, $ret will be returned. @@ -1835,6 +1837,35 @@ $file: the File object or false if broken link &$attribs: the attributes to be applied &$ret: the value to return if your hook returns false +'LinkRendererBegin': +Used when generating internal and interwiki links in +LinkRenderer, before processing starts. Return false to skip default +processing and return $ret. +$linkRenderer: the LinkRenderer object +$target: the LinkTarget that the link is pointing to +&$html: the contents that the tag should have (raw HTML); null means + "default". +&$customAttribs: the HTML attributes that the tag should have, in + associative array form, with keys and values unescaped. Should be merged + with default values, with a value of false meaning to suppress the + attribute. +&$query: the query string to add to the generated URL (the bit after the "?"), + in associative array form, with keys and values unescaped. +&$ret: the value to return if your hook returns false. + +'LinkRendererEnd': +Used when generating internal and interwiki links in LinkRenderer, +just before the function returns a value. If you return true, an element +with HTML attributes $attribs and contents $html will be returned. If you +return false, $ret will be returned. +$linkRenderer: the LinkRenderer object +$target: the LinkTarget object that the link is pointing to +$isKnown: boolean indicating whether the page is known or not +&$html: the final (raw HTML) contents of the tag, after processing. +&$attribs: the final HTML attributes of the tag, after processing, in + associative array form. +&$ret: the value to return if your hook returns false. + 'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the actual update. &$linksUpdate: the LinksUpdate object @@ -2494,6 +2525,12 @@ $context: (IContextSource) The RequestContext the skin is being created for. &$skin: A variable reference you may set a Skin instance or string key on to override the skin that will be used for the context. +'RequestHasSameOriginSecurity': Called to determine if the request is somehow +flagged to lack same-origin security. Return false to indicate the lack. Note +if the "somehow" involves HTTP headers, you'll probably need to make sure +the header is varied on. +WebRequest $request: The request. + 'ResetPasswordExpiration': Allow extensions to set a default password expiration $user: The user having their password expiration reset &$newExpire: The new expiration date diff --git a/includes/Category.php b/includes/Category.php index 6209a1a9ea..28b566a7f9 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -95,7 +95,11 @@ class Category { # and should not be kept, and 2) we *probably* don't have to scan many # rows to obtain the correct figure, so let's risk a one-time recount. if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { - $this->refreshCounts(); + $this->mPages = max( $this->mPages, 0 ); + $this->mSubcats = max( $this->mSubcats, 0 ); + $this->mFiles = max( $this->mFiles, 0 ); + + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); } return true; diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 0b70d1632b..dc0b60c462 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -567,10 +567,14 @@ $wgUploadDialog = [ // * upload-form-label-not-own-work-local-generic-foreign 'foreign' => 'generic-foreign', ], - // Upload comment to use. Available replacements: + // Upload comments to use for 'local' and 'foreign' uploads. This can also be set to a single + // string value, in which case it is used for both kinds of uploads. Available replacements: // * $HOST - domain name from which a cross-wiki upload originates // * $PAGENAME - wiki page name from which an upload originates - 'comment' => '', + 'comment' => [ + 'local' => '', + 'foreign' => '', + ], // Format of the file page wikitext to be generated from the fields input by the user. 'format' => [ // Wrapper for the whole page. Available replacements: @@ -1902,6 +1906,7 @@ $wgSharedSchema = false; * if available * * - max lag: (optional) Maximum replication lag before a slave will taken out of rotation + * - is static: (optional) Set to true if the dataset is static and no replication is used. * * These and any other user-defined properties will be assigned to the mLBInfo member * variable of the Database object. @@ -2385,6 +2390,13 @@ $wgSessionHandler = null; */ $wgPHPSessionHandling = 'enable'; +/** + * Number of internal PBKDF2 iterations to use when deriving session secrets. + * + * @since 1.28 + */ +$wgSessionPbkdf2Iterations = 10001; + /** * If enabled, will send MemCached debugging information to $wgDebugLogFile */ @@ -4261,7 +4273,13 @@ $wgDebugTidy = false; $wgRawHtml = false; /** - * Set a default target for external links, e.g. _blank to pop up a new window + * Set a default target for external links, e.g. _blank to pop up a new window. + * + * This will also set the "noreferrer" and "noopener" link rel to prevent the + * attack described at https://mathiasbynens.github.io/rel-noopener/ . + * Some older browsers may not support these link attributes, hence + * setting $wgExternalLinkTarget to _blank may represent a security risk + * to some of your users. */ $wgExternalLinkTarget = false; @@ -4451,7 +4469,7 @@ $wgPasswordPolicy = [ * @since 1.27 * @deprecated since 1.27, for use during development only */ -$wgDisableAuthManager = true; +$wgDisableAuthManager = false; /** * Configure AuthManager @@ -4539,9 +4557,40 @@ $wgAuthManagerAutoConfig = [ ]; /** - * If it has been this long since the last authentication, recommend - * re-authentication before security-sensitive operations (e.g. password or - * email changes). Set negative to disable. + * Time frame for re-authentication. + * + * With only password-based authentication, you'd just ask the user to re-enter + * their password to verify certain operations like changing the password or + * changing the account's email address. But under AuthManager, the user might + * not have a password (you might even have to redirect the browser to a + * third-party service or something complex like that), you might want to have + * both factors of a two-factor authentication, and so on. So, the options are: + * - Incorporate the whole multi-step authentication flow within everything + * that needs to do this. + * - Consider it good if they used Special:UserLogin during this session within + * the last X seconds. + * - Come up with a third option. + * + * MediaWiki currently takes the second option. This setting configures the + * "X seconds". + * + * This allows for configuring different time frames for different + * "operations". The operations used in MediaWiki core include: + * - LinkAccounts + * - UnlinkAccount + * - ChangeCredentials + * - RemoveCredentials + * - ChangeEmail + * + * Additional operations may be used by extensions, either explicitly by + * calling AuthManager::securitySensitiveOperationStatus(), + * ApiAuthManagerHelper::securitySensitiveOperation() or + * SpecialPage::checkLoginSecurityLevel(), or implicitly by overriding + * SpecialPage::getLoginSecurityLevel() or by subclassing + * AuthManagerSpecialPage. + * + * The key 'default' is used if a requested operation isn't defined in the array. + * * @since 1.27 * @var int[] operation => time in seconds. A 'default' key must always be provided. */ @@ -4550,8 +4599,18 @@ $wgReauthenticateTime = [ ]; /** - * Whether to allow security-sensitive operations when authentication is not possible. + * Whether to allow security-sensitive operations when re-authentication is not possible. + * + * If AuthManager::canAuthenticateNow() is false (e.g. the current + * SessionProvider is not able to change users, such as when OAuth is in use), + * AuthManager::securitySensitiveOperationStatus() cannot sensibly return + * SEC_REAUTH. Setting an operation true here will have it return SEC_OK in + * that case, while setting it false will have it return SEC_FAIL. + * + * The key 'default' is used if a requested operation isn't defined in the array. + * * @since 1.27 + * @see $wgReauthenticateTime * @var bool[] operation => boolean. A 'default' key must always be provided. */ $wgAllowSecuritySensitiveOperationIfCannotReauthenticate = [ @@ -4844,7 +4903,7 @@ $wgSessionProviders = [ MediaWiki\Session\BotPasswordSessionProvider::class => [ 'class' => MediaWiki\Session\BotPasswordSessionProvider::class, 'args' => [ [ - 'priority' => 40, + 'priority' => 75, ] ], ], ]; @@ -7051,6 +7110,7 @@ $wgExtensionCredits = []; /** * Authentication plugin. * @var $wgAuth AuthPlugin + * @deprecated since 1.27 use $wgAuthManagerConfig instead */ $wgAuth = null; diff --git a/includes/Defines.php b/includes/Defines.php index 9a6950e081..fe5083e1be 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -183,6 +183,7 @@ define( 'EDIT_SUPPRESS_RC', 8 ); define( 'EDIT_FORCE_BOT', 16 ); define( 'EDIT_DEFER_UPDATES', 32 ); // Unused since 1.27 define( 'EDIT_AUTOSUMMARY', 64 ); +define( 'EDIT_INTERNAL', 128 ); /**@}*/ /**@{ @@ -305,3 +306,9 @@ define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions define( 'CONTENT_FORMAT_XML', 'application/xml' ); /**@}*/ + +/**@{ + * Max string length for shell invocations; based on binfmts.h + */ +define( 'SHELL_MAX_ARG_STRLEN', '100000' ); +/**@}*/ diff --git a/includes/DummyLinker.php b/includes/DummyLinker.php index 6545c4aac5..d9330eebc2 100644 --- a/includes/DummyLinker.php +++ b/includes/DummyLinker.php @@ -72,7 +72,7 @@ class DummyLinker { $html = null, $customAttribs = [], $query = [], - $options = [ 'known', 'noclasses' ] + $options = [ 'known' ] ) { return Linker::linkKnown( $target, diff --git a/includes/EditPage.php b/includes/EditPage.php index 0f529832da..f2403fe222 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -2833,9 +2833,14 @@ class EditPage { "
\n$1\n
", [ 'anoneditwarning', // Log-in link - '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}', + SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ), // Sign-up link - '{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ] + SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ + 'returnto' => $this->getTitle()->getPrefixedDBkey() + ] ) + ] ); } else { $wgOut->wrapWikiMsg( "
\n$1
", @@ -3496,6 +3501,8 @@ HTML $cancelParams = []; if ( !$this->isConflict && $this->oldid > 0 ) { $cancelParams['oldid'] = $this->oldid; + } elseif ( $this->getContextTitle()->isRedirect() ) { + $cancelParams['redirect'] = 'no'; } $attrs = [ 'id' => 'mw-editform-cancel' ]; diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 618fa4cab5..d5c6553958 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -30,7 +30,6 @@ use MediaWiki\Session\SessionManager; // Hide compatibility functions from Doxygen /// @cond - /** * Compatibility functions * @@ -2457,6 +2456,15 @@ function wfShellExec( $cmd, &$retval = null, $environ = [], } wfDebug( "wfShellExec: $cmd\n" ); + // Don't try to execute commands that exceed Linux's MAX_ARG_STRLEN. + // Other platforms may be more accomodating, but we don't want to be + // accomodating, because very long commands probably include user + // input. See T129506. + if ( strlen( $cmd ) > SHELL_MAX_ARG_STRLEN ) { + throw new Exception( __METHOD__ . + '(): total length of $cmd must not exceed SHELL_MAX_ARG_STRLEN' ); + } + $desc = [ 0 => [ 'file', 'php://stdin', 'r' ], 1 => [ 'pipe', 'w' ], diff --git a/includes/Linker.php b/includes/Linker.php index 6a869dd45f..0b2d3a71b5 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -20,6 +20,7 @@ * @file */ use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; /** * Some internal bits split of from Skin.php. These functions are used @@ -137,22 +138,30 @@ class Linker { * Return the CSS colour of a known link * * @since 1.16.3 - * @param Title $t + * @param LinkTarget $t * @param int $threshold User defined threshold * @return string CSS class */ - public static function getLinkColour( $t, $threshold ) { - $colour = ''; - if ( $t->isRedirect() ) { + public static function getLinkColour( LinkTarget $t, $threshold ) { + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); + // Make sure the target is in the cache + $id = $linkCache->addLinkObj( $t ); + if ( $id == 0 ) { + // Doesn't exist + return ''; + } + + if ( $linkCache->getGoodLinkFieldObj( $t, 'redirect' ) ) { # Page is a redirect - $colour = 'mw-redirect'; - } elseif ( $threshold > 0 && $t->isContentPage() && - $t->exists() && $t->getLength() < $threshold + return 'mw-redirect'; + } elseif ( $threshold > 0 && MWNamespace::isContent( $t->getNamespace() ) + && $linkCache->getGoodLinkFieldObj( $t, 'length' ) < $threshold ) { # Page is a stub - $colour = 'stub'; + return 'stub'; } - return $colour; + + return ''; } /** @@ -210,55 +219,35 @@ class Linker { wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' ); $query = wfCgiToArray( $query ); } - $options = (array)$options; - - $dummy = new DummyLinker; // dummy linker instance for bc on the hooks - - $ret = null; - if ( !Hooks::run( 'LinkBegin', - [ $dummy, $target, &$html, &$customAttribs, &$query, &$options, &$ret ] ) - ) { - return $ret; - } - # Normalize the Title if it's a special page - $target = self::normaliseSpecialPage( $target ); - - # If we don't know whether the page exists, let's find out. - if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) { - if ( $target->isKnown() ) { - $options[] = 'known'; - } else { - $options[] = 'broken'; + $services = MediaWikiServices::getInstance(); + $options = (array)$options; + if ( $options ) { + // Custom options, create new LinkRenderer + if ( !isset( $options['stubThreshold'] ) ) { + $defaultLinkRenderer = $services->getLinkRenderer(); + $options['stubThreshold'] = $defaultLinkRenderer->getStubThreshold(); } + $linkRenderer = $services->getLinkRendererFactory() + ->createFromLegacyOptions( $options ); + } else { + $linkRenderer = $services->getLinkRenderer(); } - $oldquery = []; - if ( in_array( "forcearticlepath", $options, true ) && $query ) { - $oldquery = $query; - $query = []; - } - - # Note: we want the href attribute first, for prettiness. - $attribs = [ 'href' => self::linkUrl( $target, $query, $options ) ]; - if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) { - $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery ); - } - - $attribs = array_merge( - $attribs, - self::linkAttribs( $target, $customAttribs, $options ) - ); - if ( is_null( $html ) ) { - $html = self::linkText( $target ); - } - - $ret = null; - if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) { - $ret = Html::rawElement( 'a', $attribs, $html ); + if ( $html !== null ) { + $text = new HtmlArmor( $html ); + } else { + $text = $html; // null + } + if ( in_array( 'known', $options, true ) ) { + return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query ); + } elseif ( in_array( 'broken', $options, true ) ) { + return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query ); + } elseif ( in_array( 'noclasses', $options, true ) ) { + return $linkRenderer->makePreloadedLink( $target, $text, '', $customAttribs, $query ); + } else { + return $linkRenderer->makeLink( $target, $text, $customAttribs, $query ); } - - return $ret; } /** @@ -269,135 +258,11 @@ class Linker { */ public static function linkKnown( $target, $html = null, $customAttribs = [], - $query = [], $options = [ 'known', 'noclasses' ] + $query = [], $options = [ 'known' ] ) { return self::link( $target, $html, $customAttribs, $query, $options ); } - /** - * Returns the Url used to link to a Title - * - * @param LinkTarget $target - * @param array $query Query parameters - * @param array $options - * @return string - */ - private static function linkUrl( LinkTarget $target, $query, $options ) { - # We don't want to include fragments for broken links, because they - # generally make no sense. - if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) { - $target = $target->createFragmentTarget( '' ); - } - - # If it's a broken link, add the appropriate query pieces, unless - # there's already an action specified, or unless 'edit' makes no sense - # (i.e., for a nonexistent special page). - if ( in_array( 'broken', $options, true ) && empty( $query['action'] ) - && $target->getNamespace() !== NS_SPECIAL ) { - $query['action'] = 'edit'; - $query['redlink'] = '1'; - } - - if ( in_array( 'http', $options, true ) ) { - $proto = PROTO_HTTP; - } elseif ( in_array( 'https', $options, true ) ) { - $proto = PROTO_HTTPS; - } else { - $proto = PROTO_RELATIVE; - } - - $title = Title::newFromLinkTarget( $target ); - $ret = $title->getLinkURL( $query, false, $proto ); - return $ret; - } - - /** - * Returns the array of attributes used when linking to the Title $target - * - * @param Title $target - * @param array $attribs - * @param array $options - * - * @return array - */ - private static function linkAttribs( $target, $attribs, $options ) { - global $wgUser; - $defaults = []; - - if ( !in_array( 'noclasses', $options, true ) ) { - # Now build the classes. - $classes = []; - - if ( in_array( 'broken', $options, true ) ) { - $classes[] = 'new'; - } - - if ( $target->isExternal() ) { - $classes[] = 'extiw'; - } - - if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387) - $colour = self::getLinkColour( - $target, - isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold() - ); - if ( $colour !== '' ) { - $classes[] = $colour; # mw-redirect or stub - } - } - if ( $classes != [] ) { - $defaults['class'] = implode( ' ', $classes ); - } - } - - # Get a default title attribute. - if ( $target->getPrefixedText() == '' ) { - # A link like [[#Foo]]. This used to mean an empty title - # attribute, but that's silly. Just don't output a title. - } elseif ( in_array( 'known', $options, true ) ) { - $defaults['title'] = $target->getPrefixedText(); - } else { - // This ends up in parser cache! - $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() ) - ->inContentLanguage() - ->text(); - } - - # Finally, merge the custom attribs with the default ones, and iterate - # over that, deleting all "false" attributes. - $ret = []; - $merged = Sanitizer::mergeAttributes( $defaults, $attribs ); - foreach ( $merged as $key => $val ) { - # A false value suppresses the attribute, and we don't want the - # href attribute to be overridden. - if ( $key != 'href' && $val !== false ) { - $ret[$key] = $val; - } - } - return $ret; - } - - /** - * Default text of the links to the Title $target - * - * @param Title $target - * - * @return string - */ - private static function linkText( $target ) { - if ( !$target instanceof Title ) { - wfWarn( __METHOD__ . ': Requires $target to be a Title object.' ); - return ''; - } - // If the target is just a fragment, with no title, we return the fragment - // text. Otherwise, we return the title text itself. - if ( $target->getPrefixedText() === '' && $target->hasFragment() ) { - return htmlspecialchars( $target->getFragment() ); - } - - return htmlspecialchars( $target->getPrefixedText() ); - } - /** * Make appropriate markup for a link to the current article. This is * currently rendered as the bold link text. The calling sequence is the @@ -940,7 +805,15 @@ class Linker { $redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title ); if ( $redir ) { - return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) ); + // We already know it's a redirect, so mark it + // accordingly + return self::link( + $title, + $encLabel, + [ 'class' => 'mw-redirect' ], + wfCgiToArray( $query ), + [ 'known', 'noclasses' ] + ); } $href = self::getUploadUrl( $title, $query ); @@ -950,7 +823,7 @@ class Linker { $encLabel . '
'; } - return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) ); + return self::link( $title, $encLabel, [], wfCgiToArray( $query ), [ 'known', 'noclasses' ] ); } /** @@ -1084,7 +957,16 @@ class Linker { if ( !$title ) { $title = $wgTitle; } - $attribs['rel'] = Parser::getExternalLinkRel( $url, $title ); + $newRel = Parser::getExternalLinkRel( $url, $title ); + if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) { + $attribs['rel'] = $newRel; + } elseif ( $newRel !== '' ) { + // Merge the rel attributes. + $newRels = explode( ' ', $newRel ); + $oldRels = explode( ' ', $attribs['rel'] ); + $combined = array_unique( array_merge( $newRels, $oldRels ) ); + $attribs['rel'] = implode( ' ', $combined ); + } $link = ''; $success = Hooks::run( 'LinkerMakeExternalLink', [ &$url, &$text, &$link, &$attribs, $linktype ] ); @@ -1872,7 +1754,7 @@ class Linker { * work if $wgShowRollbackEditCount is disabled, so this can only function * as an additional check. * - * If the option noBrackets is set the rollback link wont be enclosed in [] + * If the option noBrackets is set the rollback link wont be enclosed in "[]". * * @since 1.16.3. $context added in 1.20. $options added in 1.21 * @@ -1996,11 +1878,14 @@ class Linker { $query = [ 'action' => 'rollback', 'from' => $rev->getUserText(), - 'token' => $context->getUser()->getEditToken( [ - $title->getPrefixedText(), - $rev->getUserText() - ] ), + 'token' => $context->getUser()->getEditToken( 'rollback' ), ]; + $attrs = [ + 'data-mw' => 'interface', + 'title' => $context->msg( 'tooltip-rollback' )->text(), + ]; + $options = [ 'known', 'noclasses' ]; + if ( $context->getRequest()->getBool( 'bot' ) ) { $query['bot'] = '1'; $query['hidediff'] = '1'; // bug 15999 @@ -2025,27 +1910,16 @@ class Linker { } if ( $editCount > $wgShowRollbackEditCount ) { - $editCount_output = $context->msg( 'rollbacklinkcount-morethan' ) + $html = $context->msg( 'rollbacklinkcount-morethan' ) ->numParams( $wgShowRollbackEditCount )->parse(); } else { - $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); + $html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse(); } - return self::link( - $title, - $editCount_output, - [ 'title' => $context->msg( 'tooltip-rollback' )->text() ], - $query, - [ 'known', 'noclasses' ] - ); + return self::link( $title, $html, $attrs, $query, $options ); } else { - return self::link( - $title, - $context->msg( 'rollbacklink' )->escaped(), - [ 'title' => $context->msg( 'tooltip-rollback' )->text() ], - $query, - [ 'known', 'noclasses' ] - ); + $html = $context->msg( 'rollbacklink' )->escaped(); + return self::link( $title, $html, $attrs, $query, $options ); } } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index ff469e4e39..ee03f020a8 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -667,10 +667,10 @@ class MediaWiki { $trxLimits = $this->config->get( 'TrxProfilerLimits' ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) ); - if ( $request->wasPosted() ) { - $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); - } else { + if ( $request->hasSafeMethod() ) { $trxProfiler->setExpectations( $trxLimits['GET'], __METHOD__ ); + } else { + $trxProfiler->setExpectations( $trxLimits['POST'], __METHOD__ ); } // If the user has forceHTTPS set to true, or if the user @@ -680,6 +680,8 @@ class MediaWiki { // isLoggedIn() will do all sorts of weird stuff. if ( $request->getProtocol() == 'http' && + // switch to HTTPS only when supported by the server + preg_match( '#^https://#', wfExpandUrl( $request->getRequestURL(), PROTO_HTTPS ) ) && ( $request->getSession()->shouldForceHTTPS() || // Check the cookie manually, for paranoia diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index 891f426d74..6613db1602 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -11,9 +11,12 @@ use LBFactory; use LinkCache; use Liuggio\StatsdClient\Factory\StatsdDataFactory; use LoadBalancer; +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\Linker\LinkRendererFactory; +use MediaWiki\Services\SalvageableService; use MediaWiki\Services\ServiceContainer; use MWException; -use ResourceLoader; +use ObjectCache; use SearchEngine; use SearchEngineConfig; use SearchEngineFactory; @@ -23,6 +26,7 @@ use WatchedItemStore; use SkinFactory; use TitleFormatter; use TitleParser; +use MediaWiki\Interwiki\InterwikiLookup; /** * Service locator for MediaWiki core services. @@ -88,7 +92,7 @@ class MediaWikiServices extends ServiceContainer { // even if it's just a file name or database credentials to load // configuration from. $bootstrapConfig = new GlobalVarConfig(); - self::$instance = self::newInstance( $bootstrapConfig ); + self::$instance = self::newInstance( $bootstrapConfig, 'load' ); } return self::$instance; @@ -121,7 +125,7 @@ class MediaWikiServices extends ServiceContainer { /** * Creates a new instance of MediaWikiServices and sets it as the global default * instance. getInstance() will return a different MediaWikiServices object - * after every call to resetGlobalServiceLocator(). + * after every call to resetGlobalInstance(). * * @since 1.28 * @@ -129,7 +133,7 @@ class MediaWikiServices extends ServiceContainer { * when the configuration has changed significantly since bootstrap time, e.g. * during the installation process or during testing. * - * @warning Calling resetGlobalServiceLocator() may leave the application in an inconsistent + * @warning Calling resetGlobalInstance() may leave the application in an inconsistent * state. Calling this is only safe under the ASSUMPTION that NO REFERENCE to * any of the services managed by MediaWikiServices exist. If any service objects * managed by the old MediaWikiServices instance remain in use, they may INTERFERE @@ -150,11 +154,14 @@ class MediaWikiServices extends ServiceContainer { * was no previous instance, a new GlobalVarConfig object will be used to * bootstrap the services. * + * @param string $quick Set this to "quick" to allow expensive resources to be re-used. + * See SalvageableService for details. + * * @throws MWException If called after MW_SERVICE_BOOTSTRAP_COMPLETE has been defined in * Setup.php (unless MW_PHPUNIT_TEST or MEDIAWIKI_INSTALL or RUN_MAINTENANCE_IF_MAIN * is defined). */ - public static function resetGlobalInstance( Config $bootstrapConfig = null ) { + public static function resetGlobalInstance( Config $bootstrapConfig = null, $quick = '' ) { if ( self::$instance === null ) { // no global instance yet, nothing to reset return; @@ -166,9 +173,38 @@ class MediaWikiServices extends ServiceContainer { $bootstrapConfig = self::$instance->getBootstrapConfig(); } - self::$instance->destroy(); + $oldInstance = self::$instance; self::$instance = self::newInstance( $bootstrapConfig ); + self::$instance->importWiring( $oldInstance, [ 'BootstrapConfig' ] ); + + if ( $quick === 'quick' ) { + self::$instance->salvage( $oldInstance ); + } else { + $oldInstance->destroy(); + } + + } + + /** + * Salvages the state of any salvageable service instances in $other. + * + * @note $other will have been destroyed when salvage() returns. + * + * @param MediaWikiServices $other + */ + private function salvage( self $other ) { + foreach ( $this->getServiceNames() as $name ) { + $oldService = $other->peekService( $name ); + + if ( $oldService instanceof SalvageableService ) { + /** @var SalvageableService $newService */ + $newService = $this->getService( $name ); + $newService->salvage( $oldService ); + } + } + + $other->destroy(); } /** @@ -177,21 +213,23 @@ class MediaWikiServices extends ServiceContainer { * ServiceWiringFiles setting are loaded, and the MediaWikiServices hook is called. * * @param Config|null $bootstrapConfig The Config object to be registered as the - * 'BootstrapConfig' service. This has to contain at least the information - * needed to set up the 'ConfigFactory' service. If not provided, any call - * to getBootstrapConfig(), getConfigFactory, or getMainConfig will fail. - * A MediaWikiServices instance without access to configuration is called - * "primordial". + * 'BootstrapConfig' service. + * + * @param string $loadWiring set this to 'load' to load the wiring files specified + * in the 'ServiceWiringFiles' setting in $bootstrapConfig. * * @return MediaWikiServices * @throws MWException + * @throws \FatalError */ - private static function newInstance( Config $bootstrapConfig ) { + private static function newInstance( Config $bootstrapConfig, $loadWiring = '' ) { $instance = new self( $bootstrapConfig ); // Load the default wiring from the specified files. - $wiringFiles = $bootstrapConfig->get( 'ServiceWiringFiles' ); - $instance->loadWiringFiles( $wiringFiles ); + if ( $loadWiring === 'load' ) { + $wiringFiles = $bootstrapConfig->get( 'ServiceWiringFiles' ); + $instance->loadWiringFiles( $wiringFiles ); + } // Provide a traditional hook point to allow extensions to configure services. Hooks::run( 'MediaWikiServices', [ $instance ] ); @@ -222,6 +260,8 @@ class MediaWikiServices extends ServiceContainer { foreach ( $destroy as $name ) { $services->disableService( $name ); } + + ObjectCache::clear(); } /** @@ -261,7 +301,7 @@ class MediaWikiServices extends ServiceContainer { * instances to clean up. * * @param string $name - * @param string $destroy Whether the service instance should be destroyed if it exists. + * @param bool $destroy Whether the service instance should be destroyed if it exists. * When set to false, any existing service instance will effectively be detached * from the container. * @@ -381,6 +421,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'SiteStore' ); } + /** + * @since 1.28 + * @return InterwikiLookup + */ + public function getInterwikiLookup() { + return $this->getService( 'InterwikiLookup' ); + } + /** * @since 1.27 * @return StatsdDataFactory @@ -470,6 +518,25 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'LinkCache' ); } + /** + * @since 1.28 + * @return LinkRendererFactory + */ + public function getLinkRendererFactory() { + return $this->getService( 'LinkRendererFactory' ); + } + + /** + * LinkRenderer instance that can be used + * if no custom options are needed + * + * @since 1.28 + * @return LinkRenderer + */ + public function getLinkRenderer() { + return $this->getService( 'LinkRenderer' ); + } + /** * @since 1.28 * @return TitleFormatter diff --git a/includes/Message.php b/includes/Message.php index c7752aac24..712d3f17fb 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -384,22 +384,30 @@ class Message implements MessageSpecifier, Serializable { /** * Transform a MessageSpecifier or a primitive value used interchangeably with - * specifiers (a message key string, or a key + params array) into a proper Message + * specifiers (a message key string, or a key + params array) into a proper Message. + * + * Also accepts a MessageSpecifier inside an array: that's not considered a valid format + * but is an easy error to make due to how StatusValue stores messages internally. + * Further array elements are ignored in that case. + * * @param string|array|MessageSpecifier $value * @return Message * @throws InvalidArgumentException * @since 1.27 */ public static function newFromSpecifier( $value ) { + $params = []; + if ( is_array( $value ) ) { + $params = $value; + $value = array_shift( $params ); + } + if ( $value instanceof RawMessage ) { $message = new RawMessage( $value->getKey(), $value->getParams() ); } elseif ( $value instanceof MessageSpecifier ) { $message = new Message( $value ); - } elseif ( is_array( $value ) ) { - $key = array_shift( $value ); - $message = new Message( $key, $value ); } elseif ( is_string( $value ) ) { - $message = new Message( $value ); + $message = new Message( $value, $params ); } else { throw new InvalidArgumentException( __METHOD__ . ': invalid argument type ' . gettype( $value ) ); @@ -447,12 +455,12 @@ class Message implements MessageSpecifier, Serializable { public function getTitle() { global $wgContLang, $wgForceUIMsgAsContentMsg; - $code = $this->getLanguage()->getCode(); $title = $this->key; if ( - $wgContLang->getCode() !== $code + !$this->language->equals( $wgContLang ) && in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) ) { + $code = $this->language->getCode(); $title .= '/' . $code; } diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index c9c326b150..2f47006272 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -154,8 +154,8 @@ function wfGzipHandler( $s ) { */ function wfMangleFlashPolicy( $s ) { # Avoid weird excessive memory usage in PCRE on big articles - if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $s ) ) { - return preg_replace( '/\<\s*cross-domain-policy\s*\>/i', '', $s ); + if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $s ) ) { + return preg_replace( '/\<(\s*)(cross-domain-policy(?=\s|\>))/i', '<$1NOT-$2', $s ); } else { return $s; } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 67e9a4ff82..ad7c97603b 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1277,15 +1277,10 @@ class OutputPage extends ContextSource { # Fetch existence plus the hiddencat property $dbr = wfGetDB( DB_SLAVE ); - $fields = [ 'page_id', 'page_namespace', 'page_title', 'page_len', - 'page_is_redirect', 'page_latest', 'pp_value' ]; - - if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) { - $fields[] = 'page_content_model'; - } - if ( $this->getConfig()->get( 'PageLanguageUseDB' ) ) { - $fields[] = 'page_lang'; - } + $fields = array_merge( + LinkCache::getSelectFields(), + [ 'page_namespace', 'page_title', 'pp_value' ] + ); $res = $dbr->select( [ 'page', 'page_props' ], $fields, @@ -2026,6 +2021,11 @@ class OutputPage extends ContextSource { * @return string */ public function getVaryHeader() { + // If we vary on cookies, let's make sure it's always included here too. + if ( $this->getCacheVaryCookies() ) { + $this->addVaryHeader( 'Cookie' ); + } + foreach ( SessionManager::singleton()->getVaryHeaders() as $header => $options ) { $this->addVaryHeader( $header, $options ); } @@ -3096,12 +3096,6 @@ class OutputPage extends ContextSource { $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED ); } - // Group JS is only enabled if site JS is enabled. - $links[] = $this->makeResourceLoaderLink( - 'user.groups', - ResourceLoaderModule::TYPE_COMBINED - ); - return self::getHtmlFromLoaderLinks( $links ); } @@ -3667,7 +3661,6 @@ class OutputPage extends ContextSource { // Per-site custom styles $moduleStyles[] = 'site'; $moduleStyles[] = 'noscript'; - $moduleStyles[] = 'user.groups'; // Per-user custom styles if ( $this->getConfig()->get( 'AllowUserCss' ) && $this->getTitle()->isCssSubpage() diff --git a/includes/PHPVersionCheck.php b/includes/PHPVersionCheck.php index 1eafcfa5b8..ab8aada836 100644 --- a/includes/PHPVersionCheck.php +++ b/includes/PHPVersionCheck.php @@ -30,7 +30,7 @@ * version are hardcoded here */ function wfEntryPointCheck( $entryPoint ) { - $mwVersion = '1.27'; + $mwVersion = '1.28'; $minimumVersionPHP = '5.5.9'; $phpVersion = PHP_VERSION; diff --git a/includes/Preferences.php b/includes/Preferences.php index 9a55ae3487..3083a8d215 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -296,7 +296,7 @@ class Preferences { $allowPasswordChange = $wgDisableAuthManager ? $wgAuth->allowPasswordChange() : AuthManager::singleton()->allowsAuthenticationDataChange( - new PasswordAuthenticationRequest(), false ); + new PasswordAuthenticationRequest(), false )->isGood(); if ( $canEditPrivateInfo && $allowPasswordChange ) { $link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ), $context->msg( 'prefs-resetpass' )->escaped(), [], diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 293e6eb176..b076d07ef6 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -37,6 +37,8 @@ * MediaWiki code base. */ +use MediaWiki\Interwiki\ClassicInterwikiLookup; +use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\MediaWikiServices; return [ @@ -88,6 +90,19 @@ return [ return $services->getConfigFactory()->makeConfig( 'main' ); }, + 'InterwikiLookup' => function( MediaWikiServices $services ) { + global $wgContLang; // TODO: manage $wgContLang as a service + $config = $services->getMainConfig(); + return new ClassicInterwikiLookup( + $wgContLang, + ObjectCache::getMainWANInstance(), + $config->get( 'InterwikiExpiry' ), + $config->get( 'InterwikiCache' ), + $config->get( 'InterwikiScopes' ), + $config->get( 'InterwikiFallbackSite' ) + ); + }, + 'StatsdDataFactory' => function( MediaWikiServices $services ) { return new BufferingStatsdDataFactory( rtrim( $services->getMainConfig()->get( 'StatsdMetricPrefix' ), '.' ) @@ -145,6 +160,22 @@ return [ ); }, + 'LinkRendererFactory' => function( MediaWikiServices $services ) { + return new LinkRendererFactory( + $services->getTitleFormatter() + ); + }, + + 'LinkRenderer' => function( MediaWikiServices $services ) { + global $wgUser; + + if ( defined( 'MW_NO_SESSION' ) ) { + return $services->getLinkRendererFactory()->create(); + } else { + return $services->getLinkRendererFactory()->createForUser( $wgUser ); + } + }, + 'GenderCache' => function( MediaWikiServices $services ) { return new GenderCache(); }, diff --git a/includes/Services/SalvageableService.php b/includes/Services/SalvageableService.php new file mode 100644 index 0000000000..a613050df1 --- /dev/null +++ b/includes/Services/SalvageableService.php @@ -0,0 +1,58 @@ +destroy() + * after carefully detaching all relevant resources. + * + * @param SalvageableService $other The object to salvage state from. $other must have the + * exact same type as $this. + */ + public function salvage( SalvageableService $other ); + +} diff --git a/includes/Services/ServiceContainer.php b/includes/Services/ServiceContainer.php index 66ee918491..b336795ee4 100644 --- a/includes/Services/ServiceContainer.php +++ b/includes/Services/ServiceContainer.php @@ -55,6 +55,11 @@ class ServiceContainer implements DestructibleService { */ private $serviceInstantiators = []; + /** + * @var boolean[] disabled status, per service name + */ + private $disabled = []; + /** * @var array */ @@ -126,6 +131,28 @@ class ServiceContainer implements DestructibleService { } } + /** + * Imports all wiring defined in $container. Wiring defined in $container + * will override any wiring already defined locally. However, already + * existing service instances will be preserved. + * + * @since 1.28 + * + * @param ServiceContainer $container + * @param string[] $skip A list of service names to skip during import + */ + public function importWiring( ServiceContainer $container, $skip = [] ) { + $newInstantiators = array_diff_key( + $container->serviceInstantiators, + array_flip( $skip ) + ); + + $this->serviceInstantiators = array_merge( + $this->serviceInstantiators, + $newInstantiators + ); + } + /** * Returns true if a service is defined for $name, that is, if a call to getService( $name ) * would return a service instance. @@ -220,6 +247,7 @@ class ServiceContainer implements DestructibleService { } $this->serviceInstantiators[$name] = $instantiator; + unset( $this->disabled[$name] ); } /** @@ -244,9 +272,7 @@ class ServiceContainer implements DestructibleService { public function disableService( $name ) { $this->resetService( $name ); - $this->redefineService( $name, function() use ( $name ) { - throw new ServiceDisabledException( $name ); - } ); + $this->disabled[$name] = true; } /** @@ -282,6 +308,7 @@ class ServiceContainer implements DestructibleService { } unset( $this->services[$name] ); + unset( $this->disabled[$name] ); } /** @@ -299,7 +326,8 @@ class ServiceContainer implements DestructibleService { * @param string $name The service name * * @throws NoSuchServiceException if $name is not a known service. - * @throws ServiceDisabledException if this container has already been destroyed. + * @throws ContainerDisabledException if this container has already been destroyed. + * @throws ServiceDisabledException if the requested service has been disabled. * * @return object The service instance */ @@ -308,6 +336,10 @@ class ServiceContainer implements DestructibleService { throw new ContainerDisabledException(); } + if ( isset( $this->disabled[$name] ) ) { + throw new ServiceDisabledException( $name ); + } + if ( !isset( $this->services[$name] ) ) { $this->services[$name] = $this->createService( $name ); } @@ -327,6 +359,7 @@ class ServiceContainer implements DestructibleService { $this->serviceInstantiators[$name], array_merge( [ $this ], $this->extraInstantiationParams ) ); + // NOTE: when adding more wiring logic here, make sure copyWiring() is kept in sync! } else { throw new NoSuchServiceException( $name ); } diff --git a/includes/Setup.php b/includes/Setup.php index e57b96a8b6..2c78061750 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -517,7 +517,7 @@ if ( !class_exists( 'AutoLoader' ) ) { // Reset the global service locator, so any services that have already been created will be // re-created while taking into account any custom settings and extensions. -MediaWikiServices::resetGlobalInstance( new GlobalVarConfig() ); +MediaWikiServices::resetGlobalInstance( new GlobalVarConfig(), 'quick' ); // Define a constant that indicates that the bootstrapping of the service locator // is complete. @@ -850,15 +850,20 @@ if ( !defined( 'MW_NO_SESSION' ) && !$wgCommandLineMode ) { if ( $sessionUser->getId() === 0 && User::isValidUserName( $sessionUser->getName() ) ) { $ps_autocreate = Profiler::instance()->scopedProfileIn( $fname . '-autocreate' ); if ( $wgDisableAuthManager ) { - MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser ); + $res = MediaWiki\Session\SessionManager::autoCreateUser( $sessionUser ); } else { - MediaWiki\Auth\AuthManager::singleton()->autoCreateUser( + $res = MediaWiki\Auth\AuthManager::singleton()->autoCreateUser( $sessionUser, - MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSSION, + MediaWiki\Auth\AuthManager::AUTOCREATE_SOURCE_SESSION, true ); } Profiler::instance()->scopedProfileOut( $ps_autocreate ); + \MediaWiki\Logger\LoggerFactory::getInstance( 'authmanager' )->info( 'Autocreation attempt', [ + 'event' => 'autocreate', + 'status' => $res, + ] ); + unset( $res ); } unset( $sessionUser ); } diff --git a/includes/Status.php b/includes/Status.php index f8370e4488..45d8bed2fa 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -118,6 +118,7 @@ class Status { /** * Returns the wrapped StatusValue object * @return StatusValue + * @since 1.27 */ public function getStatusValue() { return $this->sv; @@ -250,12 +251,22 @@ class Status { } /** - * Get the error list as a Message object + * Get a bullet list of the errors as a Message object. * - * @param string|string[] $shortContext A short enclosing context message name (or an array of - * message names), to be used when there is a single error. - * @param string|string[] $longContext A long enclosing context message name (or an array of - * message names), for a list. + * $shortContext and $longContext can be used to wrap the error list in some text. + * $shortContext will be preferred when there is a single error; $longContext will be + * preferred when there are multiple ones. In either case, $1 will be replaced with + * the list of errors. + * + * $shortContext is assumed to use $1 as an inline parameter: if there is a single item, + * it will not be made into a list; if there are multiple items, newlines will be inserted + * around the list. + * $longContext is assumed to use $1 as a standalone parameter; it will always receive a list. + * + * If both parameters are missing, and there is only one error, no bullet will be added. + * + * @param string|string[] $shortContext A message name or an array of message names. + * @param string|string[] $longContext A message name or an array of message names. * @param string|Language $lang Language to use for processing messages * @return Message */ @@ -286,10 +297,6 @@ class Status { $msgs = $this->getErrorMessageArray( $rawErrors, $lang ); $msgCount = count( $msgs ); - if ( $shortContext ) { - $msgCount++; - } - $s = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) ); $s->params( $msgs )->parse(); diff --git a/includes/Title.php b/includes/Title.php index 25fbce3f57..4555f16d9b 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -22,6 +22,7 @@ * @file */ use MediaWiki\Linker\LinkTarget; +use MediaWiki\Interwiki\InterwikiLookup; use MediaWiki\MediaWikiServices; /** @@ -170,6 +171,18 @@ class Title implements LinkTarget { return MediaWikiServices::getInstance()->getTitleFormatter(); } + /** + * B/C kludge: provide an InterwikiLookup for use by Title. + * Ideally, Title would have no methods that need this. + * Avoid usage of this singleton by using TitleValue + * and the associated services when possible. + * + * @return InterwikiLookup + */ + private static function getInterwikiLookup() { + return MediaWikiServices::getInstance()->getInterwikiLookup(); + } + /** * @access protected */ @@ -760,7 +773,7 @@ class Title implements LinkTarget { */ public function isLocal() { if ( $this->isExternal() ) { - $iw = Interwiki::fetch( $this->mInterwiki ); + $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki ); if ( $iw ) { return $iw->isLocal(); } @@ -808,7 +821,7 @@ class Title implements LinkTarget { return false; } - return Interwiki::fetch( $this->mInterwiki )->isTranscludable(); + return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable(); } /** @@ -821,7 +834,7 @@ class Title implements LinkTarget { return false; } - return Interwiki::fetch( $this->mInterwiki )->getWikiID(); + return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID(); } /** @@ -908,7 +921,9 @@ class Title implements LinkTarget { * @return string Content model id */ public function getContentModel( $flags = 0 ) { - if ( !$this->mContentModel && $this->getArticleID( $flags ) ) { + if ( ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE ) && + $this->getArticleID( $flags ) + ) { $linkCache = LinkCache::singleton(); $linkCache->addLinkObj( $this ); # in case we already had an article ID $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' ); @@ -1675,7 +1690,7 @@ class Title implements LinkTarget { $query = self::fixUrlQueryArgs( $query, $query2 ); - $interwiki = Interwiki::fetch( $this->mInterwiki ); + $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); if ( $interwiki ) { $namespace = $this->getNsText(); if ( $namespace != '' ) { @@ -1714,7 +1729,7 @@ class Title implements LinkTarget { if ( $url === false && $wgVariantArticlePath && preg_match( '/^variant=([^&]*)$/', $query, $matches ) - && $wgContLang->getCode() === $this->getPageLanguage()->getCode() + && $this->getPageLanguage()->equals( $wgContLang ) && $this->getPageLanguage()->hasVariants() ) { $variant = urldecode( $matches[1] ); @@ -2971,6 +2986,8 @@ class Title implements LinkTarget { /** * Purge expired restrictions from the page_restrictions table + * + * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows */ static function purgeExpiredRestrictions() { if ( wfReadOnly() ) { @@ -2981,11 +2998,24 @@ class Title implements LinkTarget { wfGetDB( DB_MASTER ), __METHOD__, function ( IDatabase $dbw, $fname ) { - $dbw->delete( + $config = MediaWikiServices::getInstance()->getMainConfig(); + $ids = $dbw->selectFieldValues( 'page_restrictions', + 'pr_id', [ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ], - $fname + $fname, + [ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470 ); + if ( $ids ) { + $dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname ); + } + } + ) ); + + DeferredUpdates::addUpdate( new AtomicSectionUpdate( + wfGetDB( DB_MASTER ), + __METHOD__, + function ( IDatabase $dbw, $fname ) { $dbw->delete( 'protected_titles', [ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ], diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php index eb652ce118..515fbfce86 100644 --- a/includes/WatchedItemStore.php +++ b/includes/WatchedItemStore.php @@ -1,7 +1,6 @@ deferredUpdatesAddCallableUpdateCallback; $this->deferredUpdatesAddCallableUpdateCallback = $callback; return new ScopedCallback( function() use ( $previousValue ) { @@ -106,14 +103,12 @@ class WatchedItemStore implements StatsdAwareInterface { * @return ScopedCallback to reset the overridden value * @throws MWException */ - public function overrideRevisionGetTimestampFromIdCallback( $callback ) { + public function overrideRevisionGetTimestampFromIdCallback( callable $callback ) { if ( !defined( 'MW_PHPUNIT_TEST' ) ) { throw new MWException( 'Cannot override Revision::getTimestampFromId callback in operation.' ); } - Assert::parameterType( 'callable', $callback, '$callback' ); - $previousValue = $this->revisionGetTimestampFromIdCallback; $this->revisionGetTimestampFromIdCallback = $callback; return new ScopedCallback( function() use ( $previousValue ) { @@ -145,16 +140,28 @@ class WatchedItemStore implements StatsdAwareInterface { } private function uncacheLinkTarget( LinkTarget $target ) { + $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' ); if ( !isset( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] ) ) { return; } - $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget' ); foreach ( $this->cacheIndex[$target->getNamespace()][$target->getDBkey()] as $key ) { $this->stats->increment( 'WatchedItemStore.uncacheLinkTarget.items' ); $this->cache->delete( $key ); } } + private function uncacheUser( User $user ) { + $this->stats->increment( 'WatchedItemStore.uncacheUser' ); + foreach ( $this->cacheIndex as $ns => $dbKeyArray ) { + foreach ( $dbKeyArray as $dbKey => $userArray ) { + if ( isset( $userArray[$user->getId()] ) ) { + $this->stats->increment( 'WatchedItemStore.uncacheUser.items' ); + $this->cache->delete( $userArray[$user->getId()] ); + } + } + } + } + /** * @param User $user * @param LinkTarget $target @@ -667,6 +674,41 @@ class WatchedItemStore implements StatsdAwareInterface { return $success; } + /** + * @param User $user The user to set the timestamp for + * @param string $timestamp Set the update timestamp to this value + * @param LinkTarget[] $targets List of targets to update. Default to all targets + * + * @return bool success + */ + public function setNotificationTimestampsForUser( User $user, $timestamp, array $targets = [] ) { + // Only loggedin user can have a watchlist + if ( $user->isAnon() ) { + return false; + } + + $dbw = $this->getConnection( DB_MASTER ); + + $conds = [ 'wl_user' => $user->getId() ]; + if ( $targets ) { + $batch = new LinkBatch( $targets ); + $conds[] = $batch->constructSet( 'wl', $dbw ); + } + + $success = $dbw->update( + 'watchlist', + [ 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) ], + $conds, + __METHOD__ + ); + + $this->reuseConnection( $dbw ); + + $this->uncacheUser( $user ); + + return $success; + } + /** * @param User $editor The editor that triggered the update. Their notification * timestamp will not be updated(they have already seen it) @@ -697,15 +739,24 @@ class WatchedItemStore implements StatsdAwareInterface { $fname = __METHOD__; $dbw->onTransactionIdle( function () use ( $dbw, $timestamp, $watchers, $target, $fname ) { - $dbw->update( 'watchlist', - [ /* SET */ - 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) - ], [ /* WHERE */ - 'wl_user' => $watchers, - 'wl_namespace' => $target->getNamespace(), - 'wl_title' => $target->getDBkey(), - ], $fname - ); + global $wgUpdateRowsPerQuery; + + $watchersChunks = array_chunk( $watchers, $wgUpdateRowsPerQuery ); + foreach ( $watchersChunks as $watchersChunk ) { + $dbw->update( 'watchlist', + [ /* SET */ + 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) + ], [ /* WHERE - TODO Use wl_id T130067 */ + 'wl_user' => $watchersChunk, + 'wl_namespace' => $target->getNamespace(), + 'wl_title' => $target->getDBkey(), + ], $fname + ); + if ( count( $watchersChunks ) > 1 ) { + $dbw->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $dbw->getWikiID() ] ); + } + } $this->uncacheLinkTarget( $target ); } ); diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 2333c78559..152a3d2d19 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -1249,6 +1249,26 @@ HTML; $this->ip = $ip; } + /** + * Check if this request uses a "safe" HTTP method + * + * Safe methods are verbs (e.g. GET/HEAD/OPTIONS) used for obtaining content. Such requests + * are not expected to mutate content, especially in ways attributable to the client. Verbs + * like POST and PUT are typical of non-safe requests which often change content. + * + * @return bool + * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html + * @since 1.28 + */ + public function hasSafeMethod() { + if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) { + return false; // CLI mode + } + + return in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS', 'TRACE' ] ); + } + /** * Whether this request should be identified as being "safe" * @@ -1268,21 +1288,15 @@ HTML; * @since 1.28 */ public function isSafeRequest() { - if ( !isset( $_SERVER['REQUEST_METHOD'] ) ) { - return false; // CLI mode - } - - if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { - return $this->markedAsSafe; - } elseif ( in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD', 'OPTIONS' ] ) ) { - return true; // HTTP "safe methods" + if ( $this->markedAsSafe && $this->wasPosted() ) { + return true; // marked as a "safe" POST } - return false; // PUT/DELETE + return $this->hasSafeMethod(); } /** - * Mark this request is identified as being nullipotent even if it is a POST request + * Mark this request as identified as being nullipotent even if it is a POST request * * POST requests are often used due to the need for a client payload, even if the request * is otherwise equivalent to a "safe method" request. diff --git a/includes/WebStart.php b/includes/WebStart.php index 29ad456c12..d063ce3d6e 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -26,6 +26,10 @@ * @file */ +if ( ini_get( 'mbstring.func_overload' ) ) { + die( 'MediaWiki does not support installations where mbstring.func_overload is non-zero.' ); +} + # bug 15461: Make IE8 turn off content sniffing. Everybody else should ignore this # We're adding it here so that it's *always* set, even for alternate entry # points and when $wgOut gets disabled or overridden. diff --git a/includes/actions/Action.php b/includes/actions/Action.php index 839d7b23e7..84bf16ee8f 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -62,7 +62,7 @@ abstract class Action { * the action is disabled, or null if it's not recognised * @param string $action * @param array $overrides - * @return bool|null|string|callable + * @return bool|null|string|callable|Action */ final private static function getClass( $action, array $overrides ) { global $wgActions; diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index b5f7ff2536..7be2aa7566 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -478,16 +478,18 @@ class InfoAction extends FormlessAction { if ( $firstRev ) { $firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER ); if ( $firstRevUser !== '' ) { - $batch->add( NS_USER, $firstRevUser ); - $batch->add( NS_USER_TALK, $firstRevUser ); + $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser ); + $batch->addObj( $firstRevUserTitle ); + $batch->addObj( $firstRevUserTitle->getTalkPage() ); } } if ( $lastRev ) { $lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER ); if ( $lastRevUser !== '' ) { - $batch->add( NS_USER, $lastRevUser ); - $batch->add( NS_USER_TALK, $lastRevUser ); + $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser ); + $batch->addObj( $lastRevUserTitle ); + $batch->addObj( $lastRevUserTitle->getTalkPage() ); } } diff --git a/includes/actions/RawAction.php b/includes/actions/RawAction.php index c7b18a4ba1..5bf24f60e6 100644 --- a/includes/actions/RawAction.php +++ b/includes/actions/RawAction.php @@ -80,6 +80,12 @@ class RawAction extends FormlessAction { } } + // Set standard Vary headers so cache varies on cookies and such (T125283) + $response->header( $this->getOutput()->getVaryHeader() ); + if ( $config->get( 'UseKeyHeader' ) ) { + $response->header( $this->getOutput()->getKeyHeader() ); + } + $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' ); // Output may contain user-specific data; // vary generated content for open sessions on private wikis diff --git a/includes/actions/RollbackAction.php b/includes/actions/RollbackAction.php index d002da8e89..3e760fd99b 100644 --- a/includes/actions/RollbackAction.php +++ b/includes/actions/RollbackAction.php @@ -35,39 +35,61 @@ class RollbackAction extends FormlessAction { return 'rollback'; } + /** + * Temporarily unused message keys due to T88044/T136375: + * - confirm-rollback-top + * - confirm-rollback-button + * - rollbackfailed + * - rollback-missingparam + */ + + /** + * @throws ErrorPageError + */ public function onView() { // TODO: use $this->useTransactionalTimeLimit(); when POST only wfTransactionalTimeLimit(); - $details = null; - $request = $this->getRequest(); $user = $this->getUser(); + $from = $request->getVal( 'from' ); + $rev = $this->page->getRevision(); + if ( $from === null || $from === '' ) { + throw new ErrorPageError( 'rollbackfailed', 'rollback-missingparam' ); + } + if ( $from !== $rev->getUserText() ) { + throw new ErrorPageError( 'rollbackfailed', 'alreadyrolled', [ + $this->getTitle()->getPrefixedText(), + $from, + $rev->getUserText() + ] ); + } - $result = $this->page->doRollback( - $request->getVal( 'from' ), + $data = null; + $errors = $this->page->doRollback( + $from, $request->getText( 'summary' ), $request->getVal( 'token' ), $request->getBool( 'bot' ), - $details, + $data, $this->getUser() ); - if ( in_array( [ 'actionthrottledtext' ], $result ) ) { + if ( in_array( [ 'actionthrottledtext' ], $errors ) ) { throw new ThrottledError; } - if ( isset( $result[0][0] ) && - ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) + if ( isset( $errors[0][0] ) && + ( $errors[0][0] == 'alreadyrolled' || $errors[0][0] == 'cantrollback' ) ) { $this->getOutput()->setPageTitle( $this->msg( 'rollbackfailed' ) ); - $errArray = $result[0]; + $errArray = $errors[0]; $errMsg = array_shift( $errArray ); $this->getOutput()->addWikiMsgArray( $errMsg, $errArray ); - if ( isset( $details['current'] ) ) { + if ( isset( $data['current'] ) ) { /** @var Revision $current */ - $current = $details['current']; + $current = $data['current']; if ( $current->getComment() != '' ) { $this->getOutput()->addHTML( $this->msg( 'editcomment' )->rawParams( @@ -79,21 +101,20 @@ class RollbackAction extends FormlessAction { } # NOTE: Permission errors already handled by Action::checkExecute. - - if ( $result == [ [ 'readonlytext' ] ] ) { + if ( $errors == [ [ 'readonlytext' ] ] ) { throw new ReadOnlyError; } # XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object. - # Right now, we only show the first error - foreach ( $result as $error ) { + # Right now, we only show the first error + foreach ( $errors as $error ) { throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) ); } /** @var Revision $current */ - $current = $details['current']; - $target = $details['target']; - $newId = $details['newid']; + $current = $data['current']; + $target = $data['target']; + $newId = $data['newid']; $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) ); $this->getOutput()->setRobotPolicy( 'noindex,nofollow' ); @@ -121,6 +142,7 @@ class RollbackAction extends FormlessAction { ); $de->showDiff( '', '' ); } + return; } protected function getDescription() { diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php index 806b8d2344..0a4b6dc214 100644 --- a/includes/api/ApiAMCreateAccount.php +++ b/includes/api/ApiAMCreateAccount.php @@ -109,9 +109,12 @@ class ApiAMCreateAccount extends ApiBase { } public function getAllowedParams() { - return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE, + $ret = ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE, 'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue' ); + $ret['preservestate'][ApiBase::PARAM_HELP_MSG_APPEND][] = + 'apihelp-createaccount-param-preservestate'; + return $ret; } public function dynamicParameterDocumentation() { diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php index 299740571b..e30f22b64e 100644 --- a/includes/api/ApiAuthManagerHelper.php +++ b/includes/api/ApiAuthManagerHelper.php @@ -244,7 +244,7 @@ class ApiAuthManagerHelper { $describe = $req->describeCredentials(); $reqInfo = [ 'id' => $req->getUniqueId(), - 'metadata' => $req->getMetadata(), + 'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ], ]; switch ( $req->required ) { case AuthenticationRequest::OPTIONAL: @@ -283,7 +283,6 @@ class ApiAuthManagerHelper { private function formatFields( array $fields ) { static $copy = [ 'type' => true, - 'image' => true, 'value' => true, ]; diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 7d0ae32c6a..639f6be0b8 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -503,7 +503,13 @@ abstract class ApiBase extends ContextSource { * @return bool */ public function lacksSameOriginSecurity() { - return $this->getMain()->getRequest()->getVal( 'callback' ) !== null; + // Main module has this method overridden + // Safety - avoid infinite loop: + if ( $this->isMain() ) { + ApiBase::dieDebug( __METHOD__, 'base method was called on main module.' ); + } + + return $this->getMain()->lacksSameOriginSecurity(); } /** diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php index 54547efe4b..aea28195f0 100644 --- a/includes/api/ApiChangeAuthenticationData.php +++ b/includes/api/ApiChangeAuthenticationData.php @@ -56,6 +56,7 @@ class ApiChangeAuthenticationData extends ApiBase { // Make the change $status = $manager->allowsAuthenticationDataChange( $req, true ); + Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] ); if ( !$status->isGood() ) { $this->dieStatus( $status ); } diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php index 711234a65b..cffccb15b5 100644 --- a/includes/api/ApiClientLogin.php +++ b/includes/api/ApiClientLogin.php @@ -23,6 +23,7 @@ use MediaWiki\Auth\AuthManager; use MediaWiki\Auth\AuthenticationRequest; use MediaWiki\Auth\AuthenticationResponse; +use MediaWiki\Auth\CreateFromLoginAuthenticationRequest; /** * Log in to the wiki with AuthManager @@ -90,6 +91,13 @@ class ApiClientLogin extends ApiBase { $res = $manager->beginAuthentication( $reqs, $params['returnurl'] ); } + // Remove CreateFromLoginAuthenticationRequest from $res->neededRequests. + // It's there so a RESTART treated as UI will work right, but showing + // it to the API client is just confusing. + $res->neededRequests = ApiAuthManagerHelper::blacklistAuthenticationRequests( + $res->neededRequests, [ CreateFromLoginAuthenticationRequest::class ] + ); + $this->getResult()->addValue( null, 'clientlogin', $helper->formatAuthenticationResponse( $res ) ); } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index e28b0684d5..c7dc303ada 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -79,6 +79,7 @@ class ApiFeedContributions extends ApiBase { 'deletedOnly' => $params['deletedonly'], 'topOnly' => $params['toponly'], 'newOnly' => $params['newonly'], + 'hideMinor' => $params['hideminor'], 'showSizeDiff' => $params['showsizediff'], ] ); @@ -208,6 +209,7 @@ class ApiFeedContributions extends ApiBase { 'deletedonly' => false, 'toponly' => false, 'newonly' => false, + 'hideminor' => false, 'showsizediff' => [ ApiBase::PARAM_DFLT => false, ], diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 41de9253f6..814450ecb9 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -103,9 +103,9 @@ class ApiFormatJson extends ApiFormatBase { // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API, so we need to // work around it. - if ( preg_match( '/\<\s*cross-domain-policy\s*\>/i', $json ) ) { + if ( preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $json ) ) { $json = preg_replace( - '/\<(\s*cross-domain-policy\s*)\>/i', '\\u003C$1\\u003E', $json + '/\<(\s*cross-domain-policy(?=\s|\>))/i', '\\u003C$1', $json ); } diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index d111af5d33..fc25f47723 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -65,7 +65,7 @@ class ApiFormatPhp extends ApiFormatBase { // just be broken in a useful manner. if ( $this->getConfig()->get( 'MangleFlashPolicy' ) && in_array( 'wfOutputHandler', ob_list_handlers(), true ) && - preg_match( '/\<\s*cross-domain-policy\s*\>/i', $text ) + preg_match( '/\<\s*cross-domain-policy(?=\s|\>)/i', $text ) ) { $this->dieUsage( 'This response cannot be represented using format=php. ' . diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index b9443859ea..ce9587f399 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -148,6 +148,9 @@ class ApiMain extends ApiBase { private $mCacheControl = []; private $mParamsUsed = []; + /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */ + private $lacksSameOriginSecurity = null; + /** * Constructs an instance of ApiMain that utilizes the module and format specified by $request. * @@ -245,6 +248,35 @@ class ApiMain extends ApiBase { return $this->mResult; } + /** + * Get the security flag for the current request + * @return bool + */ + public function lacksSameOriginSecurity() { + if ( $this->lacksSameOriginSecurity !== null ) { + return $this->lacksSameOriginSecurity; + } + + $request = $this->getRequest(); + + // JSONP mode + if ( $request->getVal( 'callback' ) !== null ) { + $this->lacksSameOriginSecurity = true; + return true; + } + + // Header to be used from XMLHTTPRequest when the request might + // otherwise be used for XSS. + if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) { + $this->lacksSameOriginSecurity = true; + return true; + } + + // Allow extensions to override. + $this->lacksSameOriginSecurity = !Hooks::run( 'RequestHasSameOriginSecurity', [ $request ] ); + return $this->lacksSameOriginSecurity; + } + /** * Get the ApiErrorFormatter object associated with current request * @return ApiErrorFormatter @@ -439,7 +471,8 @@ class ApiMain extends ApiBase { $this->logRequest( $runTime ); if ( $this->mModule->isWriteMode() && $this->getRequest()->wasPosted() ) { $this->getStats()->timing( - 'api.' . $this->getModuleName() . '.executeTiming', 1000 * $runTime ); + 'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime + ); } } catch ( Exception $e ) { $this->handleException( $e ); @@ -730,6 +763,8 @@ class ApiMain extends ApiBase { $response = $this->getRequest()->response(); $out = $this->getOutput(); + $out->addVaryHeader( 'Treat-as-Untrusted' ); + $config = $this->getConfig(); if ( $config->get( 'VaryOnXFP' ) ) { @@ -1356,15 +1391,13 @@ class ApiMain extends ApiBase { protected function setRequestExpectations( ApiBase $module ) { $limits = $this->getConfig()->get( 'TrxProfilerLimits' ); $trxProfiler = Profiler::instance()->getTransactionProfiler(); - if ( $this->getRequest()->wasPosted() ) { - if ( $module->isWriteMode() ) { - $trxProfiler->setExpectations( $limits['POST'], __METHOD__ ); - } else { - $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ ); - $this->getRequest()->markAsSafeRequest(); - } - } else { + if ( $this->getRequest()->hasSafeMethod() ) { $trxProfiler->setExpectations( $limits['GET'], __METHOD__ ); + } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) { + $trxProfiler->setExpectations( $limits['POST-nonwrite'], __METHOD__ ); + $this->getRequest()->markAsSafeRequest(); + } else { + $trxProfiler->setExpectations( $limits['POST'], __METHOD__ ); } } @@ -1645,9 +1678,14 @@ class ApiMain extends ApiBase { $tocnumber = &$options['tocnumber']; $header = $this->msg( 'api-help-datatypes-header' )->parse(); + + // Add an additional span with sanitized ID + if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) { + $header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/datatypes' ) ] ) . + $header; + } $help['datatypes'] .= Html::rawElement( 'h' . min( 6, $level ), [ 'id' => 'main/datatypes', 'class' => 'apihelp-header' ], - Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/datatypes' ) ] ) . $header ); $help['datatypes'] .= $this->msg( 'api-help-datatypes' )->parseAsBlock(); @@ -1663,10 +1701,14 @@ class ApiMain extends ApiBase { ]; } + // Add an additional span with sanitized ID + if ( !$this->getConfig()->get( 'ExperimentalHtmlIds' ) ) { + $header = Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/credits' ) ] ) . + $header; + } $header = $this->msg( 'api-credits-header' )->parse(); $help['credits'] .= Html::rawElement( 'h' . min( 6, $level ), [ 'id' => 'main/credits', 'class' => 'apihelp-header' ], - Html::element( 'span', [ 'id' => Sanitizer::escapeId( 'main/credits' ) ] ) . $header ); $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock(); diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 2fbd50e2e5..29e67b07cd 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -72,6 +72,11 @@ class ApiMove extends ApiBase { } } + // Rate limit + if ( $user->pingLimiter( 'move' ) ) { + $this->dieUsageMsg( 'actionthrottledtext' ); + } + // Move the page $toTitleExists = $toTitle->exists(); $status = $this->movePage( $fromTitle, $toTitle, $params['reason'], !$params['noredirect'] ); diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 058e0a3909..066aaa3bca 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -30,10 +30,14 @@ use MediaWiki\MediaWikiServices; * @ingroup API */ class ApiOpenSearch extends ApiBase { + use SearchApi; private $format = null; private $fm = null; + /** @var array list of api allowed params */ + private $allowedParams = null; + /** * Get the output format * @@ -80,24 +84,13 @@ class ApiOpenSearch extends ApiBase { public function execute() { $params = $this->extractRequestParams(); $search = $params['search']; - $limit = $params['limit']; - $namespaces = $params['namespace']; $suggest = $params['suggest']; - - if ( $params['redirects'] === null ) { - // Backwards compatibility, don't resolve for JSON. - $resolveRedir = $this->getFormat() !== 'json'; - } else { - $resolveRedir = $params['redirects'] === 'resolve'; - } - $results = []; - if ( !$suggest || $this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) { // Open search results may be stored for a very long time $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) ); $this->getMain()->setCacheMode( 'public' ); - $this->search( $search, $limit, $namespaces, $resolveRedir, $results ); + $results = $this->search( $search, $params ); // Allow hooks to populate extracts and images Hooks::run( 'ApiOpenSearchSuggest', [ &$results ] ); @@ -117,21 +110,17 @@ class ApiOpenSearch extends ApiBase { /** * Perform the search - * - * @param string $search Text to search - * @param int $limit Maximum items to return - * @param array $namespaces Namespaces to search - * @param bool $resolveRedir Whether to resolve redirects - * @param array &$results Put results here. Keys have to be integers. + * @param string $search the search query + * @param array $params api request params + * @return array search results. Keys are integers. */ - protected function search( $search, $limit, $namespaces, $resolveRedir, &$results ) { - $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); - $searchEngine->setLimitOffset( $limit ); - $searchEngine->setNamespaces( $namespaces ); + private function search( $search, array $params ) { + $searchEngine = $this->buildSearchEngine( $params ); $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); + $results = []; if ( !$titles ) { - return; + return $results; } // Special pages need unique integer ids in the return list, so we just @@ -139,6 +128,13 @@ class ApiOpenSearch extends ApiBase { // always positive articleIds that non-special pages get. $nextSpecialPageId = -1; + if ( $params['redirects'] === null ) { + // Backwards compatibility, don't resolve for JSON. + $resolveRedir = $this->getFormat() !== 'json'; + } else { + $resolveRedir = $params['redirects'] === 'resolve'; + } + if ( $resolveRedir ) { // Query for redirects $redirects = []; @@ -206,6 +202,8 @@ class ApiOpenSearch extends ApiBase { ]; } } + + return $results; } /** @@ -271,7 +269,10 @@ class ApiOpenSearch extends ApiBase { } public function getAllowedParams() { - return [ + if ( $this->allowedParams !== null ) { + return $this->allowedParams; + } + $this->allowedParams = [ 'search' => null, 'limit' => [ ApiBase::PARAM_DFLT => $this->getConfig()->get( 'OpenSearchDefaultLimit' ), @@ -295,6 +296,20 @@ class ApiOpenSearch extends ApiBase { ], 'warningsaserror' => false, ]; + + $profileParam = $this->buildProfileApiParam( SearchEngine::COMPLETION_PROFILE_TYPE, + 'apihelp-query+prefixsearch-param-profile' ); + if ( $profileParam ) { + $this->allowedParams['profile'] = $profileParam; + } + return $this->allowedParams; + } + + public function getSearchProfileParams() { + if ( isset( $this->getAllowedParams()['profile'] ) ) { + return [ SearchEngine::COMPLETION_PROFILE_TYPE => 'profile' ]; + } + return []; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 3ca4c08da4..ed4d373a7c 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -554,23 +554,34 @@ class ApiQuery extends ApiBase { } public function isReadMode() { - // We need to make an exception for ApiQueryTokens so login tokens can - // be fetched on private wikis. Restrict that exception as much as - // possible: no other modules allowed, and no pageset parameters - // either. We do allow the 'rawcontinue' and 'indexpageids' parameters - // since frameworks might add these unconditionally and they can't - // expose anything here. + // We need to make an exception for certain meta modules that should be + // accessible even without the 'read' right. Restrict the exception as + // much as possible: no other modules allowed, and no pageset + // parameters either. We do allow the 'rawcontinue' and 'indexpageids' + // parameters since frameworks might add these unconditionally and they + // can't expose anything here. + $this->mParams = $this->extractRequestParams(); $params = array_filter( array_diff_key( - $this->extractRequestParams() + $this->getPageSet()->extractRequestParams(), + $this->mParams + $this->getPageSet()->extractRequestParams(), [ 'rawcontinue' => 1, 'indexpageids' => 1 ] ) ); - if ( $params === [ 'meta' => [ 'tokens' ] ] ) { - return false; + if ( array_keys( $params ) !== [ 'meta' ] ) { + return true; + } + + // Ask each module if it requires read mode. Any true => this returns + // true. + $modules = []; + $this->instantiateModules( $modules, 'meta' ); + foreach ( $modules as $module ) { + if ( $module->isReadMode() ) { + return true; + } } - return true; + return false; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index f1d787b8b3..e0ba4ea1c1 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -113,15 +113,14 @@ class ApiQueryAllMessages extends ApiQueryBase { $customiseFilterEnabled = $params['customised'] !== 'all'; if ( $customiseFilterEnabled ) { global $wgContLang; - $lang = $langObj->getCode(); $customisedMessages = AllMessagesTablePager::getCustomisedStatuses( array_map( [ $langObj, 'ucfirst' ], $messages_target ), - $lang, - $lang != $wgContLang->getCode() + $langObj->getCode(), + !$langObj->equals( $wgContLang ) ); $customised = $params['customised'] === 'modified'; diff --git a/includes/api/ApiQueryAuthManagerInfo.php b/includes/api/ApiQueryAuthManagerInfo.php index b591f9c00a..1d250e97d1 100644 --- a/includes/api/ApiQueryAuthManagerInfo.php +++ b/includes/api/ApiQueryAuthManagerInfo.php @@ -43,7 +43,6 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { 'canauthenticatenow' => $manager->canAuthenticateNow(), 'cancreateaccounts' => $manager->canCreateAccounts(), 'canlinkaccounts' => $manager->canLinkAccounts(), - 'haspreservedstate' => $helper->getPreservedRequest() !== null, ]; if ( $params['securitysensitiveoperation'] !== null ) { @@ -53,10 +52,27 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { } if ( $params['requestsfor'] ) { - $reqs = $manager->getAuthenticationRequests( $params['requestsfor'], $this->getUser() ); + $action = $params['requestsfor']; + + $preservedReq = $helper->getPreservedRequest(); + if ( $preservedReq ) { + $ret += [ + 'haspreservedstate' => $preservedReq->hasStateForAction( $action ), + 'hasprimarypreservedstate' => $preservedReq->hasPrimaryStateForAction( $action ), + 'preservedusername' => (string)$preservedReq->username, + ]; + } else { + $ret += [ + 'haspreservedstate' => false, + 'hasprimarypreservedstate' => false, + 'preservedusername' => '', + ]; + } + + $reqs = $manager->getAuthenticationRequests( $action, $this->getUser() ); // Filter out blacklisted requests, depending on the action - switch ( $params['requestsfor'] ) { + switch ( $action ) { case AuthManager::ACTION_CHANGE: $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests( $reqs, $this->getConfig()->get( 'ChangeCredentialsBlacklist' ) @@ -75,8 +91,8 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { $this->getResult()->addValue( [ 'query' ], $this->getModuleName(), $ret ); } - public function getCacheMode( $params ) { - return 'public'; + public function isReadMode() { + return false; } public function getAllowedParams() { @@ -95,7 +111,7 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { AuthManager::ACTION_UNLINK, ], ], - ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields' ); + ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields', 'messageformat' ); } protected function getExamplesMessages() { diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php index 5c50273261..46538e0eb1 100644 --- a/includes/api/ApiQueryPrefixSearch.php +++ b/includes/api/ApiQueryPrefixSearch.php @@ -25,6 +25,11 @@ use MediaWiki\MediaWikiServices; * @ingroup API */ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { + use SearchApi; + + /** @var array list of api allowed params */ + private $allowedParams; + public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ps' ); } @@ -44,12 +49,9 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; - $namespaces = $params['namespace']; $offset = $params['offset']; - $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); - $searchEngine->setLimitOffset( $limit + 1, $offset ); - $searchEngine->setNamespaces( $namespaces ); + $searchEngine = $this->buildSearchEngine( $params ); $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); if ( $resultPageSet ) { @@ -60,7 +62,7 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { return $current; } ); if ( count( $titles ) > $limit ) { - $this->setContinueEnumParameter( 'offset', $offset + $params['limit'] ); + $this->setContinueEnumParameter( 'offset', $offset + $limit ); array_pop( $titles ); } $resultPageSet->populateFromTitles( $titles ); @@ -72,7 +74,7 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { $count = 0; foreach ( $titles as $title ) { if ( ++$count > $limit ) { - $this->setContinueEnumParameter( 'offset', $offset + $params['limit'] ); + $this->setContinueEnumParameter( 'offset', $offset + $limit ); break; } $vals = [ @@ -101,29 +103,45 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - return [ - 'search' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'namespace' => [ - ApiBase::PARAM_DFLT => NS_MAIN, - ApiBase::PARAM_TYPE => 'namespace', - ApiBase::PARAM_ISMULTI => true, - ], - 'limit' => [ - ApiBase::PARAM_DFLT => 10, - ApiBase::PARAM_TYPE => 'limit', - ApiBase::PARAM_MIN => 1, - // Non-standard value for compatibility with action=opensearch - ApiBase::PARAM_MAX => 100, - ApiBase::PARAM_MAX2 => 200, - ], - 'offset' => [ - ApiBase::PARAM_DFLT => 0, - ApiBase::PARAM_TYPE => 'integer', - ], - ]; + if ( $this->allowedParams !== null ) { + return $this->allowedParams; + } + $this->allowedParams = [ + 'search' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ], + 'namespace' => [ + ApiBase::PARAM_DFLT => NS_MAIN, + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_ISMULTI => true, + ], + 'limit' => [ + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + // Non-standard value for compatibility with action=opensearch + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 200, + ], + 'offset' => [ + ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_TYPE => 'integer', + ], + ]; + $profileParam = $this->buildProfileApiParam( SearchEngine::COMPLETION_PROFILE_TYPE, + 'apihelp-query+prefixsearch-param-profile' ); + if ( $profileParam ) { + $this->allowedParams['profile'] = $profileParam; + } + return $this->allowedParams; + } + + public function getSearchProfileParams() { + if ( isset( $this->getAllowedParams()['profile'] ) ) { + return [ SearchEngine::COMPLETION_PROFILE_TYPE => 'profile' ]; + } + return []; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 64022ff2fa..b816f43842 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -80,8 +80,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { return false; } - return $wgUser->getEditToken( - [ $title->getPrefixedText(), $rev->getUserText() ] ); + return $wgUser->getEditToken( 'rollback' ); } protected function run( ApiPageSet $resultPageSet = null ) { diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index f57d3a30cf..80798a10cd 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -32,6 +32,10 @@ use MediaWiki\MediaWikiServices; * @ingroup API */ class ApiQuerySearch extends ApiQueryGeneratorBase { + use SearchApi; + + /** @var array list of api allowed params */ + private $allowedParams; /** * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't @@ -61,8 +65,11 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { global $wgContLang; $params = $this->extractRequestParams(); + if ( isset( $params['backend'] ) && $params['backend'] == self::BACKEND_NULL_PARAM ) { + unset( $params['backend'] ); + } + // Extract parameters - $limit = $params['limit']; $query = $params['search']; $what = $params['what']; $interwiki = $params['interwiki']; @@ -80,11 +87,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } // Create search engine instance and set options - $type = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? - $params['backend'] : null; - $search = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); - $search->setLimitOffset( $limit + 1, $params['offset'] ); - $search->setNamespaces( $params['namespace'] ); + $search = $this->buildSearchEngine( $params ); $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] ); $query = $search->transformSearchTerm( $query ); @@ -152,6 +155,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $titles = []; $count = 0; $result = $matches->next(); + $limit = $params['limit']; while ( $result ) { if ( ++$count > $limit ) { @@ -301,7 +305,11 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - $params = [ + if ( $this->allowedParams !== null ) { + return $this->allowedParams; + } + + $this->allowedParams = [ 'search' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true @@ -368,13 +376,31 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { if ( $alternatives[0] === null ) { $alternatives[0] = self::BACKEND_NULL_PARAM; } - $params['backend'] = [ + $this->allowedParams['backend'] = [ ApiBase::PARAM_DFLT => $searchConfig->getSearchType(), ApiBase::PARAM_TYPE => $alternatives, ]; + // @todo: support profile selection when multiple + // backends are available. The solution could be to + // merge all possible profiles and let ApiBase + // subclasses do the check. Making ApiHelp and ApiSandbox + // comprehensive might be more difficult. + } else { + $profileParam = $this->buildProfileApiParam( SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE, + 'apihelp-query+search-param-qiprofile' ); + if ( $profileParam ) { + $this->allowedParams['qiprofile'] = $profileParam; + } } - return $params; + return $this->allowedParams; + } + + public function getSearchProfileParams() { + if ( isset( $this->getAllowedParams()['qiprofile'] ) ) { + return [ SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE => 'qiprofile' ]; + } + return []; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index a08740a43e..590a71265e 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -245,7 +245,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['uploadsenabled'] = UploadBase::isEnabled(); $data['maxuploadsize'] = UploadBase::getMaxUploadSize(); - $data['minuploadchunksize'] = (int)$this->getConfig()->get( 'MinUploadChunkSize' ); + $data['minuploadchunksize'] = (int)$config->get( 'MinUploadChunkSize' ); $data['thumblimits'] = $config->get( 'ThumbLimits' ); ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' ); @@ -264,10 +264,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['favicon'] = wfExpandUrl( $favicon, PROTO_RELATIVE ); } - $data['centralidlookupprovider'] = $this->getConfig()->get( 'CentralIdLookupProvider' ); - $providerIds = array_keys( $this->getConfig()->get( 'CentralIdLookupProviders' ) ); + $data['centralidlookupprovider'] = $config->get( 'CentralIdLookupProvider' ); + $providerIds = array_keys( $config->get( 'CentralIdLookupProviders' ) ); $data['allcentralidlookupproviders'] = $providerIds; + $data['interwikimagic'] = (bool)$config->get( 'InterwikiMagic' ); + Hooks::run( 'APIQuerySiteInfoGeneralInfo', [ $this, &$data ] ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -485,7 +487,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data = []; $result = $this->getResult(); - $allGroups = User::getAllGroups(); + $allGroups = array_values( User::getAllGroups() ); foreach ( $config->get( 'GroupPermissions' ) as $group => $permissions ) { $arr = [ 'name' => $group, @@ -512,7 +514,11 @@ class ApiQuerySiteinfo extends ApiQueryBase { foreach ( $groupArr as $type => $rights ) { if ( isset( $rights[$group] ) ) { - $groups = array_intersect( $rights[$group], $allGroups ); + if ( $rights[$group] === true ) { + $groups = $allGroups; + } else { + $groups = array_intersect( $rights[$group], $allGroups ); + } if ( $groups ) { $arr[$type] = $groups; ApiResult::setArrayType( $arr[$type], 'BCarray' ); diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 68ec38dd9f..5afb66f225 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -262,8 +262,11 @@ class ApiQueryUsers extends ApiQueryBase { } else { $data[$u]['missing'] = true; if ( isset( $this->prop['cancreate'] ) && !$this->getConfig()->get( 'DisableAuthManager' ) ) { - $data[$u]['cancreate'] = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u ) - ->isGood(); + $status = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u ); + $data[$u]['cancreate'] = $status->isGood(); + if ( !$status->isGood() ) { + $data[$u]['cancreateerror'] = $this->getErrorFormatter()->arrayFromStatus( $status ); + } } } } else { diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php index 30e40fb2b6..d72c8a407e 100644 --- a/includes/api/ApiRemoveAuthenticationData.php +++ b/includes/api/ApiRemoveAuthenticationData.php @@ -73,6 +73,7 @@ class ApiRemoveAuthenticationData extends ApiBase { // Perform the removal $status = $manager->allowsAuthenticationDataChange( $req, true ); + Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] ); if ( !$status->isGood() ) { $this->dieStatus( $status ); } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 55f7143719..b9911da138 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -45,16 +45,6 @@ class ApiRollback extends ApiBase { $user = $this->getUser(); $params = $this->extractRequestParams(); - // WikiPage::doRollback needs a Web UI token, so get one of those if we - // validated based on an API rollback token. - $token = $params['token']; - if ( $user->matchEditToken( $token, 'rollback', $this->getRequest() ) ) { - $token = $this->getUser()->getEditToken( - $this->getWebUITokenSalt( $params ), - $this->getRequest() - ); - } - $titleObj = $this->getRbTitle( $params ); $pageObj = WikiPage::factory( $titleObj ); $summary = $params['summary']; @@ -72,15 +62,30 @@ class ApiRollback extends ApiBase { $retval = $pageObj->doRollback( $this->getRbUser( $params ), $summary, - $token, + $params['token'], $params['markbot'], $details, $user, $params['tags'] ); + // We don't care about multiple errors, just report one of them if ( $retval ) { - // We don't care about multiple errors, just report one of them + if ( isset( $retval[0][0] ) && + ( $retval[0][0] == 'alreadyrolled' || $retval[0][0] == 'cantrollback' ) + ) { + $error = $retval[0]; + $userMessage = $this->msg( $error[0], array_slice( $error, 1 ) ); + // dieUsageMsg() doesn't support $extraData + $errorCode = $error[0]; + $errorInfo = isset( ApiBase::$messageMap[$errorCode] ) ? + ApiBase::$messageMap[$errorCode]['info'] : + $errorCode; + $this->dieUsage( $errorInfo, $errorCode, 0, [ + 'messageHtml' => $userMessage->parseAsBlock() + ] ); + } + $this->dieUsageMsg( reset( $retval ) ); } @@ -97,10 +102,23 @@ class ApiRollback extends ApiBase { 'pageid' => intval( $details['current']->getPage() ), 'summary' => $details['summary'], 'revid' => intval( $details['newid'] ), + // The revision being reverted (previously the current revision of the page) 'old_revid' => intval( $details['current']->getID() ), + // The revision being restored (the last revision before revision(s) by the reverted user) 'last_revid' => intval( $details['target']->getID() ) ]; + $oldUser = $details['current']->getUserText( Revision::FOR_THIS_USER ); + $lastUser = $details['target']->getUserText( Revision::FOR_THIS_USER ); + $diffUrl = $titleObj->getFullURL( [ + 'diff' => $info['revid'], + 'oldid' => $info['old_revid'], + 'diffonly' => '1' + ] ); + $info['messageHtml'] = $this->msg( 'rollback-success-notify' ) + ->params( $oldUser, $lastUser, $diffUrl ) + ->parseAsBlock(); + $this->getResult()->addValue( null, $this->getModuleName(), $info ); } @@ -148,13 +166,6 @@ class ApiRollback extends ApiBase { return 'rollback'; } - protected function getWebUITokenSalt( array $params ) { - return [ - $this->getRbTitle( $params )->getPrefixedText(), - $this->getRbUser( $params ) - ]; - } - /** * @param array $params * diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index ea52e1433a..f3356821ef 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -24,6 +24,7 @@ * * @file */ +use MediaWiki\MediaWikiServices; /** * API interface for setting the wl_notificationtimestamp field @@ -98,13 +99,14 @@ class ApiSetNotificationTimestamp extends ApiBase { } } + $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore(); $apiResult = $this->getResult(); $result = []; if ( $params['entirewatchlist'] ) { // Entire watchlist mode: Just update the thing and return a success indicator - $dbw->update( 'watchlist', [ 'wl_notificationtimestamp' => $timestamp ], - [ 'wl_user' => $user->getId() ], - __METHOD__ + $watchedItemStore->setNotificationTimestampsForUser( + $user, + $timestamp ); $result['notificationtimestamp'] = is_null( $timestamp ) @@ -133,23 +135,17 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( $pageSet->getTitles() ) { // Now process the valid titles - $lb = new LinkBatch( $pageSet->getTitles() ); - $dbw->update( 'watchlist', [ 'wl_notificationtimestamp' => $timestamp ], - [ 'wl_user' => $user->getId(), $lb->constructSet( 'wl', $dbw ) ], - __METHOD__ + $watchedItemStore->setNotificationTimestampsForUser( + $user, + $timestamp, + $pageSet->getTitles() ); // Query the results of our update - $timestamps = []; - $res = $dbw->select( - 'watchlist', - [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ], - [ 'wl_user' => $user->getId(), $lb->constructSet( 'wl', $dbw ) ], - __METHOD__ + $timestamps = $watchedItemStore->getNotificationTimestampsBatch( + $user, + $pageSet->getTitles() ); - foreach ( $res as $row ) { - $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; - } // Now, put the valid titles into the result /** @var $title Title */ diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index 3539eed4f0..814a111ee4 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -41,11 +41,16 @@ class ApiStashEdit extends ApiBase { const ERROR_UNCACHEABLE = 'uncacheable'; const PRESUME_FRESH_TTL_SEC = 30; + const MAX_CACHE_TTL = 300; // 5 minutes public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); + if ( $user->isBot() ) { // sanity + $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' ); + } + $page = $this->getTitleOrPageId( $params ); $title = $page->getTitle(); @@ -123,6 +128,8 @@ class ApiStashEdit extends ApiBase { $status = 'busy'; } + $this->getStats()->increment( "editstash.cache_stores.$status" ); + $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => $status ] ); } @@ -139,9 +146,10 @@ class ApiStashEdit extends ApiBase { $format = $content->getDefaultFormat(); $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false ); + $title = $page->getTitle(); if ( $editInfo && $editInfo->output ) { - $key = self::getStashKey( $page->getTitle(), $content, $user ); + $key = self::getStashKey( $title, $content, $user ); // Let extensions add ParserOutput metadata or warm other caches Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] ); @@ -156,14 +164,14 @@ class ApiStashEdit extends ApiBase { if ( $stashInfo ) { $ok = $cache->set( $key, $stashInfo, $ttl ); if ( $ok ) { - $logger->debug( "Cached parser output for key '$key'." ); + $logger->debug( "Cached parser output for key '$key' ('$title')." ); return self::ERROR_NONE; } else { - $logger->error( "Failed to cache parser output for key '$key'." ); + $logger->error( "Failed to cache parser output for key '$key' ('$title')." ); return self::ERROR_CACHE; } } else { - $logger->info( "Uncacheable parser output for key '$key'." ); + $logger->info( "Uncacheable parser output for key '$key' ('$title')." ); return self::ERROR_UNCACHEABLE; } } @@ -209,7 +217,8 @@ class ApiStashEdit extends ApiBase { // PST parser options are for the user (handles signatures, etc...) $user = $pstOpts->getUser(); // Get a key based on the source text, format, and user preferences - $key = self::getStashKey( $page->getTitle(), $content, $user ); + $title = $page->getTitle(); + $key = self::getStashKey( $title, $content, $user ); // Parser output options must match cannonical options. // Treat some options as matching that are different but don't matter. @@ -217,7 +226,7 @@ class ApiStashEdit extends ApiBase { $canonicalPOpts->setIsPreview( true ); // force match $canonicalPOpts->setTimestamp( $pOpts->getTimestamp() ); // force match if ( !$pOpts->matches( $canonicalPOpts ) ) { - $logger->info( "Uncacheable preview output for key '$key' (options)." ); + $logger->info( "Uncacheable preview output for key '$key' ('$title') [options]." ); return false; } @@ -227,13 +236,13 @@ class ApiStashEdit extends ApiBase { // Build a value to cache with a proper TTL list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp, $user ); if ( !$stashInfo ) { - $logger->info( "Uncacheable parser output for key '$key' (rev/TTL)." ); + $logger->info( "Uncacheable parser output for key '$key' ('$title') [rev/TTL]." ); return false; } $ok = $cache->set( $key, $stashInfo, $ttl ); if ( !$ok ) { - $logger->error( "Failed to cache preview parser output for key '$key'." ); + $logger->error( "Failed to cache preview parser output for key '$key' ('$title')." ); } else { $logger->debug( "Cached preview output for key '$key'." ); } @@ -259,6 +268,10 @@ class ApiStashEdit extends ApiBase { * @return stdClass|bool Returns false on cache miss */ public static function checkCache( Title $title, Content $content, User $user ) { + if ( $user->isBot() ) { + return false; // bots never stash - don't pollute stats + } + $cache = ObjectCache::getLocalClusterInstance(); $logger = LoggerFactory::getInstance( 'StashEdit' ); $stats = RequestContext::getMain()->getStats(); @@ -284,7 +297,7 @@ class ApiStashEdit extends ApiBase { if ( !is_object( $editInfo ) || !$editInfo->output ) { $stats->increment( 'editstash.cache_misses.no_stash' ); - $logger->debug( "No cache value for key '$key'." ); + $logger->debug( "Empty cache for key '$key' ('$title'); user '{$user->getName()}'." ); return false; } @@ -294,69 +307,38 @@ class ApiStashEdit extends ApiBase { $logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." ); return $editInfo; // assume nothing changed } elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) { + // Logged-in user made no local upload/template edits in the meantime $stats->increment( 'editstash.cache_hits.presumed_fresh' ); $logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." ); - return $editInfo; // use made no local upload/template edits in the meantime - } - - $dbr = wfGetDB( DB_SLAVE ); - - $templates = []; // conditions to find changes/creations - $templateUses = 0; // expected existing templates - foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) { - foreach ( $stuff as $dbkey => $revId ) { - $templates[(string)$ns][$dbkey] = (int)$revId; - ++$templateUses; - } - } - // Check that no templates used in the output changed... - if ( count( $templates ) ) { - $res = $dbr->select( - 'page', - [ 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ], - $dbr->makeWhereFrom2d( $templates, 'page_namespace', 'page_title' ), - __METHOD__ - ); - $changed = false; - foreach ( $res as $row ) { - $changed = $changed || ( $row->page_latest != $templates[$row->ns][$row->dbk] ); - } - - if ( $changed || $res->numRows() != $templateUses ) { - $stats->increment( 'editstash.cache_misses.proven_stale' ); - $logger->info( "Stale cache for key '$key'; template changed. (age: $age sec)" ); - return false; - } - } - - $files = []; // conditions to find changes/creations - foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) { - $files[$name] = (string)$options['sha1']; + return $editInfo; + } elseif ( $user->isAnon() + && self::lastEditTime( $user ) < $editInfo->output->getCacheTime() + ) { + // Logged-out user made no local upload/template edits in the meantime + $stats->increment( 'editstash.cache_hits.presumed_fresh' ); + $logger->debug( "Edit check based cache hit for key '$key' (age: $age sec)." ); + return $editInfo; } - // Check that no files used in the output changed... - if ( count( $files ) ) { - $res = $dbr->select( - 'image', - [ 'name' => 'img_name', 'img_sha1' ], - [ 'img_name' => array_keys( $files ) ], - __METHOD__ - ); - $changed = false; - foreach ( $res as $row ) { - $changed = $changed || ( $row->img_sha1 != $files[$row->name] ); - } - if ( $changed || $res->numRows() != count( $files ) ) { - $stats->increment( 'editstash.cache_misses.proven_stale' ); - $logger->info( "Stale cache for key '$key'; file changed. (age: $age sec)" ); - return false; - } - } + $stats->increment( 'editstash.cache_misses.proven_stale' ); + $logger->info( "Stale cache for key '$key'; old key with outside edits. (age: $age sec)" ); - $stats->increment( 'editstash.cache_hits.proven_fresh' ); - $logger->debug( "Verified cache hit for key '$key' (age: $age sec)." ); + return false; + } - return $editInfo; + /** + * @param User $user + * @return string|null TS_MW timestamp or null + */ + private static function lastEditTime( User $user ) { + $time = wfGetDB( DB_SLAVE )->selectField( + 'recentchanges', + 'MAX(rc_timestamp)', + [ 'rc_user_text' => $user->getName() ], + __METHOD__ + ); + + return wfTimestampOrNull( TS_MW, $time ); } /** @@ -371,13 +353,15 @@ class ApiStashEdit extends ApiBase { * @param User $user User to get parser options from * @return string */ - protected static function getStashKey( Title $title, Content $content, User $user ) { + private static function getStashKey( Title $title, Content $content, User $user ) { $hash = sha1( implode( ':', [ + // Account for the edit model/text $content->getModel(), $content->getDefaultFormat(), sha1( $content->serialize( $content->getDefaultFormat() ) ), - $user->getId() ?: md5( $user->getName() ), // account for user parser options - $user->getId() ? $user->getDBTouched() : '-' // handle preference change races + // Account for user name related variables like signatures + $user->getId(), + md5( $user->getName() ) ] ) ); return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash ); @@ -394,13 +378,13 @@ class ApiStashEdit extends ApiBase { * @param User $user * @return array (stash info array, TTL in seconds) or (null, 0) */ - protected static function buildStashValue( + private static function buildStashValue( Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user ) { // If an item is renewed, mind the cache TTL determined by config and parser functions. // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness. $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() ); - $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 ); + $ttl = min( $parserOutput->getCacheExpiry() - $since, self::MAX_CACHE_TTL ); if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) { // Only store what is actually needed diff --git a/includes/api/SearchApi.php b/includes/api/SearchApi.php new file mode 100644 index 0000000000..139793d12b --- /dev/null +++ b/includes/api/SearchApi.php @@ -0,0 +1,116 @@ +getSearchEngineFactory()->create( $backendType ); + } else { + $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); + } + + $profiles = $searchEngine->getProfiles( $profileType ); + if ( $profiles ) { + $types = []; + $helpMessages = []; + $defaultProfile = null; + foreach ( $profiles as $profile ) { + $types[] = $profile['name']; + if ( isset ( $profile['desc-message'] ) ) { + $helpMessages[$profile['name']] = $profile['desc-message']; + } + if ( !empty( $profile['default'] ) ) { + $defaultProfile = $profile['name']; + } + } + return [ + ApiBase::PARAM_TYPE => $types, + ApiBase::PARAM_HELP_MSG => $helpMsg, + ApiBase::PARAM_HELP_MSG_PER_VALUE => $helpMessages, + ApiBase::PARAM_DFLT => $defaultProfile, + ]; + } + return null; + } + + /** + * Build the search engine to use. + * If $params is provided then the following searchEngine options + * will be set: + * - limit: mandatory + * - offset: optional, if set limit will be incremented by + * one ( to support the continue parameter ) + * - namespace: mandatory + * - search engine profiles defined by SearchApi::getSearchProfileParams() + * @param string[]|null API request params (must be sanitized by + * ApiBase::extractRequestParams() before) + * @return SearchEngine the search engine + */ + public function buildSearchEngine( array $params = null ) { + if ( $params != null ) { + $type = isset( $params['backend'] ) ? $params['backend'] : null; + $searchEngine = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); + $limit = $params['limit']; + $searchEngine->setNamespaces( $params['namespace'] ); + $offset = null; + if ( isset( $params['offset'] ) ) { + // If the API supports offset then it probably + // wants to fetch limit+1 so it can check if + // more results are available to properly set + // the continue param + $offset = $params['offset']; + $limit += 1; + } + $searchEngine->setLimitOffset( $limit, $offset ); + foreach ( $this->getSearchProfileParams() as $type => $param ) { + if ( isset( $params[$param] ) ) { + $searchEngine->setFeatureData( $type, $params[$param] ); + } + } + } else { + $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); + } + return $searchEngine; + } + + /** + * @return string[] the list of supported search profile types. Key is + * the profile type and its associated value is the request param. + */ + abstract public function getSearchProfileParams(); +} diff --git a/includes/api/i18n/ba.json b/includes/api/i18n/ba.json index 512ab83aa6..7dcc4fd366 100644 --- a/includes/api/i18n/ba.json +++ b/includes/api/i18n/ba.json @@ -292,6 +292,7 @@ "apihelp-query+allfileusages-param-dir": "Һанау йүнәлеше.", "apihelp-query+allfileusages-example-unique": "Атамаларҙың уҙенсәлекле файлдары исемлеге.", "apihelp-query+allfileusages-example-unique-generator": "Төшөп ҡалғандарҙы айырып, барлыҡ исем-һылтанмаларҙы алырға.", + "apihelp-query+allfileusages-example-generator": "Һылтанмалы биттәр бар.", "apihelp-query+allimages-description": "Бер-бер артлы бөтә образдарҙы һанап сығырға.", "apihelp-query+allimages-param-sort": "Сортировкалау үҙенсәлектәре.", "apihelp-query+allimages-param-dir": "Һанау йүнәлеше.", @@ -329,16 +330,23 @@ "apihelp-query+allredirects-param-prefix": "Был мәғәнәнән башланған бар атамаларҙы категориялар буйынса эҙләргә.", "apihelp-query+allredirects-param-prop": "Ҡайһы мәғлүмәтте күрһәтергә:", "apihelp-query+allredirects-param-namespace": "Һанау өсөн исемдәр арауығы.", + "apihelp-query+allredirects-param-limit": "Нисә битте тергеҙергә?", "apihelp-query+allredirects-param-dir": "Һанау йүнәлеше.", "apihelp-query+allredirects-example-generator": "Һылтанмалы биттәр бар.", "apihelp-query+allrevisions-param-start": "Иҫәп күсереү башланған ваҡыт билдәһе", "apihelp-query+allrevisions-param-end": "Иҫәп күсереү башланған ваҡыт билдәһе", "apihelp-query+allrevisions-param-user": "Бары тик был ҡулланыусының үҙгәртеүҙәр исемлеге.", "apihelp-query+allrevisions-param-excludeuser": "Бары тик был ҡулланыусының үҙгәртеүҙәр исемлеге.", + "apihelp-query+alltransclusions-param-to": "Һанауҙы туҡтатыу һылтанмаһы атамаһы.", + "apihelp-query+alltransclusions-param-prop": "Ҡайһы мәғлүмәтте күрһәтергә:", + "apihelp-query+alltransclusions-param-namespace": "Һанау өсөн исемдәр арауығы.", + "apihelp-query+alltransclusions-param-limit": "Нисә битте тергеҙергә?", + "apihelp-query+alltransclusions-param-dir": "Һанау йүнәлеше.", "apihelp-query+alltransclusions-example-generator": "Һылтанмалы биттәр бар.", "apihelp-query+allusers-param-from": "Иҫәп күсереү башланған ваҡыт билдәһе", "apihelp-query+allusers-param-to": "Иҫәп күсереү башланған ваҡыт билдәһе", "apihelp-query+allusers-param-prefix": "Был мәғәнәнән башланған бар атамаларҙы категориялар буйынса эҙләргә.", + "apihelp-query+allusers-param-dir": "Сортлау йүнәлештәре.", "apihelp-query+allusers-param-prop": "Ҡайһы мәғлүмәтте күрһәтергә:", "apihelp-query+backlinks-param-title": "Мөхәриррләү өсөн биттең исеме.$1биттәрҙән бергә файҙаланыу мөмкин түгел.", "apihelp-query+backlinks-param-pageid": "Бит идентифакторын мөхәррирләү өсөн биттәр. $1title менән бергә ҡулланыла алмайҙар", diff --git a/includes/api/i18n/de.json b/includes/api/i18n/de.json index 5402ab47e3..8c6a71f9bd 100644 --- a/includes/api/i18n/de.json +++ b/includes/api/i18n/de.json @@ -61,6 +61,7 @@ "apihelp-compare-param-torev": "Zweite zu vergleichende Version.", "apihelp-compare-example-1": "Unterschied zwischen Version 1 und 2 abrufen", "apihelp-createaccount-description": "Erstellen eines neuen Benutzerkontos.", + "apihelp-createaccount-param-preservestate": "Falls [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] für hasprimarypreservedstate wahr ausgegeben hat, sollten Anfragen, die als primary-required markiert wurden, ausgelassen werden. Falls ein nicht-leerer Wert für preservedusername zurückgegeben wurde, muss dieser Benutzername für den Parameter username verwendet werden.", "apihelp-createaccount-param-name": "Benutzername.", "apihelp-createaccount-param-password": "Passwort (wird ignoriert, wenn $1mailpassword angegeben ist).", "apihelp-createaccount-param-domain": "Domain für die externe Authentifizierung (optional).", @@ -209,7 +210,7 @@ "apihelp-import-param-namespace": "In diesen Namensraum importieren. Kann nicht zusammen mit $1rootpage verwendet werden.", "apihelp-import-param-rootpage": "Als Unterseite dieser Seite importieren. Kann nicht zusammen mit $1namespace verwendet werden.", "apihelp-import-example-import": "Importiere [[meta:Help:ParserFunctions]] mit der kompletten Versionsgeschichte in den Namensraum 100.", - "apihelp-login-description": "Anmelden und Authentifizierungs-Cookies beziehen.\n\nFalls das Anmelden erfolgreich war, werden die benötigten Cookies im Header der HTTP-Antwort des Servers übermittelt. Bei fehlgeschlagenen Anmeldeversuchen können weitere Versuche gedrosselt werden, um automatische Passwortermittlungsattacken zu verhinden.", + "apihelp-login-description": "Anmelden und Authentifizierungs-Cookies beziehen.\n\nDiese Aktion sollte nur in Kombination mit [[Special:BotPasswords]] verwendet werden. Die Verwendung für die Anmeldung beim Hauptkonto ist veraltet und kann ohne Warnung fehlschlagen. Um sich sicher beim Hauptkonto anzumelden, verwende [[Special:ApiHelp/clientlogin|action=clientlogin]].", "apihelp-login-param-name": "Benutzername.", "apihelp-login-param-password": "Passwort.", "apihelp-login-param-domain": "Domain (optional).", @@ -738,6 +739,7 @@ "apihelp-query+pageswithprop-param-limit": "Die maximale Anzahl zurückzugebender Seiten.", "apihelp-query+prefixsearch-param-search": "Such-Zeichenfolge.", "apihelp-query+prefixsearch-param-offset": "Anzahl der zu überspringenden Ergebnisse.", + "apihelp-query+prefixsearch-param-profile": "Zu verwendendes Suchprofil.", "apihelp-query+protectedtitles-param-prop": "Zurückzugebende Eigenschaften:", "apihelp-query+querypage-param-limit": "Anzahl der zurückzugebenden Ergebnisse.", "apihelp-query+recentchanges-description": "Listet die letzten Änderungen auf.", @@ -767,6 +769,7 @@ "apihelp-query+search-param-what": "Welcher Suchtyp ausgeführt werden soll.", "apihelp-query+search-param-info": "Welche Metadaten zurückgegeben werden sollen.", "apihelp-query+search-param-prop": "Eigenschaften zur Rückgabe:", + "apihelp-query+search-param-qiprofile": "Zu verwendendes anfrageunabhängiges Profil (wirkt sich auf den Ranking-Algorithmus aus).", "apihelp-query+search-paramvalue-prop-wordcount": "Ergänzt den Wortzähler der Seite.", "apihelp-query+search-param-limit": "Wie viele Seiten insgesamt zurückgegeben werden sollen.", "apihelp-query+search-example-simple": "Nach meaning suchen.", @@ -785,6 +788,8 @@ "apihelp-query+usercontribs-paramvalue-prop-ids": "Fügt die Seiten- und Versionskennung hinzu.", "apihelp-query+usercontribs-paramvalue-prop-timestamp": "Ergänzt den Zeitstempel der Bearbeitung.", "apihelp-query+usercontribs-paramvalue-prop-comment": "Fügt den Kommentar der Bearbeitung hinzu.", + "apihelp-query+usercontribs-paramvalue-prop-patrolled": "Markiert kontrollierte Bearbeitungen.", + "apihelp-query+usercontribs-paramvalue-prop-tags": "Listet die Markierungen für die Bearbeitung auf.", "apihelp-query+userinfo-paramvalue-prop-blockinfo": "Markiert, ob der aktuelle Benutzer gesperrt ist, von wem und aus welchem Grund.", "apihelp-query+userinfo-paramvalue-prop-editcount": "Ergänzt den Bearbeitungszähler des aktuellen Benutzers.", "apihelp-query+userinfo-paramvalue-prop-realname": "Fügt den bürgerlichen Namen des Benutzers hinzu.", @@ -797,8 +802,14 @@ "apihelp-query+users-paramvalue-prop-implicitgroups": "Listet alle Gruppen auf, bei denen der Benutzer automatisch Mitglied ist.", "apihelp-query+users-paramvalue-prop-rights": "Listet alle Rechte auf, die jeder Benutzer hat.", "apihelp-query+users-paramvalue-prop-editcount": "Ergänzt den Bearbeitungszähler des Benutzers.", + "apihelp-query+users-param-users": "Eine Liste der Benutzer, für die Informationen abgerufen werden sollen.", "apihelp-query+users-example-simple": "Gibt Informationen für den Benutzer Example zurück.", + "apihelp-query+watchlist-param-user": "Listet nur Änderungen von diesem Benutzer auf.", + "apihelp-query+watchlist-param-excludeuser": "Listet keine Änderungen von diesem Benutzer auf.", "apihelp-query+watchlist-param-prop": "Zusätzlich zurückzugebende Eigenschaften:", + "apihelp-query+watchlist-paramvalue-prop-ids": "Ergänzt die Versions- und Seitenkennungen.", + "apihelp-query+watchlist-paramvalue-prop-title": "Ergänzt den Titel der Seite.", + "apihelp-query+watchlist-paramvalue-prop-flags": "Ergänzt die Markierungen für die Bearbeitungen.", "apihelp-query+watchlist-paramvalue-prop-user": "Ergänzt den Benutzer, der die Bearbeitung ausgeführt hat.", "apihelp-query+watchlist-paramvalue-prop-userid": "Ergänzt die Kennung des Benutzers, der die Bearbeitung ausgeführt hat.", "apihelp-query+watchlist-paramvalue-prop-comment": "Ergänzt den Kommentar der Bearbeitung.", diff --git a/includes/api/i18n/diq.json b/includes/api/i18n/diq.json index c737355833..1ab1bc5254 100644 --- a/includes/api/i18n/diq.json +++ b/includes/api/i18n/diq.json @@ -2,9 +2,11 @@ "@metadata": { "authors": [ "Gorizon", - "Mirzali" + "Mirzali", + "Kumkumuk" ] }, + "apihelp-block-description": "Enê karberi bloqe ke", "apihelp-createaccount-param-name": "Nameyê karberi.", "apihelp-delete-description": "Pele bestere.", "apihelp-disabled-description": "Eno modul aktiv niyo.", @@ -25,6 +27,9 @@ "apihelp-feedrecentchanges-param-hidebots": "Vurnayışanê botan bınımne.", "apihelp-feedrecentchanges-param-hideanons": "Vurnayışanê karberanê anoniman bınımne.", "apihelp-feedrecentchanges-param-hideliu": "Vurnayışanê karberanê qeydınan bınımne.", + "apihelp-feedrecentchanges-param-tagfilter": "Filtrey etiketi", + "apihelp-feedrecentchanges-example-simple": "Vurnayışê peyênan bıvin", + "apihelp-feedrecentchanges-example-30days": "Peyni vurnayışanê 30 raco bımosne", "apihelp-login-param-name": "Nameyê karberi.", "apihelp-login-param-password": "Parola.", "apihelp-login-param-domain": "Domain (optional).", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index a802cc7285..82a83496c6 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -48,7 +48,7 @@ "apihelp-clientlogin-description": "Log in to the wiki using the interactive flow.", "apihelp-clientlogin-example-login": "Start the process of logging in to the wiki as user Example with password ExamplePassword.", - "apihelp-clientlogin-example-login2": "Continue logging in after a UI response for two-factor auth, supplying an OATHToken of 987654.", + "apihelp-clientlogin-example-login2": "Continue logging in after a UI response for two-factor auth, supplying an OATHToken of 987654.", "apihelp-compare-description": "Get the difference between 2 pages.\n\nA revision number, a page title, or a page ID for both \"from\" and \"to\" must be passed.", "apihelp-compare-param-fromtitle": "First title to compare.", @@ -60,6 +60,7 @@ "apihelp-compare-example-1": "Create a diff between revision 1 and 2.", "apihelp-createaccount-description": "Create a new user account.", + "apihelp-createaccount-param-preservestate": "If [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] returned true for hasprimarypreservedstate, requests marked as primary-required should be omitted. If it returned a non-empty value for preservedusername, that username must be used for the username parameter.", "apihelp-createaccount-example-create": "Start the process of creating user Example with password ExamplePassword.", "apihelp-createaccount-param-name": "Username.", "apihelp-createaccount-param-password": "Password (ignored if $1mailpassword is set).", @@ -154,6 +155,7 @@ "apihelp-feedcontributions-param-deletedonly": "Show only deleted contributions.", "apihelp-feedcontributions-param-toponly": "Only show edits that are latest revisions.", "apihelp-feedcontributions-param-newonly": "Only show edits that are page creations.", + "apihelp-feedcontributions-param-hideminor": "Hide minor edits.", "apihelp-feedcontributions-param-showsizediff": "Show the size difference between revisions.", "apihelp-feedcontributions-example-simple": "Return contributions for user Example.", @@ -962,6 +964,7 @@ "apihelp-query+prefixsearch-param-limit": "Maximum number of results to return.", "apihelp-query+prefixsearch-param-offset": "Number of results to skip.", "apihelp-query+prefixsearch-example-simple": "Search for page titles beginning with meaning.", + "apihelp-query+prefixsearch-param-profile": "Search profile to use.", "apihelp-query+protectedtitles-description": "List all titles protected from creation.", "apihelp-query+protectedtitles-param-namespace": "Only list titles in these namespaces.", @@ -1082,6 +1085,7 @@ "apihelp-query+search-param-what": "Which type of search to perform.", "apihelp-query+search-param-info": "Which metadata to return.", "apihelp-query+search-param-prop": "Which properties to return:", + "apihelp-query+search-param-qiprofile": "Query independent profile to use (affects ranking algorithm).", "apihelp-query+search-paramvalue-prop-size": "Adds the size of the page in bytes.", "apihelp-query+search-paramvalue-prop-wordcount": "Adds the word count of the page.", "apihelp-query+search-paramvalue-prop-timestamp": "Adds the timestamp of when the page was last edited.", @@ -1505,7 +1509,7 @@ "api-help-right-apihighlimits": "Use higher limits in API queries (slow queries: $1; fast queries: $2). The limits for slow queries also apply to multivalue parameters.", "api-help-open-in-apisandbox": "[open in sandbox]", - "api-help-authmanager-general-usage": "The general procedure to use this module is:\n# Fetch the fields available from [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] with amirequestsfor=$4, and a $5 token from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying $1returnurl and any relevant fields.\n# Check the status in the response.\n#* If you received PASS or FAIL, you're done. The operation either succeeded or it didn't.\n#* If you received UI, present the new fields to the user and obtain their submission. Then post to this module with $1continue and the relevant fields set, and repeat step 4.\n#* If you received REDIRECT, direct the user to the redirecttarget and wait for the return to $1returnurl. Then post to this module with $1continue and any fields passed to the return URL, and repeat step 4.\n#* If you received RESTART, that means the authentication worked but we don't have an linked user account. You might treat this as UI or as FAIL.", + "api-help-authmanager-general-usage": "The general procedure to use this module is:\n# Fetch the fields available from [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] with amirequestsfor=$4, and a $5 token from [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying $1returnurl and any relevant fields.\n# Check the status in the response.\n#* If you received PASS or FAIL, you're done. The operation either succeeded or it didn't.\n#* If you received UI, present the new fields to the user and obtain their submission. Then post to this module with $1continue and the relevant fields set, and repeat step 4.\n#* If you received REDIRECT, direct the user to the redirecttarget and wait for the return to $1returnurl. Then post to this module with $1continue and any fields passed to the return URL, and repeat step 4.\n#* If you received RESTART, that means the authentication worked but we don't have a linked user account. You might treat this as UI or as FAIL.", "api-help-authmanagerhelper-requests": "Only use these authentication requests, by the id returned from [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] with amirequestsfor=$1 or from a previous response from this module.", "api-help-authmanagerhelper-request": "Use this authentication request, by the id returned from [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] with amirequestsfor=$1.", "api-help-authmanagerhelper-messageformat": "Format to use for returning messages.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index 284c23c8ab..10f6c7f537 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -18,7 +18,10 @@ "AlvaroMolina", "Ciencia Al Poder", "Lemondoge", - "Mgpena" + "Mgpena", + "Rubentl134", + "2axterix2", + "Dgstranz" ] }, "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|Preguntas frecuentes]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de correos]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API de anuncios]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Errores y peticiones]\n
\nEstado: Todas las características que se muestran en esta página debería funcionar, pero la API aún está en desarrollo activo y puede cambiar en cualquier momento. Suscríbete a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la lista de correo de mediawiki-api-announce] para estar al día de las actualizaciones.\n\nSolicitudes erróneas: Cuando se envían solicitudes erróneas a la API, se envía un encabezado HTTP con la clave \"MediaWiki-API-Error\" y ambos valores, del encabezado y el código de error, se establecerán en el mismo valor. Para más información, véase [[mw:API:Errors_and_warnings|API: Errores y advertencias]].\n\nPruebas: para facilitar las pruebas de solicitudes a la API, consulta [[Special:ApiSandbox]].", @@ -128,6 +131,7 @@ "apihelp-expandtemplates-paramvalue-prop-ttl": "El tiempo máximo tras el cual deberían invalidarse los resultados en caché.", "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "Da las variables de configuración JavaScript específicas para la página.", "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "Da las variables de configuración JavaScript específicas para la página como una cadena JSON.", + "apihelp-expandtemplates-param-includecomments": "Incluir o no los comentarios HTML en la salida.", "apihelp-expandtemplates-param-generatexml": "Generar un árbol de análisis XML (remplazado por $1prop=parsetree).", "apihelp-expandtemplates-example-simple": "Expandir el wikitexto {{Project:Sandbox}}.", "apihelp-feedcontributions-description": "Devuelve el canal de contribuciones de un usuario.", @@ -306,6 +310,7 @@ "apihelp-protect-example-protect": "Proteger una página", "apihelp-protect-example-unprotect": "Desproteger una página estableciendo la restricción a all.", "apihelp-protect-example-unprotect2": "Desproteger una página anulando las restricciones.", + "apihelp-purge-description": "Purgar la caché de los títulos proporcionados.\n\nSe requiere una solicitud POST si el usuario no ha iniciado sesión.", "apihelp-purge-param-forcelinkupdate": "Actualizar las tablas de enlaces.", "apihelp-purge-param-forcerecursivelinkupdate": "Actualizar la tabla de enlaces y todas las tablas de enlaces de cualquier página que use esta página como una plantilla.", "apihelp-purge-example-simple": "Purgar la Main Page y la página API.", @@ -413,6 +418,7 @@ "apihelp-query+mystashedfiles-param-limit": "Cuántos archivos obtener.", "apihelp-query+alltransclusions-param-prefix": "Buscar todos los títulos transcluidos que comiencen con este valor.", "apihelp-query+alltransclusions-param-prop": "Qué piezas de información incluir:", + "apihelp-query+alltransclusions-paramvalue-prop-title": "Añade el título de la transclusión.", "apihelp-query+alltransclusions-example-unique": "Listar títulos transcluidos de forma única.", "apihelp-query+alltransclusions-example-unique-generator": "Obtiene todos los títulos transcluidos, marcando los que faltan.", "apihelp-query+allusers-description": "Enumerar todos los usuarios registrados.", @@ -425,7 +431,9 @@ "apihelp-query+allusers-param-limit": "Cuántos nombres de usuario se devolverán.", "apihelp-query+allusers-param-activeusers": "Solo listar usuarios activos en {{PLURAL:$1|el último día|los $1 últimos días}}.", "apihelp-query+allusers-example-Y": "Listar usuarios que empiecen por Y.", + "apihelp-query+filerepoinfo-example-login": "Captura de las solicitudes que puede ser utilizadas al comienzo de inicio de sesión.", "apihelp-query+backlinks-param-pageid": "Identificador de página que buscar. No puede usarse junto con $1title", + "apihelp-query+backlinks-param-filterredir": "Cómo filtrar redirecciones. Si se establece a nonredirects cuando está activo $1redirect, esto sólo se aplica al segundo nivel.", "apihelp-query+backlinks-param-limit": "Cuántas páginas en total se devolverán. Si está activo $1redirect, el límite aplica a cada nivel por separado (lo que significa que se pueden devolver hasta 2 * $1limit resultados).", "apihelp-query+backlinks-example-simple": "Mostrar enlaces a Main page.", "apihelp-query+backlinks-example-generator": "Obtener información acerca de las páginas enlazadas a Main page.", @@ -435,6 +443,7 @@ "apihelp-query+blocks-param-prop": "Qué propiedades se obtendrán:", "apihelp-query+blocks-paramvalue-prop-userid": "Añade el identificador del usuario bloqueado.", "apihelp-query+blocks-paramvalue-prop-timestamp": "Añade la fecha y hora de cuando se aplicó el bloque.", + "apihelp-query+blocks-paramvalue-prop-reason": "Añade la razón dada para el bloqueo.", "apihelp-query+blocks-example-simple": "Listar bloques.", "apihelp-query+categories-param-prop": "Qué propiedades adicionales obtener para cada categoría:", "apihelp-query+categories-param-show": "Qué tipo de categorías mostrar.", @@ -740,6 +749,7 @@ "apihelp-unblock-description": "Desbloquear un usuario.", "apihelp-unblock-param-user": "Nombre de usuario, dirección IP o rango de direcciones IP para desbloquear. No se puede utilizar junto con $1id.", "apihelp-unblock-param-reason": "Motivo del desbloqueo.", + "apihelp-unblock-example-id": "Desbloquear el bloqueo de ID #105", "apihelp-unblock-example-user": "Desbloquear al usuario Bob con el motivo Sorry Bob", "apihelp-undelete-param-reason": "Motivo de la restauración.", "apihelp-undelete-example-revisions": "Restaurar dos revisiones de la página Main Page.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 4c0d1775bb..fa8aa03577 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -23,7 +23,9 @@ "Umherirrender", "Elfix", "Lbayle", - "Verdy p" + "Verdy p", + "Yasten", + "Trial" ] }, "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentation]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Liste de diffusion]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annonces de l’API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bogues et demandes]\n
\nÉtat : Toutes les fonctionnalités affichées sur cette page devraient fonctionner, mais l’API est encore en cours de développement et peut changer à tout moment. Inscrivez-vous à [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la liste de diffusion mediawiki-api-announce] pour être informé des mises à jour.\n\nRequêtes erronées : Si des requêtes erronées sont envoyées à l’API, un en-tête HTTP sera renvoyé avec la clé « MediaWiki-API-Error ». La valeur de cet en-tête et le code d’erreur renvoyé prendront la même valeur. Pour plus d’information, voyez [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\nTest : Pour faciliter le test des requêtes de l’API, voyez [[Special:ApiSandbox]].", @@ -52,6 +54,8 @@ "apihelp-block-param-watchuser": "Surveiller les pages utilisateur et de discussion de l’utilisateur ou de l’adresse IP.", "apihelp-block-example-ip-simple": "Bloquer l’adresse IP 192.0.2.5 pour trois jours avec le motif Premier avertissement.", "apihelp-block-example-user-complex": "Bloquer indéfiniment l’utilisateur Vandal avec le motif Vandalism, et empêcher la création de nouveau compte et l'envoi de courriel.", + "apihelp-changeauthenticationdata-description": "Modifier les données d’authentification pour l’utilisateur actuel.", + "apihelp-changeauthenticationdata-example-password": "Tentative de modification du mot de passe de l’utilisateur actuel en ExempleMotDePasse.", "apihelp-checktoken-description": "Vérifier la validité d'un jeton de [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-checktoken-param-type": "Type de jeton testé", "apihelp-checktoken-param-token": "Jeton à tester.", @@ -59,6 +63,9 @@ "apihelp-checktoken-example-simple": "Tester la validité d'un jeton de csrf.", "apihelp-clearhasmsg-description": "Efface le drapeau hasmsg pour l’utilisateur courant.", "apihelp-clearhasmsg-example-1": "Effacer le drapeau hasmsg pour l’utilisateur courant", + "apihelp-clientlogin-description": "Se connecter au wiki en utilisant le flux interactif.", + "apihelp-clientlogin-example-login": "Commencer le processus de connexion au wiki en tant qu’utilisateur Exemple avec le mot de passe ExempleMotDePasse.", + "apihelp-clientlogin-example-login2": "Continuer la connexion après une réponse de l’IHM pour l’authentification à deux facteurs, en fournissant un OATHToken valant 987654.", "apihelp-compare-description": "Obtenir la différence entre 2 pages.\n\nVous devez passer un numéro de révision, un titre de page, ou un ID de page, à la fois pour « from » et « to ».", "apihelp-compare-param-fromtitle": "Premier titre à comparer.", "apihelp-compare-param-fromid": "ID de la première page à comparer.", @@ -68,6 +75,8 @@ "apihelp-compare-param-torev": "Seconde révision à comparer.", "apihelp-compare-example-1": "Créer une différence entre les révisions 1 et 2", "apihelp-createaccount-description": "Créer un nouveau compte utilisateur.", + "apihelp-createaccount-param-preservestate": "Si [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] renvoyé true pour hasprimarypreservedstate, les demandes marquées comme primary-required doivent être omises. Si elle a retourné une valeur non vide pour preservedusername, ce nom d'utilisateur doit être utilisé pour le paramètre username.", + "apihelp-createaccount-example-create": "Commencer le processus de création d’un utilisateur Exemple avec le mot de passe ExempleMotDePasse.", "apihelp-createaccount-param-name": "Nom d’utilisateur.", "apihelp-createaccount-param-password": "Mot de passe (ignoré si $1mailpassword est défini).", "apihelp-createaccount-param-domain": "Domaine pour l’authentification externe (facultatif).", @@ -216,7 +225,11 @@ "apihelp-import-param-namespace": "Importer vers cet espace de noms. Impossible à utiliser avec $1rootpage.", "apihelp-import-param-rootpage": "Importer comme une sous-page de cette page. Impossible à utiliser avec $1namespace.", "apihelp-import-example-import": "Importer [[meta:Help:ParserFunctions]] vers l’espace de noms 100 avec tout l’historique.", - "apihelp-login-description": "Se connecter et obtenir les cookies d’authentification.\n\nDans le cas d’une connexion réussie, les cookies nécessaires seront inclus dans les entêtes de la réponse HTTP. Dans le cas d’une connexion en échec, les essais ultérieurs pourront être réduits afin de limiter les attaques automatisées de découverte du mot de passe.", + "apihelp-linkaccount-description": "Lier un compte d’un fournisseur tiers à l’utilisateur actuel.", + "apihelp-linkaccount-example-link": "Commencer le processus de liaison d’un compte depuis Exemple.", + "apihelp-login-description": "Se connecter et obtenir les cookies d’authentification.\n\nCette action ne devrait être utilisée qu’en lien avec [[Special:BotPasswords]] ; l’utiliser pour la connexion du compte principal est obsolète et peut échouer sans avertissement. Pour se connecter sans problème au compte principal, utiliser [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nobotpasswords": "Se connecter et obtenir les cookies d’authentification.\n\nCette action est obsolète et peut échouer sans prévenir. Pour se connecter sans problème, utiliser [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nonauthmanager": "Se connecter et obtenir les cookies d’authentification.\n\nDans le cas d’une connexion réussie, les cookies nécessaires seront inclus dans les entêtes HTTP de la réponse. Dans le cas d’une connexion en échec, des tentatives ultérieures pourront être limitées pour éviter les attaques automatiques pour deviner les mots de passe.", "apihelp-login-param-name": "Nom d’utilisateur.", "apihelp-login-param-password": "Mot de passe.", "apihelp-login-param-domain": "Domaine (facultatif).", @@ -548,6 +561,12 @@ "apihelp-query+allusers-param-activeusers": "Lister uniquement les utilisateurs actifs durant {{PLURAL:$1|le dernier jour|les $1 derniers jours}}.", "apihelp-query+allusers-param-attachedwiki": "Avec $1prop=centralids, indiquer aussi si l’utilisateur est attaché avec le wiki identifié par cet ID.", "apihelp-query+allusers-example-Y": "Lister les utilisateurs en commençant à Y.", + "apihelp-query+authmanagerinfo-description": "Récupérer les informations concernant l’état d’authentification actuel.", + "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Tester si l’état d’authentification actuel de l’utilisateur est suffisant pour l’opération spécifiée comme sensible du point de vue sécurité.", + "apihelp-query+authmanagerinfo-param-requestsfor": "Récupérer les informations sur les requêtes d’authentification nécessaires pour l’action d’authentification spécifiée.", + "apihelp-query+filerepoinfo-example-login": "Récupérer les requêtes qui peuvent être utilisées en commençant une connexion.", + "apihelp-query+filerepoinfo-example-login-merged": "Récupérer les requêtes qui peuvent être utilisées au début de la connexion, avec les champs de formulaire intégrés.", + "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Tester si l’authentification est suffisante pour l’action foo.", "apihelp-query+backlinks-description": "Trouver toutes les pages qui ont un lien vers la page donnée.", "apihelp-query+backlinks-param-title": "Titre à rechercher. Impossible à utiliser avec $1pageid.", "apihelp-query+backlinks-param-pageid": "ID de la page à chercher. Impossible à utiliser avec $1title.", @@ -889,6 +908,7 @@ "apihelp-query+prefixsearch-param-limit": "Nombre maximal de résultats à renvoyer.", "apihelp-query+prefixsearch-param-offset": "Nombre de résultats à sauter.", "apihelp-query+prefixsearch-example-simple": "Rechercher les titres de page commençant par meaning.", + "apihelp-query+prefixsearch-param-profile": "Rechercher le profil à utiliser.", "apihelp-query+protectedtitles-description": "Lister tous les titres protégés en création.", "apihelp-query+protectedtitles-param-namespace": "Lister uniquement les titres dans ces espaces de nom.", "apihelp-query+protectedtitles-param-level": "Lister uniquement les titres avec ces niveaux de protection.", @@ -1001,6 +1021,7 @@ "apihelp-query+search-param-what": "Quel type de recherche effectuer.", "apihelp-query+search-param-info": "Quelles métadonnées renvoyer.", "apihelp-query+search-param-prop": "Quelles propriétés renvoyer :", + "apihelp-query+search-param-qiprofile": "Profil indépendant des requêtes à utiliser (affecte algorithme de classement).", "apihelp-query+search-paramvalue-prop-size": "Ajoute la taille de la page en octets.", "apihelp-query+search-paramvalue-prop-wordcount": "Ajoute le nombre de mots de la page.", "apihelp-query+search-paramvalue-prop-timestamp": "Ajoute l’horodatage de la dernière modification de la page.", @@ -1045,6 +1066,7 @@ "apihelp-query+siteinfo-paramvalue-prop-variables": "Renvoie une liste des IDs de variable.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "Renvoie une liste des protocoles qui sont autorisés dans les liens externes.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Renvoie les valeurs par défaut pour les préférences utilisateur.", + "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Renvoie la configuration du dialogue de téléversement.", "apihelp-query+siteinfo-param-filteriw": "Renvoyer uniquement les entrées locales ou uniquement les non locales de la correspondance interwiki.", "apihelp-query+siteinfo-param-showalldb": "Lister tous les serveurs de base de données, pas seulement celui avec la plus grande latence.", "apihelp-query+siteinfo-param-numberingroup": "Liste le nombre d’utilisateurs dans les groupes.", @@ -1145,6 +1167,7 @@ "apihelp-query+users-paramvalue-prop-emailable": "Marque si l’utilisateur peut et veut recevoir des courriels via [[Special:Emailuser]].", "apihelp-query+users-paramvalue-prop-gender": "Marque le sexe de l’utilisateur. Renvoie « male », « female », ou « unknown ».", "apihelp-query+users-paramvalue-prop-centralids": "Ajoute les IDs centraux et l’état d’attachement de l’utilisateur.", + "apihelp-query+users-paramvalue-prop-cancreate": "Indique si un compte peut être créé pour les noms d’utilisateurs valides mais non enregistrés.", "apihelp-query+users-param-attachedwiki": "Avec $1prop=centralids, indiquer si l’utilisateur est attaché au wiki identifié par cet ID.", "apihelp-query+users-param-users": "Une liste des utilisateurs sur lesquels obtenir de l’information.", "apihelp-query+users-param-token": "Utiliser plutôt [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", @@ -1197,6 +1220,15 @@ "apihelp-query+watchlistraw-param-totitle": "Terminer l'énumération avec ce Titre (inclure le préfixe d'espace de noms) :", "apihelp-query+watchlistraw-example-simple": "Lister les pages dans la liste de suivi de l’utilisateur actuel", "apihelp-query+watchlistraw-example-generator": "Chercher l’information sur les pages de la liste de suivi de l’utilisateur actuel", + "apihelp-removeauthenticationdata-description": "Supprimer les données d’authentification pour l’utilisateur actuel.", + "apihelp-removeauthenticationdata-example-simple": "Tentative de suppression des données de l’utilisateur pour FooAuthenticationRequest.", + "apihelp-resetpassword-description": "Envoyer un courriel de réinitialisation du mot de passe à un utilisateur.", + "apihelp-resetpassword-description-noroutes": "Aucun chemin pour réinitialiser le mot de passe n’est disponible.\n\nActiver les chemins dans [[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]] pour utiliser ce module.", + "apihelp-resetpassword-param-user": "Utilisateur ayant été réinitialisé.", + "apihelp-resetpassword-param-email": "Adresse courriel de l’utilisateur ayant été réinitialisé.", + "apihelp-resetpassword-param-capture": "Renvoyer les mots de passe temporaires déjà envoyés. Nécessite le droit utilisateur passwordreset.", + "apihelp-resetpassword-example-user": "Envoyer un courriel de réinitialisation du mot de passe à l’utilisateur Exemple.", + "apihelp-resetpassword-example-email": "Envoyer un courriel pour la réinitialisation de mot de passe à tous les utilisateurs avec une adresse email user@example.com.", "apihelp-revisiondelete-description": "Supprimer et annuler la suppression des révisions.", "apihelp-revisiondelete-param-type": "Type de suppression de révision en cours de traitement.", "apihelp-revisiondelete-param-target": "Titre de page pour la suppression de révision, s’il est nécessaire pour le type.", @@ -1265,6 +1297,8 @@ "apihelp-undelete-param-watchlist": "Ajouter ou supprimer la page de la liste de suivi de l’utilisateur actuel sans condition, utiliser les préférences ou ne pas modifier le suivi.", "apihelp-undelete-example-page": "Annuler la suppression de la page Main Page.", "apihelp-undelete-example-revisions": "Annuler la suppression de deux révisions de la page Main Page.", + "apihelp-unlinkaccount-description": "Supprimer un compte tiers lié de l’utilisateur actuel.", + "apihelp-unlinkaccount-example-simple": "Essayer de supprimer le lien de l’utilisateur actuel pour le fournisseur associé avec FooAuthenticationRequest.", "apihelp-upload-description": "Téléverser un fichier, ou obtenir l’état des téléversements en cours.\n\nPlusieurs méthodes sont disponibles :\n* Téléverser directement le contenu du fichier, en utilisant le paramètre $1file.\n* Téléverser le fichier par morceaux, en utilisant les paramètres $1filesize, $1chunk, and $1offset.\n* Pour que le serveur MédiaWiki cherche un fichier depuis une URL, utilisez le paramètre $1url.\n* Terminer un téléversement précédent qui a échoué à cause d’avertissements, en utilisant le paramètre $1filekey.\nNoter que le POST HTTP doit être fait comme un téléversement de fichier (par ex. en utilisant multipart/form-data) en envoyant le multipart/form-data.", "apihelp-upload-param-filename": "Nom de fichier cible.", "apihelp-upload-param-comment": "Télécharger le commentaire. Utilisé aussi comme texte de la page initiale pour les nouveaux fichiers si $1text n’est pas spécifié.", @@ -1374,6 +1408,15 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|Accordé à}} : $2", "api-help-right-apihighlimits": "Utiliser des valeurs plus hautes dans les requêtes de l’API (requêtes lentes : $1 ; requêtes rapides : $2). Les limites pour les requêtes lentes s’appliquent aussi aux paramètres multivalués.", "api-help-open-in-apisandbox": "[ouvrir dans le bac à sable]", + "api-help-authmanager-general-usage": "La procédure générale pour utiliser ce module est la suivante :\n# Récupérer les champs disponibles avec [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] avec amirequestsfor=$4, et un jeton $5 avec [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Présenter les champs à l’utilisateur, et les lui faire soumettre.\n# Faire un envoi à ce module, en fournissant $1returnurl et les champs appropriés.\n# Vérifier le status dans la réponse.\n#* Si vous avez reçu PASS ou FAIL, c’est terminé. L’opération a soit réussi, soit échoué.\n#* Si vous avez reçu UI, affichez les nouveaux champs à l’utilisateur et faites-les-lui soumettre. Puis envoyez-les à ce module avec $1continue et l’ensemble des champs appropriés, et recommencez l’étape 4.\n#* Si vous avez reçu REDIRECT, envoyez l’utilisateur vers la cible redirecttarget et attendez le retour vers $1returnurl. Puis envoyez à ce module avec $1continue et tous les champs passés à l’URL de retour, puis répétez l’étape 4.\n#* Si vous avez reçu RESTART, cela veut dire que l’authentification a fonctionné, mais nous n’avons pas de compte utilisateur lié. Vous pouvez traiter cela comme un UI ou un FAIL.", + "api-help-authmanagerhelper-requests": "Utiliser uniquement ces requêtes d’authentification, avec l’id renvoyé par [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] avec amirequestsfor=$1 ou depuis une réponse précédente de ce module.", + "api-help-authmanagerhelper-request": "Utiliser cette requête d’authentification, avec l’id renvoyé par [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] avec amirequestsfor=$1.", + "api-help-authmanagerhelper-messageformat": "Format à utiliser pour retourner les messages.", + "api-help-authmanagerhelper-mergerequestfields": "Fusionner dans un tableau le champ information de toutes les demandes d'authentification.", + "api-help-authmanagerhelper-preservestate": "Conserver l'état d'une précédente tentative de connexion qui a échoué, si possible.", + "api-help-authmanagerhelper-returnurl": "Renvoyer l’URL pour les flux d’authentification tiers, qui doit être absolue. Cela ou $1continue est obligatoire.\n\nDès réception d’une réponse REDIRECT, vous ouvrirez typiquement un navigateur ou un affichage web vers l’URL redirecttarget spécifiée pour un flux d’authentification tiers. Une fois ceci terminé, le tiers renverra le navigateur ou l’affichage web vers cette URL. Vous devez extraire toute requête ou paramètre POST de l’URL et les passer comme une requête $1continue à ce module de l’API.", + "api-help-authmanagerhelper-continue": "Cette requête est une continuation après une précédente réponse UI ou REDIRECT. Cela ou $1returnurl est obligatoire.", + "api-help-authmanagerhelper-additional-params": "Ce module accepte des paramètres supplémentaires selon les requêtes d’authentification disponibles. Utiliser [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] avec amirequestsfor=$1 (ou une réponse précédente de ce module, le cas échéant) pour déterminer les requêtes disponibles et les champs qu’elles utilisent.", "api-credits-header": "Remerciements", "api-credits": "Développeurs de l’API :\n* Roan Kattouw (développeur en chef Sept. 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (créateur, développeur en chef Sept. 2006–Sept. 2007)\n* Brad Jorsch (développeur en chef depuis 2013)\n\nVeuillez envoyer vos commentaires, suggestions et questions à mediawiki-api@lists.wikimedia.org\nou remplir un rapport de bogue sur https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index be92dc2ad9..a7ecabd63a 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -39,6 +39,8 @@ "apihelp-block-param-watchuser": "Vixiar a páxina de usuario ou direccións IP e a de conversa deste usuario", "apihelp-block-example-ip-simple": "Bloquear dirección IP 192.0.2.5 durante tres días coa razón Primeiro aviso.", "apihelp-block-example-user-complex": "Bloquear indefinidamente ó usuario Vandal coa razón Vandalism, e impedir a creación de novas contas e envío de correos electrónicos.", + "apihelp-changeauthenticationdata-description": "Cambiar os datos de autenticación do usuario actual.", + "apihelp-changeauthenticationdata-example-password": "Intento de cambiar o contrasinal do usuario actua a ExemploContrasinal.", "apihelp-checktoken-description": "Verificar a validez dun identificador de [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-checktoken-param-type": "Tipo de identificador a probar.", "apihelp-checktoken-param-token": "Símbolo a testar", @@ -46,6 +48,9 @@ "apihelp-checktoken-example-simple": "Verificar a validez de un identificador csrf.", "apihelp-clearhasmsg-description": "Limpar a bandeira hasmsg para o usuario actual", "apihelp-clearhasmsg-example-1": "Limpar a bandeira hasmsg para o usuario actual", + "apihelp-clientlogin-description": "Conectarse á wiki usando o fluxo interactivo.", + "apihelp-clientlogin-example-login": "Comezar o proceso de conexión á wiki como o usuario Exemplo con contrasinal ExemploContrasinal.", + "apihelp-clientlogin-example-login2": "Continuar a conexión despois dunha resposta de UI para unha autenticación de dous factores, proporcionando un OATHToken con valor 987654.", "apihelp-compare-description": "Obter as diferencias entre dúas páxinas.\n\nDebe indicar un número de revisión, un título de páxina, ou un ID de páxina tanto para \"from\" como para \"to\".", "apihelp-compare-param-fromtitle": "Primeiro título para comparar.", "apihelp-compare-param-fromid": "Identificador da primeira páxina a comparar.", @@ -55,6 +60,7 @@ "apihelp-compare-param-torev": "Segunda revisión a comparar.", "apihelp-compare-example-1": "Mostrar diferencias entre a revisión 1 e a 2", "apihelp-createaccount-description": "Crear unha nova conta de usuario.", + "apihelp-createaccount-example-create": "Comezar o proceso de crear un usuario Exemplo con contrasinal ExemploContrasinal.", "apihelp-createaccount-param-name": "Nome de usuario.", "apihelp-createaccount-param-password": "Contrasinal (ignorado se $1mailpassword está activo)", "apihelp-createaccount-param-domain": "Dominio para autenticación externa (opcional)", @@ -203,7 +209,11 @@ "apihelp-import-param-namespace": "Importar a este espazo de nomes. Non se pode usar de forma conxunta con $1rootpage.", "apihelp-import-param-rootpage": "Importar como subpáxina desta páxina. Non se pode usar de forma conxunta con $1namespace.", "apihelp-import-example-import": "Importar [[meta:Help:ParserFunctions]] ó espazo de nomes 100 con todo o historial.", + "apihelp-linkaccount-description": "Vincular unha conta dun provedor externo ó usuario actual.", + "apihelp-linkaccount-example-link": "Comezar o proceso de vincular a unha conta de Exemplo.", "apihelp-login-description": "No caso dunha conexión correcta, as cookies necesarias incluiranse nas cabeceiras HTTP de resposta. No caso dunha conexión fallida, os intentos posteriores poden ser reducidos para limitar ataques automaticos de roubo de contrasinais.", + "apihelp-login-description-nobotpasswords": "Conectarse e obter as cookies de autenticación. \n\nEsta acción está obsoleta e pode fallar sen avisar. Para conectarse sen problema use [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nonauthmanager": "Conectarse e obter as cookies de autenticación. \n\nNo caso dunha conexión correcta, as cookies necesarias incluiranse nas cabeceiras HTTP de resposta. No caso dunha conexión fallida, os intentos posteriores poden ser reducidos para limitar ataques automaticos de roubo de contrasinais.", "apihelp-login-param-name": "Nome de usuario.", "apihelp-login-param-password": "Contrasinal", "apihelp-login-param-domain": "Dominio (opcional).", @@ -535,6 +545,12 @@ "apihelp-query+allusers-param-activeusers": "Só listar usuarios activos {{PLURAL:$1|no último día|nos $1 últimos días}}.", "apihelp-query+allusers-param-attachedwiki": "Con $1prop=centralids, \ntamén indica se o usuario está acoplado á wiki identificada por este identificador.", "apihelp-query+allusers-example-Y": "Listar usuarios que comecen por Y.", + "apihelp-query+authmanagerinfo-description": "Recuperar información sobre o estado de autenticación actual.", + "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Comprobar se o estado de autenticación actual do usuario é abondo para a operación especificada como sensible dende o punto de vista da seguridade.", + "apihelp-query+authmanagerinfo-param-requestsfor": "Recuperar a información sobre as peticións de autenticación necesarias para a acción de autenticación especificada.", + "apihelp-query+filerepoinfo-example-login": "Recuperar as peticións que poden ser usadas ó comezo dunha conexión.", + "apihelp-query+filerepoinfo-example-login-merged": "Recuperar as peticións que poden ser usadas ó comezo dunha conexión, xunto cos campos de formulario integrados.", + "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Probar se a autenticación é abondo para a acción foo.", "apihelp-query+backlinks-description": "Atopar todas as páxinas que ligan coa páxina dada.", "apihelp-query+backlinks-param-title": "Título a buscar. Non pode usarse xunto con $1pageid.", "apihelp-query+backlinks-param-pageid": "Identificador de páxina a buscar. Non pode usarse xunto con $1title.", @@ -876,6 +892,7 @@ "apihelp-query+prefixsearch-param-limit": "Número máximo de resultados a visualizar.", "apihelp-query+prefixsearch-param-offset": "Número de resultados a saltar.", "apihelp-query+prefixsearch-example-simple": "Buscar títulos de páxina que comecen con meaning.", + "apihelp-query+prefixsearch-param-profile": "Buscar o perfil a usar.", "apihelp-query+protectedtitles-description": "Listar todos os títulos protexidos en creación.", "apihelp-query+protectedtitles-param-namespace": "Só listar títulos nestes espazos de nomes.", "apihelp-query+protectedtitles-param-level": "Só listar títulos con estos niveis de protección.", @@ -988,6 +1005,7 @@ "apihelp-query+search-param-what": "Que tipo de busca lanzar.", "apihelp-query+search-param-info": "Que metadatos devolver.", "apihelp-query+search-param-prop": "Que propiedades devolver:", + "apihelp-query+search-param-qiprofile": "Perfil independente das consultas a usar (afecta ó algoritmo de clasificación).", "apihelp-query+search-paramvalue-prop-size": "Engade o tamaño da páxina en bytes.", "apihelp-query+search-paramvalue-prop-wordcount": "Engade o número de palabras da páxina.", "apihelp-query+search-paramvalue-prop-timestamp": "Engade o selo de tempo da última vez que foi editada a páxina.", @@ -1032,6 +1050,7 @@ "apihelp-query+siteinfo-paramvalue-prop-variables": "Devolve unha lista de identificadores de variable.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "Devolve unha lista de protocolos que están permitidos nas ligazóns externas.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "Devolve os valores por defecto das preferencias de usuario.", + "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "Devolve a configuración do diálogo de subas.", "apihelp-query+siteinfo-param-filteriw": "Só devolver entradas locais ou só non locais da correspondencia interwiki.", "apihelp-query+siteinfo-param-showalldb": "Listar todos os servidores de base de datos, non só o que teña máis retardo.", "apihelp-query+siteinfo-param-numberingroup": "Listar o número de usuarios nos grupos de usuarios.", @@ -1132,6 +1151,7 @@ "apihelp-query+users-paramvalue-prop-emailable": "Marca se o usuario pode e quere recibir correos usando [[Special:Emailuser]].", "apihelp-query+users-paramvalue-prop-gender": "Marca o xénero do usuario. Devolve \"home\", \"muller\" ou \"descoñecido\".", "apihelp-query+users-paramvalue-prop-centralids": "Engade os identificadores centrais e o estado de acoplamento do usuario.", + "apihelp-query+users-paramvalue-prop-cancreate": "Indica se unha conta pode ser creada para nomes de usuario válidos pero non rexistrados.", "apihelp-query+users-param-attachedwiki": "Con $1prop=centralids, \nindica que o usuario está acoplado á wiki identificada por este identificador.", "apihelp-query+users-param-users": "Lista de usuarios para os que obter información.", "apihelp-query+users-param-token": "Usar [[Special:ApiHelp/query+tokens|action=query&meta=tokens]] no canto diso.", @@ -1184,6 +1204,15 @@ "apihelp-query+watchlistraw-param-totitle": "Título (co prefixo de espazo de nomes) no que rematar de enumerar.", "apihelp-query+watchlistraw-example-simple": "Listar páxinas na lista de vixiancia do usuario actual.", "apihelp-query+watchlistraw-example-generator": "Buscar a información de páxina das páxinas da lista de vixiancia do usuario actual.", + "apihelp-removeauthenticationdata-description": "Elimina os datos de autenticación do usuario actual.", + "apihelp-removeauthenticationdata-example-simple": "Intenta eliminar os datos de usuario actual para FooAuthenticationRequest.", + "apihelp-resetpassword-description": "Envía un correo de inicialización de contrasinal a un usuario.", + "apihelp-resetpassword-description-noroutes": "Non están dispoñibles as rutas de reinicio de contrasinal \n\nActive as rutas en [[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]] para usar este módulo.", + "apihelp-resetpassword-param-user": "Usuario sendo reinicializado.", + "apihelp-resetpassword-param-email": "Está reinicializándose o enderezo de correo electrónico do usuario.", + "apihelp-resetpassword-param-capture": "Devolve os contrasinais temporais que se enviaron. Require o dereito de usuario passwordreset .", + "apihelp-resetpassword-example-user": "Enviar un correo de reinicialización de contrasinal ó usuario Exemplo.", + "apihelp-resetpassword-example-email": "Enviar un correo de reinicialización de contrasinal a todos os usuarios con enderezo de correo electrónico usario@exemplo.com.", "apihelp-revisiondelete-description": "Borrar e restaurar revisións.", "apihelp-revisiondelete-param-type": "Tipo de borrado de revisión a ser tratada.", "apihelp-revisiondelete-param-target": "Título de páxina para o borrado da revisión, se requerido para o tipo.", @@ -1252,6 +1281,8 @@ "apihelp-undelete-param-watchlist": "Engadir ou eliminar a páxina da lista de vixiancia do usuario actual sen condicións, use as preferencias ou non cambie a vixiancia.", "apihelp-undelete-example-page": "Restaurar a Páxina Principal.", "apihelp-undelete-example-revisions": "Restaurar dúas revisións de Main Page.", + "apihelp-unlinkaccount-description": "Elimina unha conta vinculada do usuario actual.", + "apihelp-unlinkaccount-example-simple": "Tentar eliminar a ligazón do usuario actual co provedor asociado con FooAuthenticationRequest.", "apihelp-upload-description": "Subir un ficheiro, ou obter o estado de subas pedentes.\n\nHai varios métodos dispoñibles:\n*Subir o contido do ficheiro directamente, usando o parámetro $1file.\n*Subir o ficheiro por partes, usando os parámetros $1filesize, $1chunk, e $1offset.\n*Mandar ó servidor MediaWiki que colla un ficheiro dunha URL, usando o parámetro $1url.\n*Completar unha suba anterior que fallou a causa dos avisos, usando o parámetro $1filekey. \nTeña en conta que o HTTP POST debe facerse como suba de ficheiro (p.ex. usando multipart/form-data)cando se envie o $1file.", "apihelp-upload-param-filename": "Nome de ficheiro obxectivo.", "apihelp-upload-param-comment": "Subir comentario. Tamén usado como texto da páxina inicial para ficheiros novos se non se especifica $1text.", @@ -1361,6 +1392,12 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2", "api-help-right-apihighlimits": "Usar os valores superiores das consultas da API (consultas lentas: $1; consultas rápidas: $2). Os límites para as consultas lentas tamén se aplican ós parámetros multivaluados.", "api-help-open-in-apisandbox": "[abrir en zona de probas]", + "api-help-authmanagerhelper-requests": "Só usar estas peticións de autenticación, co id devolto por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1 ou dunha resposta previa deste módulo.", + "api-help-authmanagerhelper-request": "Usar esta petición de autenticación, co id devolto por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1.", + "api-help-authmanagerhelper-messageformat": "Formato a usar para devolver as mensaxes.", + "api-help-authmanagerhelper-mergerequestfields": "Fusionar os campos de información para todas as peticións de autenticación nunha táboa.", + "api-help-authmanagerhelper-preservestate": "Conservar o estado dun intento previo de conexión fallida, se é posible.", + "api-help-authmanagerhelper-continue": "Esta petición é unha continucación despois dun resposta precedente UI ou REDIRECT. Esta ou $1returnurl é requirida.", "api-credits-header": "Créditos", "api-credits": "Desenvolvedores da API:\n* Roan Kattouw (desenvolvedor principal, set. 2007-2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (creador e desenvolvedor principal, set. 2006-sep. 2007)\n* Brad Jorsch (desenvolvedor principal, 2013-actualidade)\n\nEnvía comentarios, suxerencias e preguntas a mediawiki-api@lists.wikimedia.org\nou informa dun erro en https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index 7a842f4fca..c85a62b8c7 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -40,6 +40,8 @@ "apihelp-block-param-watchuser": "לעקוב אחרי דף המשתמש ודף השיחה של המשתמש או של כתובת ה־IP.", "apihelp-block-example-ip-simple": "חסימת כתובת ה־IP‏ 192.0.2.5 לשלושה ימים עם הסיבה First strike.", "apihelp-block-example-user-complex": "חסימת המשתמש Vandal ללא הגבלת זמן עם הסיבה Vandalism, ומניעת יצירת חשבונות חדשים ושליחת דוא\"ל.", + "apihelp-changeauthenticationdata-description": "שינוי נתוני אימות עבור המשתמש הנוכחי.", + "apihelp-changeauthenticationdata-example-password": "ניסיון לשנות את הססמה של המשתמש הנוכחי ל־ExamplePassword.", "apihelp-checktoken-description": "בדיקת התקינות של האסימון מ־[[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-checktoken-param-type": "סוג האסימון שבבדיקה.", "apihelp-checktoken-param-token": "איזה אסימון לבדוק.", @@ -47,6 +49,9 @@ "apihelp-checktoken-example-simple": "בדיקת התקינות של אסימון csrf.", "apihelp-clearhasmsg-description": "מנקה את דגל hasmsg עבור המשתמש הנוכחי.", "apihelp-clearhasmsg-example-1": "לנקות את דגל hasmsg עבור המשתמש הנוכחי.", + "apihelp-clientlogin-description": "כניסה לוויקי באמצעות זרימה הידודית.", + "apihelp-clientlogin-example-login": "תחילת תהליך כניסה לוויקי בתור משתמש Example עם הססמה ExamplePassword.", + "apihelp-clientlogin-example-login2": "המשך כניסה אחרי תשובת UI לאימות דו־גורמי, עם OATHToken של 987654.", "apihelp-compare-description": "קבלת ההבדל בין 2 דפים.\n\nיש להעביר מספר גרסה, כותרת דף או מזהה דף גם ל־\"from\" וגם ל־\"to\".", "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.", "apihelp-compare-param-fromid": "מס׳ זיהוי של העמוד הראשון להשוואה.", @@ -56,6 +61,8 @@ "apihelp-compare-param-torev": "גרסה שנייה להשוואה.", "apihelp-compare-example-1": "יצירת תיעוד שינוי בין גרסה 1 ל־2.", "apihelp-createaccount-description": "יצירת חשבון משתמש חדש.", + "apihelp-createaccount-param-preservestate": "אם [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] החזיר true עבור hasprimarypreservedstate, בקשות שמסומנות בתור primary-required אמורות להיות מושמטות. אם מוחזר ערך לא ריק ל־preservedusername, שם המשתמש הזה ישמש לפרמטר username.", + "apihelp-createaccount-example-create": "תחילת תהליך יצירת המשתמש Example עם הססמה ExamplePassword.", "apihelp-createaccount-param-name": "שם משתמש.", "apihelp-createaccount-param-password": "ססמה (לא ישפיע אם הוגדר $1mailpassword).", "apihelp-createaccount-param-domain": "שם מתחם לאימות חיצוני (רשות).", @@ -204,7 +211,11 @@ "apihelp-import-param-namespace": "לייבא למרחב השם הזה. לא ניתן להשתמש בזה יחד עם $1rootpage.", "apihelp-import-param-rootpage": "לייבא בתור תת־משנה של הדף הזה. לא ניתן להשתמש בזה יחד עם $1namespace.", "apihelp-import-example-import": "לייבא את [[meta:Help:ParserFunctions]] למרחב השם 100 עם היסטוריה מלאה.", - "apihelp-login-description": "להיכנס ולקבל עוגיות אימות.\n\nבמקרה של כניסה מוצלחת, העוגיות הדרושות תיכללנה בכותרות תשובות של HTTP. במקרה של כניסה לא מוצלחת, הניסיונות הבאים עשויים להיות חנוקים כדי להגביל תקיפות ניחוש ססמה אוטומטי.", + "apihelp-linkaccount-description": "קישור חשבון של ספק צד־שלישי למשתמש הנוכחי.", + "apihelp-linkaccount-example-link": "תחילת תהליך הקישור לחשבון מ־Example.", + "apihelp-login-description": "להיכנס ולקבל עוגיות אימות.\n\nהפעולה הזאת צריכה לשמש רק בשילוב [[Special:BotPasswords]]; שימוש לכניסה לחשבון ראשי מיושן ועשוי להיכשל ללא אזהרה. כדי להיכנס בבטחה לחשבון הראשי, יש להשתמש ב־[[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nobotpasswords": "להיכנס ולקבל עוגיות אימות.\n\nהפעולה הזאת מיושנת ועשויה להיכשל ללא אזהרה. כדי להיכנס בבטחה, יש להשתמש ב־[[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nonauthmanager": "להיכנס ולקבל עוגיות אימות.\n\nבמקרה של כניסה מוצלחת, העוגיות המקוננות תיכללנה בכותרות תשובות ה־HTTP. במקרה של כניסה כושלת, הניסיונות הבאים יוגבלו למספר ניסויי ניחוש הססמה האוטומטיים.", "apihelp-login-param-name": "שם משתמש.", "apihelp-login-param-password": "ססמה.", "apihelp-login-param-domain": "שם מתחם (רשות).", @@ -536,6 +547,12 @@ "apihelp-query+allusers-param-activeusers": "לרשום רק משתמשים שהיו פעילים {{PLURAL:$1|ביום האחרון|ביומיים האחרונים|ב־$1 הימים האחרונים}}.", "apihelp-query+allusers-param-attachedwiki": "עם $1prop=centralids, לציין גם האם המשתמש משויך לוויקי עם המזהה הזה.", "apihelp-query+allusers-example-Y": "לרשום משתמשים שמתחילים ב־Y.", + "apihelp-query+authmanagerinfo-description": "אחזור מידע אודות מצב האימות הנוכחי.", + "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "בדיקה האם מצב האימות הנוכחי של המשתמש מספיק בשביל הפעולה הרגישה מבחינת אבטחה שצוינה.", + "apihelp-query+authmanagerinfo-param-requestsfor": "אחזור מידע על בקשות האימות הדרושות לפעולת האימות המבוקשת.", + "apihelp-query+filerepoinfo-example-login": "אחזור הבקשות שיכולות לשמש לתחילת הכניסה.", + "apihelp-query+filerepoinfo-example-login-merged": "אחזור הבקשות שיכולות לשמש לתחילת הכניסה, עם שדות טופס ממוזגים.", + "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "בדיקה האם האימות מספיק בשביל הפעולה foo.", "apihelp-query+backlinks-description": "מציאת כל הדפים שמקשרים לדף הנתון.", "apihelp-query+backlinks-param-title": "איזו כותרת לחפש. לא ניתן להשתמש בזה יחד עם $1pageid.", "apihelp-query+backlinks-param-pageid": "מזהה דף לחיפוש. לא ניתן להשתמש בזה יחד עם $1title.", @@ -877,6 +894,7 @@ "apihelp-query+prefixsearch-param-limit": "מספר התוצאות המרבי להחזרה.", "apihelp-query+prefixsearch-param-offset": "מספר תוצאות לדילוג.", "apihelp-query+prefixsearch-example-simple": "חיפוש שםות דפים שמתחילים ב־meaning.", + "apihelp-query+prefixsearch-param-profile": "באיזה פרופיל חיפוש להשתמש.", "apihelp-query+protectedtitles-description": "לרשום את כל הכותרות שמוגנות מפני יצירה.", "apihelp-query+protectedtitles-param-namespace": "לרשום רק כותרות במרחבי השם האלה.", "apihelp-query+protectedtitles-param-level": "לרשום רק שמות עם רמת ההגנה הזאת.", @@ -989,6 +1007,7 @@ "apihelp-query+search-param-what": "איזה סוג חיפוש לבצע.", "apihelp-query+search-param-info": "אילו מטא־נתונים להחזיר.", "apihelp-query+search-param-prop": "אילו מאפיינים להחזיר:", + "apihelp-query+search-param-qiprofile": "באיזה פרופיל בלתי־תלוי בשאילתה להשתמש (משפיע על אלגוריתם הדירוג).", "apihelp-query+search-paramvalue-prop-size": "הוספת גודל הדף בבתים.", "apihelp-query+search-paramvalue-prop-wordcount": "הוספת מניין המילים של הדף.", "apihelp-query+search-paramvalue-prop-timestamp": "הוספת חותם־הזמן של העריכה האחרונה של הדף.", @@ -1033,6 +1052,7 @@ "apihelp-query+siteinfo-paramvalue-prop-variables": "החזרת מזהי משתנים.", "apihelp-query+siteinfo-paramvalue-prop-protocols": "החזרת רשימת הפרוטוקולים המותרים בקישורים חיצוניים.", "apihelp-query+siteinfo-paramvalue-prop-defaultoptions": "החזרת הערכים ההתחלתיים של העדפות משתמש.", + "apihelp-query+siteinfo-paramvalue-prop-uploaddialog": "החזרת הגדרות תיבת ההעלאה.", "apihelp-query+siteinfo-param-filteriw": "החזרה רק של עיולים מקומיים או רק של עיולים לא מקומיים ממפת הבינוויקי.", "apihelp-query+siteinfo-param-showalldb": "רשימת כל שרתי מסד הנתונים, לא רק אלה שהכי מתעכבים.", "apihelp-query+siteinfo-param-numberingroup": "רשימת מספרי משתמשים בקבוצות משתמשים.", @@ -1133,6 +1153,7 @@ "apihelp-query+users-paramvalue-prop-emailable": "מתייג אם המשתמש יכול ורוצה לקבל דואר אלקטרוני דרך [[Special:Emailuser]].", "apihelp-query+users-paramvalue-prop-gender": "מתייג את המגדר של המשתמש. מחזיר \"male\"‏, \"female\" או \"unknown\".", "apihelp-query+users-paramvalue-prop-centralids": "הוספת המזהה המרכזי ומצב השיוך למשתמש.", + "apihelp-query+users-paramvalue-prop-cancreate": "ציון האם אפשר ליצור חשבון עבור שמות משתמש תקינים, אבל לא רשומים.", "apihelp-query+users-param-attachedwiki": "עם $1prop=centralids, לציין האם המשתמש משויך לוויקי עם המזהה הזה.", "apihelp-query+users-param-users": "רשימת משתמשים שעליהם צריך לקבל מידע.", "apihelp-query+users-param-token": "יש להשתמש ב־[[Special:ApiHelp/query+tokens|action=query&meta=tokens]] במקום.", @@ -1185,6 +1206,15 @@ "apihelp-query+watchlistraw-param-totitle": "באיזו כותרת (עם תחילית מרחב שם) להפסיק למנות.", "apihelp-query+watchlistraw-example-simple": "לרשום דפים ברשימת המעקב של המשתמש הנוכחי.", "apihelp-query+watchlistraw-example-generator": "אחזור מידע על הדפים עבור דפים ברשימת המעקב של המשתמש הנוכחי.", + "apihelp-removeauthenticationdata-description": "הסרת נתוני אימות עבור המשתמש הנוכחי.", + "apihelp-removeauthenticationdata-example-simple": "לנסות להסיר את נתוני המשתמש הנוכחי בשביל FooAuthenticationRequest.", + "apihelp-resetpassword-description": "שליחת דוא\"ל איפוס סיסמה למשתמש.", + "apihelp-resetpassword-description-noroutes": "אין מסלולים לאיפוס ססמה.\n\nכדי להשתמש במודול הזה, יש להפעיל מסלולים ב־[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]].", + "apihelp-resetpassword-param-user": "המשתמש שמאופס.", + "apihelp-resetpassword-param-email": "כתובת הדוא\"ל של המשתמש שהסיסמה שלו מאופסת.", + "apihelp-resetpassword-param-capture": "החזרת הססמאות הזמניות שנשלחו. דורש את ההרשאה passwordreset.", + "apihelp-resetpassword-example-user": "שליחת מכתב איפוס ססמה למשתמש Example.", + "apihelp-resetpassword-example-email": "שליחת מכתב איפוס ססמה לכל המשתמשים שהכתובת שלהם היא user@example.com.", "apihelp-revisiondelete-description": "מחיקה ושחזור ממחיקה של גרסאות.", "apihelp-revisiondelete-param-type": "סוג מחיקת הגרסה שמתבצע.", "apihelp-revisiondelete-param-target": "שם הדף למחיקת גרסה, אם זה נחוץ לסוג.", @@ -1253,6 +1283,8 @@ "apihelp-undelete-param-watchlist": "הוספה או הסרה של הדף ללא תנאי מרשימת המעקב של המשתמש הנוכחי, להשתמש בהעדפות או לא לשנות את המעקב.", "apihelp-undelete-example-page": "שחזור ממחיקה של הדף Main Page.", "apihelp-undelete-example-revisions": "שחזור שתי גרסאות של הדף Main Page.", + "apihelp-unlinkaccount-description": "ביטול קישור של חשבון צד־שלישי מהמשתמש הנוכחי.", + "apihelp-unlinkaccount-example-simple": "לנסות להסיר את הקישור של המשתמש הנוכחי לספק המשויך עם FooAuthenticationRequest.", "apihelp-upload-description": "העלאת קובץ, או קבלת מצב ההעלאות הממתינות.\n\nיש מספר שיטות:\n* להעלות את הקובץ ישירות, באמצעות הפרמטר $1file.\n* להעלות את הקובץ בחלקים, באמצעות הפרמטרים $1filesize‏, $1chunk ו־$1offset.\n* לגרום לשרת מדיה־ויקי לאחזר את הקובץ מ־URL באמצעות הפרמטר $1url.\n* להשלים העלאה קודמת שנכשלה בשל אזהרות באמצעות הפרמטר $1filekey.\nלתשומך לבך, יש לעשות את HTTP POST בתור העלאת קובץ (כלומר באמצעות multipart/form-data) בעת שליחת ה־$1file.", "apihelp-upload-param-filename": "שם קובץ היעד.", "apihelp-upload-param-comment": "הערת העלאה. משמש גם בתור טקסט הדף ההתחלתי עבור קבצים חדשים אם $1text אינו מצוין.", @@ -1362,6 +1394,15 @@ "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# אחזור השדות הזמינים מ־[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] עם amirequestsfor=$4 ואסימון $5 מתוך [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# הצגת השדות למשתמש וקבלת אישור ממנו.\n# שליחה (Post) למודול הזה עם $1returnurl וכל השדות הרלוונטיים.\n# בדיקת ה־status בתשובה.\n#* אם קיבלת PASS או FAIL, זה הסיום. הפעולה שלך הצליחה או נכשלה.\n#* אם קיבלת UI, יש להציג את השדות החדשים למשתמש ולקבל את מה שהוא ישלח. אחר־כך יש לשלוח (post) למודול הזה עם $1continue ועם הגדרות של השדות הרלוונטיים ולחזור על צעד 4.\n#* אם קיבלת REDIRECT, יש להפנות את המשתמש ל־redirecttarget ולחכות לחזרה אל $1returnurl. אחר־כך לשלוח (post) למודול הזה עם $1continue ועם כל השדות שהועברו ל־URL שחוזרים אליו ולחזור על צעד 4.\n#* אם קיבלת RESTART, זה אומר שהאימות עבד אבל אין חשבון משתמש מקושר. באפשרותך לטפל בזה כמו ב־UI או ב־FAIL.", + "api-help-authmanagerhelper-requests": "להשתמש רק בבקשות האימות האלו, מאת id שהוחזר מ־[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] עם amirequestsfor=$1 או מתשובה קודמת למודול הזה.", + "api-help-authmanagerhelper-request": "להשתמש בבקשת האימות הזאת, מאת id שהוחזר מ־[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] עם amirequestsfor=$1.", + "api-help-authmanagerhelper-messageformat": "תסדיר לשימוש בהחזרת הודעות.", + "api-help-authmanagerhelper-mergerequestfields": "מיזוג מידע של שדות עבור כל בקשות האימות למערך אחד.", + "api-help-authmanagerhelper-preservestate": "שימור מצב מניסיון כניסה קודם, אם אפשר.", + "api-help-authmanagerhelper-returnurl": "כתובת URL לחזרה עם זרימות אימות צד־שלישי, חייב להיות מוחלט. נדרש או זה או $1continue.\n\nעם קבלת תשובת REDIRECT, בדרך־כלל תפתח דפדפן או תצוגת וב בכתובת ה־redirecttarget שצוינה בשביל זרימת אימות צד־שלישי. כשזה יושלם, הצד השלישי ישלח את הדפדפן או את תצוגת הווב לכתובת הזאת. יש לחלץ את כל הפרמטרים של שאילתה או בקשת POST מה־URL ולהעביר אותם בתור בקשת $1continue למודול ה־API הזה.", + "api-help-authmanagerhelper-continue": "הבקשה הזאת היא המשך אחרי תשובת UI או REDIRECT קודמת. נדרש זה או $1returnurl.", + "api-help-authmanagerhelper-additional-params": "המודול הזה מקבל פרמטרים נוספים בהתאם לבקשות אימות זמינות. יש להשתמש ב־[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] עם amirequestsfor=$1 (או תגובה קודמת מהמודול הזה, אם זה זמין) כדי להבין מה הבקשות הזמינות ובאילו שדות הן משתמשות.", "api-credits-header": "קרדיטים", "api-credits": "מפתחי ה־API:\n* רואן קטאו (מפתח מוביל 2007–2009)\n* ויקטור וסילייב\n* בריאן טונג מין\n* סאם ריד\n* יורי אסטרחן (יוצר, מפתח מוביל מספטמבר 2006 עד ספטמבר 2007)\n* בראד יורש (מפתח מוביל מאז 2013)\n\nאנא שלחו הערות, הצעות ושאלות לכתובת mediawiki-api@lists.wikimedia.org או כתבו דיווח באג באתר https://phabricator.wikimedia.org." } diff --git a/includes/api/i18n/ia.json b/includes/api/i18n/ia.json index 7a7675d025..147a832a73 100644 --- a/includes/api/i18n/ia.json +++ b/includes/api/i18n/ia.json @@ -26,6 +26,7 @@ "apihelp-checktoken-param-type": "Typo de indicio a testar.", "apihelp-checktoken-param-token": "Indicio a testar.", "apihelp-createaccount-param-name": "Nomine de usator.", + "apihelp-query+prefixsearch-param-profile": "Le profilo de recerca a usar.", "apihelp-query+revisions-example-first5-not-localhost": "Obtener le prime 5 versiones del \"Pagina principal\" que non ha essite facite per le usator anonyme \"127.0.0.1\"", "api-credits": "Programmatores del API:\n* Roan Kattouw (programmator dirigente Sept. 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (creator, programmator dirigente Sept. 2006–Sept. 2007)\n* Brad Jorsch (programmator dirigente 2013–presente)\n\nInvia tu commentos, suggestiones e questiones a mediawiki-api@lists.wikimedia.org\no insere un reportage de bug a https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/id.json b/includes/api/i18n/id.json index dae55de349..d93dc234d1 100644 --- a/includes/api/i18n/id.json +++ b/includes/api/i18n/id.json @@ -7,9 +7,92 @@ "Kenrick95" ] }, + "apihelp-main-param-action": "Tindakan manakah yang akan dilakukan.", + "apihelp-main-param-format": "Format keluaran.", "apihelp-block-description": "Blokir pengguna.", "apihelp-block-param-user": "Nama pengguna, alamat IP, atau rentang alamat IP untuk diblokir.", + "apihelp-block-param-expiry": "Waktu kedaluwarsa. Dapat berupa waktu relatif (seperti 5 bulan atau 2 minggu) atau waktu absolut (seperti 2014-09-18T12:34:56Z). Jika diatur ke selamanya, tak terbatas, atau tidak pernah, pemblokiran itu tidak akan berakhir.", + "apihelp-block-param-reason": "Alasan pemblokiran.", + "apihelp-block-param-anononly": "Blokir hanya pengguna anonim (seperti menonaktifkan suntingan anonim untuk alamat IP ini).", + "apihelp-block-param-nocreate": "Cegah pembuatan akun.", + "apihelp-block-param-autoblock": "Blokir alamat IP terakhir yang digunakan pengguna ini, dan semua alamat IP berikutnya yang mereka coba gunakan untuk menyunting.", + "apihelp-block-param-noemail": "Cegah pengguna mengirimkan surel melalui wiki. (Membutuhkan hak blockemail).", + "apihelp-block-param-reblock": "Jika pengguna tersebut sudah diblokir, atur ulang setelah pemblokirannya.", + "apihelp-block-example-ip-simple": "Blokir alamat IP 192.0.2.5 selama tiga hari dengan alasan Serangan pertama.", + "apihelp-compare-param-fromtitle": "Judul pertama untuk dibandingkan.", + "apihelp-compare-param-fromid": "ID halaman pertama untuk dibandingkan.", + "apihelp-compare-param-fromrev": "Revisi pertama untuk dibandingkan.", + "apihelp-compare-param-toid": "ID halaman kedua untuk dibandingkan.", + "apihelp-compare-param-torev": "Revisi kedua untuk dibandingkan.", + "apihelp-compare-example-1": "Buat perbedaan antara revisi 1 dan 2.", + "apihelp-createaccount-description": "Buat akun pengguna baru.", + "apihelp-createaccount-example-create": "Mulai proses pembuatan pengguna Contoh dengan kata sandi ContohKataSandi.", "apihelp-createaccount-param-name": "Nama pengguna", + "apihelp-createaccount-param-password": "Kata sandi (diabaikan jika $1mailpassword diatur).", + "apihelp-createaccount-param-domain": "Domain untuk otentikasi eksternal (opsional).", + "apihelp-createaccount-param-token": "Token pembuatan akun yang diperoleh pada permintaan pertama.", + "apihelp-createaccount-param-email": "Alamat surel pengguna (opsional).", + "apihelp-createaccount-param-realname": "Nama asli pengguna (opsional).", + "apihelp-createaccount-param-mailpassword": "Jika diberikan nilai, kata sandi acak akan dikirimkan melalui surel kepada pengguna.", + "apihelp-createaccount-param-reason": "Alasan tambahan untuk membuat akun yang akan dicatat dalam log.", + "apihelp-createaccount-param-language": "Kode bahasa untuk diatur sebagai baku kepada pengguna (opsional, nilai bakunya adalah bahasa isi).", + "apihelp-createaccount-example-pass": "Buat pengguna testuser dengan kata sandi test123.", + "apihelp-createaccount-example-mail": "Buat pengguna testmailuser dan kirim surel berisi kata sandi acak.", + "apihelp-delete-description": "Hapus halaman", + "apihelp-delete-param-title": "Judul halaman untuk dihapus. Tidak dapat digunakan bersama dengan $1pageid.", + "apihelp-delete-param-pageid": "ID halaman dari halaman yang akan dihapus. Tidak dapat digunakan bersama dengan $1title.", + "apihelp-delete-param-reason": "Alasan penghapusan. Jika tidak diberikan, alasan yang dihasilkan secara otomatis akan digunakan.", + "apihelp-delete-param-tags": "Ganti tag untuk diterapkan ke entri di log penghapusan.", + "apihelp-delete-param-watch": "Tambahkan halaman ke daftar pantauan pengguna saat ini.", + "apihelp-delete-param-watchlist": "Buat atau hapus halaman tanpa syarat dari daftar pantauan pengguna saat ini, gunakan preferensi atau jangan ganti pantauan.", + "apihelp-delete-param-unwatch": "Hapus halaman dari daftar pantauan pengguna saat ini.", + "apihelp-delete-param-oldimage": "Nama gambar lama untuk dihapus seperti yang disebutkan oleh [[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]].", + "apihelp-delete-example-simple": "Hapus Halaman Utama.", + "apihelp-delete-example-reason": "Hapus Halaman Utama dengan alasan Persiapan untuk dialihkan.", + "apihelp-disabled-description": "Modul ini telah dimatikan.", + "apihelp-edit-description": "Buat dan sunting halaman.", + "apihelp-edit-param-title": "Judul halaman untuk dibuat. Tidak dapat digunakan bersama dengan $1pageid.", + "apihelp-edit-param-pageid": "ID halaman dari halaman yang akan disunting. Tidak dapat digunakan bersama dengan $1title.", + "apihelp-edit-param-section": "Nomor bagian. 0 untuk bagian atas, baru untuk bagian baru.", + "apihelp-edit-param-sectiontitle": "Judul untuk bagian baru.", + "apihelp-edit-param-text": "Isi halaman.", + "apihelp-edit-param-summary": "Ringkasan suntingan. Juga tajuk bagian ketika $1section=new dan $1sectiontitle tidak diatur.", + "apihelp-edit-param-tags": "Ganti tag untuk menerapkan ke revisi.", + "apihelp-edit-param-minor": "Suntingan kecil.", + "apihelp-edit-param-notminor": "Bukan suntingan kecil.", + "apihelp-edit-param-bot": "Tandai suntingan ini sebagai bot.", + "apihelp-edit-param-basetimestamp": "Stempel waktu dari revisi asal, digunakan untuk mendeteksi konflik penyuntingan. Dapat ditemukan di [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", + "apihelp-edit-param-starttimestamp": "Stempel waktu ketika proses penyuntingan dimulai, digunakan untuk mendeteksi konflik penyuntingan. Nilai yang cocok dapat ditemukan dengan menggunakan [[Special:ApiHelp/main|curtimestamp]] ketika memulai proses penyuntingan (seperti ketika memuat isi konten yang akan disunting).", + "apihelp-edit-param-recreate": "Batalkan galat yang terjadi tentang halaman yang sudah dihapus pada saat itu.", + "apihelp-edit-param-createonly": "Jangan sunting halaman itu jika sudah ada.", + "apihelp-edit-param-nocreate": "Berikan galat jika halaman belum ada.", + "apihelp-edit-param-watch": "Tambahkan halaman ke daftar pantauan pengguna saat ini.", + "apihelp-edit-param-unwatch": "Hapus halaman dari daftar pantauan pengguna saat ini.", + "apihelp-edit-param-watchlist": "Buat atau hapus halaman tanpa syarat dari daftar pantauan pengguna saat ini, gunakan preferensi atau jangan ganti pantauan.", + "apihelp-edit-param-md5": "Hash MD5 dari parameter $1text, atau parameter $1prependtext dan $1appendtext digabungkan. Jika diatur, suntingan itu tidak akan dilakukan kecuali hash tidak benar.", + "apihelp-edit-param-prependtext": "Tambahkan teks berikut ke bagian awal halaman. Abaikan $1text.", + "apihelp-edit-param-appendtext": "Tambahkan teks berikut ke bagian akhir halaman. Abaikan $1text.\n\nGunakan $1section=new untuk menambahkan sebuah bagian baru, daripada parameter ini.", + "apihelp-edit-param-undo": "Batalkan revisi ini. Abaikan $1text, $1prependtext dan $1appendtext.", + "apihelp-edit-param-undoafter": "Batalkan semua revisi dari $1undo ke revisi ini. Jika tidak diatur, batalkan satu revisi saja.", + "apihelp-edit-param-redirect": "Selesaikan pengalihan secara otomatis.", + "apihelp-edit-param-contentformat": "Format serialisasi isi digunakan untuk teks masukan.", + "apihelp-edit-param-contentmodel": "Model konten dari konten baru.", + "apihelp-edit-param-token": "Token harus selalu dikirim sebagai parameter terakhir, atau setidaknya sesudah parameter $1text.", + "apihelp-edit-example-edit": "Sunting halaman.", + "apihelp-edit-example-prepend": "Tambahkan __NOTOC__ ke halaman.", + "apihelp-edit-example-undo": "Batalkan revisi 13579 melalui 13585 dengan ringkasan otomatis.", + "apihelp-emailuser-description": "Kirim surel ke pengguna ini.", + "apihelp-emailuser-param-target": "Pengguna yang akan dikirimi surel.", + "apihelp-emailuser-param-subject": "Tajuk subjek.", + "apihelp-emailuser-param-text": "Badan pesan.", + "apihelp-emailuser-param-ccme": "Kirimkan salinan pesan ini kepada saya.", + "apihelp-expandtemplates-description": "Tambahkan semua templat dalam teks wiki.", + "apihelp-expandtemplates-param-title": "Judul halaman.", + "apihelp-expandtemplates-param-text": "Teks wiki yang akan diubah.", + "apihelp-expandtemplates-param-revid": "ID revisi, untuk {{REVISIONID}} dan variabel serupa.", + "apihelp-expandtemplates-param-prop": "Bagian informasi manakah yang ingin didapatkan.\n\nPerhatikan bahwa jika tidak ada nilai yang dipilih, hasilnya akan mengandung teks wiki, namun keluaran akan berupa format usang.", "apihelp-login-example-login": "Masuk log.", + "apihelp-query+prefixsearch-param-profile": "Cari profil untuk digunakan.", + "apihelp-query+search-param-qiprofile": "Meminta profil independen untuk digunakan (berefek pada algoritma peringkat).", "apihelp-revisiondelete-param-ids": "Penanda untuk perubahan yang akan dihapus" } diff --git a/includes/api/i18n/it.json b/includes/api/i18n/it.json index 6b0cab6948..54373a4b5f 100644 --- a/includes/api/i18n/it.json +++ b/includes/api/i18n/it.json @@ -12,7 +12,8 @@ "Macofe", "Nemo bis", "JackLantern", - "Urielejh" + "Urielejh", + "Matteocng" ] }, "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentazione]] (in inglese)\n* [[mw:API:FAQ|FAQ]] (in inglese)\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Mailing list]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Annunci sull'API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Bug & richieste]\n
\nStato: tutte le funzioni e caratteristiche mostrate su questa pagina dovrebbero funzionare, ma le API sono ancora in fase attiva di sviluppo, e potrebbero cambiare in qualsiasi momento. Iscriviti alla [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la mailing list sugli annunci delle API MediaWiki] per essere informato sugli aggiornamenti.\n\nIstruzioni sbagliate: quando vengono impartite alle API delle istruzioni sbagliate, un'intestazione HTTP verrà inviata col messaggio \"MediaWiki-API-Error\" e, sia il valore dell'intestazione, sia il codice d'errore, verranno impostati con lo stesso valore. Per maggiori informazioni leggi [[mw:API:Errors_and_warnings|API:Errori ed avvertimenti]] (in inglese).\n\nTest: per testare facilmente le richieste API, vedi [[Special:ApiSandbox]].", @@ -34,6 +35,8 @@ "apihelp-block-param-watchuser": "Segui la pagina utente e le pagine di discussione utente dell'utente o dell'indirizzo IP.", "apihelp-block-example-ip-simple": "Blocca l'indirizzo IP 192.0.2.5 per tre giorni con motivazione First strike.", "apihelp-block-example-user-complex": "Blocca l'utente Vandal a tempo indeterminato con motivazione Vandalism, e impediscigli la creazione di nuovi account e l'invio di e-mail.", + "apihelp-changeauthenticationdata-description": "Modificare i dati di autenticazione per l'utente corrente.", + "apihelp-changeauthenticationdata-example-password": "Tentativo di modificare la password dell'utente corrente a ExamplePassword.", "apihelp-checktoken-description": "Verifica la validità di un token da [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-checktoken-param-type": "Tipo di token in corso di test.", "apihelp-checktoken-param-token": "Token da testare.", @@ -41,6 +44,9 @@ "apihelp-checktoken-example-simple": "Verifica la validità di un token csrf.", "apihelp-clearhasmsg-description": "Cancella il flag hasmsg per l'utente corrente.", "apihelp-clearhasmsg-example-1": "Cancella il flag hasmsg per l'utente corrente.", + "apihelp-clientlogin-description": "Accedi al wiki utilizzando il flusso interattivo.", + "apihelp-clientlogin-example-login": "Avvia il processo di accesso alla wiki come utente Example con password ExamplePassword.", + "apihelp-clientlogin-example-login2": "Continua l'accesso dopo una risposta dell'UI per l'autenticazione a due fattori, fornendo un OATHToken di 987654.", "apihelp-compare-description": "Ottieni le differenze tra 2 pagine.\n\nUn numero di revisione, il titolo di una pagina, o un ID di pagina deve essere indicato sia per il \"da\" che per lo \"a\".", "apihelp-compare-param-fromtitle": "Primo titolo da confrontare.", "apihelp-compare-param-fromid": "Primo ID di pagina da confrontare.", @@ -50,6 +56,8 @@ "apihelp-compare-param-torev": "Seconda revisione da confrontare.", "apihelp-compare-example-1": "Crea un diff tra revisione 1 e revisione 2.", "apihelp-createaccount-description": "Crea un nuovo account utente.", + "apihelp-createaccount-param-preservestate": "Se [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] ha restituito true per hasprimarypreservedstate, le richieste contrassegnate come primary-required dovrebbero essere omesse. Se invece ha restituito un valore non vuoto per preservedusername, quel nome utente deve essere utilizzato per il parametro username.", + "apihelp-createaccount-example-create": "Avvia il processo di creazione utente Example con password ExamplePassword.", "apihelp-createaccount-param-name": "Nome utente.", "apihelp-createaccount-param-password": "Password (verrà ignorata se è impostato $1mailpassword).", "apihelp-createaccount-param-domain": "Dominio per l'autenticazione esterna (opzionale).", @@ -154,6 +162,11 @@ "apihelp-import-param-namespace": "Importa in questo namespace. Non può essere usato insieme a $1rootpage.", "apihelp-import-param-rootpage": "Importa come sottopagina di questa pagina. Non può essere usato insieme a $1namespace.", "apihelp-import-example-import": "Importa [[meta:Help:ParserFunctions]] nel namespace 100 con cronologia completa.", + "apihelp-linkaccount-description": "Collegamento di un'utenza di un provider di terze parti all'utente corrente.", + "apihelp-linkaccount-example-link": "Avvia il processo di collegamento ad un'utenza da Example.", + "apihelp-login-description": "Accedi e ottieni i cookie di autenticazione.\n\nQuesta azione deve essere usata esclusivamente in combinazione con [[Special:BotPasswords]]; utilizzarla per l'accesso all'account principale è deprecato e può fallire senza preavviso. Per accedere in modo sicuro all'utenza principale, usa [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nobotpasswords": "Accedi e ottieni i cookies di autenticazione.\n\nQuesta azione è deprecata e può fallire senza preavviso. Per accedere in modo sicuro, usa [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nonauthmanager": "Accedi e ottieni i cookie di autenticazione.\n\nIn caso di accesso riuscito, i cookies necessari saranno inclusi nella intestazioni di risposta HTTP. In caso di accesso fallito, ulteriori tentativi potrebbero essere limitati, in modo da contenere gli attacchi automatizzati per indovinare le password.", "apihelp-login-param-name": "Nome utente.", "apihelp-login-param-password": "Password.", "apihelp-login-param-domain": "Dominio (opzionale).", @@ -308,6 +321,12 @@ "apihelp-query+allusers-param-excludegroup": "Escludi gli utenti nei gruppi indicati.", "apihelp-query+allusers-param-prop": "Quali pezzi di informazioni includere:", "apihelp-query+allusers-param-limit": "Quanti nomi utente totali restituire.", + "apihelp-query+authmanagerinfo-description": "Recupera informazioni circa l'attuale stato di autenticazione.", + "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Verifica se lo stato di autenticazione dell'utente attuale è sufficiente per la specifica operazione sensibile alla sicurezza.", + "apihelp-query+authmanagerinfo-param-requestsfor": "Recupera informazioni circa le richieste di autenticazione necessarie per la specifica azione di autenticazione.", + "apihelp-query+filerepoinfo-example-login": "Recupera le richieste che possono essere utilizzate quando si inizia l'accesso.", + "apihelp-query+filerepoinfo-example-login-merged": "Recupera le richieste che possono essere utilizzate quando si inizia l'accesso, con i campi del modulo uniti.", + "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Verificare se l'autenticazione è sufficiente per l'azione foo.", "apihelp-query+backlinks-description": "Trova tutte le pagine che puntano a quella specificata.", "apihelp-query+backlinks-param-namespace": "Il namespace da elencare.", "apihelp-query+backlinks-param-dir": "La direzione in cui elencare.", @@ -448,6 +467,7 @@ "apihelp-query+prefixsearch-param-search": "Stringa di ricerca.", "apihelp-query+prefixsearch-param-limit": "Numero massimo di risultati da restituire.", "apihelp-query+prefixsearch-param-offset": "Numero di risultati da saltare", + "apihelp-query+prefixsearch-param-profile": "Profilo di ricerca da utilizzare.", "apihelp-query+protectedtitles-description": "Elenca tutti i titoli protetti dalla creazione.", "apihelp-query+protectedtitles-param-namespace": "Elenca solo i titoli in questi namespace.", "apihelp-query+protectedtitles-param-level": "Elenca solo i titoli con questi livelli di protezione.", @@ -525,6 +545,7 @@ "apihelp-query+userinfo-example-simple": "Ottieni informazioni sull'utente attuale.", "apihelp-query+users-description": "Ottieni informazioni su un elenco di utenti.", "apihelp-query+users-param-prop": "Quali pezzi di informazioni includere:", + "apihelp-query+users-paramvalue-prop-cancreate": "Indica se può essere creata un'utenza per nomi utente validi ma non registrati.", "apihelp-query+users-param-users": "Un elenco di utenti di cui ottenere informazioni.", "apihelp-query+watchlist-description": "Ottieni le ultime modifiche alle pagine tra gli osservati speciali dell'utente attuale.", "apihelp-query+watchlist-param-start": "Il timestamp da cui iniziare l'elenco.", @@ -541,6 +562,15 @@ "apihelp-query+watchlistraw-param-totitle": "Il titolo (con prefisso namespace) al quale interrompere l'elenco.", "apihelp-query+watchlistraw-example-simple": "Elenca le pagine fra gli osservati speciali dell'utente attuale.", "apihelp-query+watchlistraw-example-generator": "Recupera le informazioni sulle pagine fra gli osservati speciali dell'utente attuale.", + "apihelp-removeauthenticationdata-description": "Rimuove i dati di autenticazione per l'utente corrente.", + "apihelp-removeauthenticationdata-example-simple": "Tentativo di rimuovere gli attuali dati utente per FooAuthenticationRequest.", + "apihelp-resetpassword-description": "Invia una mail per reimpostare la password di un utente.", + "apihelp-resetpassword-description-noroutes": "Non sono disponibili rotte per la reimpostazione della password.\n\nAbilita le rotte in [[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]] per usare questo modulo.", + "apihelp-resetpassword-param-user": "Utente in corso di ripristino.", + "apihelp-resetpassword-param-email": "Indirizzo di posta elettronica dell'utente in corso di ripristino.", + "apihelp-resetpassword-param-capture": "Restituisce le password temporanee che erano state inviate. Richiede il diritto utente passwordreset.", + "apihelp-resetpassword-example-user": "Invia una mail per reimpostare la password all'utente Example.", + "apihelp-resetpassword-example-email": "Invia una mail per reimpostare la password a tutti gli utenti con indirizzo di posta elettronica user@example.com.", "apihelp-revisiondelete-description": "Cancella e ripristina le versioni.", "apihelp-revisiondelete-param-type": "Tipo di cancellazione della versione effettuata.", "apihelp-revisiondelete-param-hide": "Cosa nascondere per ogni versione.", @@ -559,6 +589,8 @@ "apihelp-undelete-param-title": "Titolo della pagina da ripristinare.", "apihelp-undelete-param-reason": "Motivo per il ripristino.", "apihelp-undelete-param-tags": "Modifica etichette da applicare all'elemento del registro delle cancellazioni.", + "apihelp-unlinkaccount-description": "Rimuove un'utenza di terze parti collegata all'utente corrente.", + "apihelp-unlinkaccount-example-simple": "Tentativo di rimuovere il collegamento dell'utente corrente per il provider associato con FooAuthenticationRequest.", "apihelp-upload-param-watch": "Osserva la pagina.", "apihelp-upload-param-file": "Contenuto del file.", "apihelp-upload-example-url": "Carica da un URL.", @@ -613,5 +645,11 @@ "api-help-examples": "{{PLURAL:$1|Esempio|Esempi}}:", "api-help-permissions": "{{PLURAL:$1|Permesso|Permessi}}:", "api-help-open-in-apisandbox": "[apri in una sandbox]", + "api-help-authmanager-general-usage": "La procedura generale per usare questo modulo é:\n# Ottenere i campi disponibili da [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$4, e un token $5 da [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Mostra i campi all'utente e ottieni i dati che invia.\n# Esegui un post a questo modulo, fornendo $1returnurl e ogni campo rilevante.\n# Controlla status nella response.\n#* Se hai ricevuto PASS o FAIL, hai finito. L'operazione nel primo caso è andata a buon fine, nel secondo no.\n#* Se hai ricevuto UI, mostra i nuovi campi all'utente e ottieni i dati che invia. Esegui un post a questo modulo con $1continue e i campi rilevanti settati, quindi ripeti il punto 4.\n#* Se hai ricevuto REDIRECT, dirigi l'utente a redirecttarget e aspetta che ritorni a $1returnurl. A quel punto esegui un post a questo modulo con $1continue e ogni campo passato all'URL di ritorno, e ripeti il punto 4.\n#* Se hai ricevuto RESTART, vuol dire che l'autenticazione ha funzionato ma non abbiamo un account collegato. Potresti considerare questo caso come UI o come FAIL.", + "api-help-authmanagerhelper-messageformat": "Formato da utilizzare per per la restituzione dei messaggi.", + "api-help-authmanagerhelper-preservestate": "Conserva lo stato da un precedente tentativo di accesso non riuscito, se possibile.", + "api-help-authmanagerhelper-returnurl": "URL di ritorno per i flussi di autenticazione di terze parti, deve essere assoluto. E' necessario fornirlo, oppure va fornito $1continue.\n\nAlla ricezione di una risposta REDIRECT, in genere si apre un browser o una vista web all'URL specificato redirecttarget per un flusso di autenticazione di terze parti. Quando questo è completato, la terza parte invierà il browser o la vista web a questo URL. Dovresti estrarre qualsiasi parametro POST o della richiesta dall'URL e passarli come un request $1continue a questo modulo API.", + "api-help-authmanagerhelper-continue": "Questa richiesta è una continuazione dopo una precedente risposta UI o REDIRECT. E' necessario fornire questo oppure $1returnurl.", + "api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1 (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.", "api-credits-header": "Crediti" } diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index a33dedfad6..a3a2db4953 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -19,8 +19,8 @@ "apihelp-main-param-action": "수행할 동작", "apihelp-main-param-format": "출력값의 형식.", "apihelp-main-param-maxlag": "최대 랙은 미디어위키가 데이터베이스 복제된 클러스터에 설치되었을 때 사용될 수 있습니다. 특정한 행동이 사이트 복제 랙을 유발할 때, 이 변수는 클라이언트가 복제 랙이 설정된 숫자 아래로 내려갈 때까지 기다리도록 지시합니다. 과도한 랙의 경우, maxlag 오류 코드와 Waiting for $host: $lag seconds lagged 메시지가 제공됩니다.
[[mw:Manual:Maxlag_parameter|매뉴얼: Maxlag 변수]] 에서 더 많은 정보를 얻을 수 있습니다.", - "apihelp-main-param-smaxage": "s-maxage HTTP 캐시 컨트롤 헤더를 설정합니다. 에러는 캐시되지 않습니다.", - "apihelp-main-param-maxage": "max-age HTTP 캐시 컨트롤 헤더를 설정합니다. 에러는 캐시되지 않습니다.", + "apihelp-main-param-smaxage": "s-maxage HTTP 캐시 컨트롤 헤더를 설정합니다. 오류는 캐시되지 않습니다.", + "apihelp-main-param-maxage": "max-age HTTP 캐시 컨트롤 헤더를 설정합니다. 오류는 캐시되지 않습니다.", "apihelp-main-param-assert": "user 플래그가 설정되어 있다면 로그인 여부를 체크하며, bot 플래그가 설정되어 있다면 봇 사용자 권한이 설정되어 있는지 확인합니다.", "apihelp-main-param-requestid": "주어진 요청 값은 응답에 포함됩니다. 요청을 구분하기 위해 사용될 수 있습니다.", "apihelp-main-param-servedby": "결과에 요청을 처리한 호스트네임을 포함합니다.", @@ -49,9 +49,9 @@ "apihelp-compare-param-fromtitle": "비교할 첫 이름.", "apihelp-compare-param-fromid": "비교할 첫 문서 ID.", "apihelp-compare-param-fromrev": "비교할 첫 판.", - "apihelp-compare-param-totitle": "비교할 두번째 제목.", - "apihelp-compare-param-toid": "비교할 두번째 문서 ID.", - "apihelp-compare-param-torev": "비교할 두번째 판.", + "apihelp-compare-param-totitle": "비교할 두 번째 제목.", + "apihelp-compare-param-toid": "비교할 두 번째 문서 ID.", + "apihelp-compare-param-torev": "비교할 두 번째 판.", "apihelp-compare-example-1": "판 1과 2의 차이를 생성합니다.", "apihelp-createaccount-description": "새 사용자 계정을 만듭니다.", "apihelp-createaccount-param-name": "사용자 이름", diff --git a/includes/api/i18n/oc.json b/includes/api/i18n/oc.json index ad54d42051..32e227d3e2 100644 --- a/includes/api/i18n/oc.json +++ b/includes/api/i18n/oc.json @@ -51,7 +51,7 @@ "apihelp-login-param-password": "Senhal.", "apihelp-login-param-domain": "Domeni (facultatiu).", "apihelp-login-example-login": "Se connectar.", - "apihelp-managetags-description": "Efectuar de prètzfaches de gestion relatius a la modificacion de las balisas.", + "apihelp-managetags-description": "Efectuar de prètzfaits de gestion relatius a la modificacion de las balisas.", "apihelp-move-description": "Desplaçar una pagina.", "apihelp-opensearch-param-search": "Cadena de recèrca.", "apihelp-parse-example-page": "Analisar una pagina.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index a9b9099d4e..2acc27be24 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -301,6 +301,8 @@ "apihelp-query+watchlist-paramvalue-prop-comment": "Dodaje komentarz do edycji.", "apihelp-query+watchlist-paramvalue-prop-timestamp": "Dodaje znacznik czasu edycji.", "apihelp-query+watchlist-paramvalue-prop-sizes": "Dodaje starą i nową długość strony.", + "apihelp-resetpassword-description": "Wyślij użytkownikowi e-mail do resetowania hasła.", + "apihelp-resetpassword-example-email": "Wyślij e-mail do resetowania hasła do wszystkich użytkowników posiadających adres user@example.com.", "apihelp-stashedit-param-title": "Tytuł edytowanej strony.", "apihelp-stashedit-param-sectiontitle": "Tytuł nowej sekcji.", "apihelp-stashedit-param-text": "Zawartość strony.", diff --git a/includes/api/i18n/ps.json b/includes/api/i18n/ps.json index 9f77e10078..89fa617adf 100644 --- a/includes/api/i18n/ps.json +++ b/includes/api/i18n/ps.json @@ -12,7 +12,7 @@ "apihelp-block-param-nocreate": "د گڼون جوړولو مخ نيول.", "apihelp-createaccount-param-name": "کارن-نوم.", "apihelp-delete-description": "يو مخ ړنگول.", - "apihelp-delete-example-simple": "Main Page ړنگول.", + "apihelp-delete-example-simple": "لومړی مخ ړنگول.", "apihelp-edit-description": "مخونه جوړول او سمول.", "apihelp-edit-param-sectiontitle": "د يوې نوې برخې سرليک.", "apihelp-edit-param-text": "مخ مېنځپانگه.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 6137457c1b..11efd46461 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -62,6 +62,7 @@ "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}", "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}", "apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}", + "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}", "apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}", "apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}", "apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}", @@ -150,6 +151,7 @@ "apihelp-feedcontributions-param-deletedonly": "{{doc-apihelp-param|feedcontributions|deletedonly}}", "apihelp-feedcontributions-param-toponly": "{{doc-apihelp-param|feedcontributions|toponly}}", "apihelp-feedcontributions-param-newonly": "{{doc-apihelp-param|feedcontributions|newonly}}", + "apihelp-feedcontributions-param-hideminor": "{{doc-apihelp-param|feedcontributions|hideminor}}", "apihelp-feedcontributions-param-showsizediff": "{{doc-apihelp-param|feedcontributions|showsizediff}}", "apihelp-feedcontributions-example-simple": "{{doc-apihelp-example|feedcontributions}}", "apihelp-feedrecentchanges-description": "{{doc-apihelp-description|feedrecentchanges}}", @@ -894,6 +896,7 @@ "apihelp-query+prefixsearch-param-limit": "{{doc-apihelp-param|query+prefixsearch|limit}}", "apihelp-query+prefixsearch-param-offset": "{{doc-apihelp-param|query+prefixsearch|offset}}", "apihelp-query+prefixsearch-example-simple": "{{doc-apihelp-example|query+prefixsearch}}", + "apihelp-query+prefixsearch-param-profile": "{{doc-apihelp-param|query+prefixsearch|profile|paramvalues=1}}", "apihelp-query+protectedtitles-description": "{{doc-apihelp-description|query+protectedtitles}}", "apihelp-query+protectedtitles-param-namespace": "{{doc-apihelp-param|query+protectedtitles|namespace}}", "apihelp-query+protectedtitles-param-level": "{{doc-apihelp-param|query+protectedtitles|level}}", @@ -1006,6 +1009,7 @@ "apihelp-query+search-param-what": "{{doc-apihelp-param|query+search|what}}", "apihelp-query+search-param-info": "{{doc-apihelp-param|query+search|info}}", "apihelp-query+search-param-prop": "{{doc-apihelp-param|query+search|prop|paramvalues=1}}", + "apihelp-query+search-param-qiprofile": "{{doc-apihelp-param|query+search|qiprofile|paramvalues=1}}", "apihelp-query+search-paramvalue-prop-size": "{{doc-apihelp-paramvalue|query+search|prop|size}}", "apihelp-query+search-paramvalue-prop-wordcount": "{{doc-apihelp-paramvalue|query+search|prop|wordcount}}", "apihelp-query+search-paramvalue-prop-timestamp": "{{doc-apihelp-paramvalue|query+search|prop|timestamp}}", diff --git a/includes/api/i18n/ta.json b/includes/api/i18n/ta.json index 04e9a43220..5626b70f1a 100644 --- a/includes/api/i18n/ta.json +++ b/includes/api/i18n/ta.json @@ -2,9 +2,13 @@ "@metadata": { "authors": [ "AntanO", - "கலைவாணன்" + "கலைவாணன்", + "Info-farmer" ] }, + "apihelp-main-param-action": "எச்செயலை செயற்படுத்த", + "apihelp-main-param-format": "பெற விரும்பும் கோப்பு வடிவம்", + "apihelp-main-param-requestid": "இங்கு கொடுக்கப்படும் மதிப்பானது, விளைவில் இணையும். கோரிக்கைகளை வேறுபடுத்தப் பயன்படலாம்.", "apihelp-import-param-namespace": "இதனைப் பெயர்வெளிக்கு இறக்குமதி செய்யவும். $1rootpage அளவுருவை மீறச்செய்யும்.", "apihelp-import-param-rootpage": "இப்பக்கத்தின் துணைப்பக்கமாக இறக்குமதி செய்யவும். $1namespace அளவுரு வழங்கப்பட்டிருந்தால் இது புறக்கணிக்கப்படும்.", "api-help-source": "மூலம்: $1", diff --git a/includes/api/i18n/tcy.json b/includes/api/i18n/tcy.json index d776a06a32..2f3d4c9e49 100644 --- a/includes/api/i18n/tcy.json +++ b/includes/api/i18n/tcy.json @@ -2,9 +2,20 @@ "@metadata": { "authors": [ "Bharathesha Alasandemajalu", - "Vishwanatha Badikana" + "Vishwanatha Badikana", + "VASANTH S.N." ] }, + "apihelp-createaccount-param-name": "ಸದಸ್ಯೆರ್ನ ಪುದರ್:", + "apihelp-delete-description": "ಪುಟೊಕುಲೆನ್ ಮಾಜಾಲೆ", + "apihelp-edit-param-minor": "ಎಲ್ಯೆಲ್ಯ ಬದಲಾವಣೆಲು", + "apihelp-edit-example-edit": "ಪುಟೊನ್ ಸಂಪಾದನೆ ಮಲ್ಪುಲೆ", + "apihelp-feedcontributions-param-year": "ಈ ಒರ್ಸೊರ್ದು(ಬೊಕ್ಕ ದುಂಬುದ):", + "apihelp-feedcontributions-param-month": "ಈ ತಿಂಗೊಲುರ್ದ್ (ಬೊಕ್ಕ ದುಂಬುದ):", + "apihelp-feedrecentchanges-example-simple": "ಇಂಚಿಪದ ಬದಲಾವಣೆಲೆನ್ ತೋಜಾಲೆ.", + "apihelp-login-param-name": "ಸದಸ್ಯೆರೆನ ಪುದರ್", + "apihelp-login-param-password": "ಪ್ರವೇಶ ಪದೊ", + "apihelp-login-example-login": "ಲಾಗಿನ್ ಆಲೆ", "apihelp-query+watchlist-param-type": "ವಾ ನಮೂನೆದ ಬದಲಾವಣೆ ತೊಜವೋಡು", "apihelp-query+watchlist-paramvalue-type-external": "ಪಿದಯೀದ ಬದಲಾವಣೇ", "apihelp-query+watchlist-paramvalue-type-new": "ಪುಟೊ ಉಂಡುಮಾನ್ಪುನಾ" diff --git a/includes/api/i18n/uk.json b/includes/api/i18n/uk.json index a525f2e7d0..0802c53c92 100644 --- a/includes/api/i18n/uk.json +++ b/includes/api/i18n/uk.json @@ -10,7 +10,8 @@ "Macofe", "Mix Gerder", "Piramidion", - "Andriykopanytsia" + "Andriykopanytsia", + "Максим Підліснюк" ] }, "apihelp-main-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 Оголошення 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:API:Errors_and_warnings|API: Errors and warnings]].\n\nТестування: Для зручності тестування запитів API, див. [[Special:ApiSandbox]].", @@ -39,6 +40,7 @@ "apihelp-block-param-watchuser": "Спостерігати за сторінкою користувача чи IP-адреси і сторінкою обговорення.", "apihelp-block-example-ip-simple": "Блокувати IP-адресу 192.0.2.5 на три дні з причиною First strike.", "apihelp-block-example-user-complex": "Блокувати користувачаVandal на невизначений термін з причиною Vandalism і заборонити створення нових облікових записів та надсилання електронної пошти.", + "apihelp-changeauthenticationdata-description": "Зміна параметрів аутентифікації для поточного користувача.", "apihelp-checktoken-description": "Перевірити коректність токена з [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].", "apihelp-checktoken-param-type": "Тип токена, який тестується.", "apihelp-checktoken-param-token": "Токен для тесту.", @@ -1032,6 +1034,7 @@ "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": "Видати лише локальні або лише нелокальні елементи карти інтервікі.", "apihelp-query+siteinfo-param-showalldb": "Перелічити усі сервери баз даних, а не лише той, який робить найбільшу затримку.", "apihelp-query+siteinfo-param-numberingroup": "Перераховує кількість користувачів у групах користувачів.", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 71daba7949..46e5c85798 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -47,6 +47,8 @@ "apihelp-block-param-watchuser": "监视用户或该 IP 的用户页和讨论页。", "apihelp-block-example-ip-simple": "封禁IP地址192.0.2.5三天,原因First strike。", "apihelp-block-example-user-complex": "无限期封禁用户Vandal,原因Vandalism,并阻止新账户创建和电子邮件发送。", + "apihelp-changeauthenticationdata-description": "更改当前用户的身份验证数据。", + "apihelp-changeauthenticationdata-example-password": "尝试更改当前用户的密码至ExamplePassword。", "apihelp-checktoken-description": "从[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]检查令牌有效性。", "apihelp-checktoken-param-type": "已开始测试的令牌类型。", "apihelp-checktoken-param-token": "要测试的令牌。", @@ -54,6 +56,9 @@ "apihelp-checktoken-example-simple": "测试csrf令牌的有效性。", "apihelp-clearhasmsg-description": "清除当前用户的hasmsg标记。", "apihelp-clearhasmsg-example-1": "清除当前用户的hasmsg标记。", + "apihelp-clientlogin-description": "使用交互式流登录wiki。", + "apihelp-clientlogin-example-login": "开始作为用户Example和密码ExamplePassword登录至wiki的过程。", + "apihelp-clientlogin-example-login2": "在UI响应双因素验证后继续登录,补充OATHToken 987654。", "apihelp-compare-description": "获取2个页面之间的差别。\n\n用于“from”和“to”的修订版本号、页面标题或页面 ID 必须获得通过。", "apihelp-compare-param-fromtitle": "要比较的第一个标题。", "apihelp-compare-param-fromid": "要比较的第一个页面 ID。", @@ -63,6 +68,8 @@ "apihelp-compare-param-torev": "要比较的第二个修订版本。", "apihelp-compare-example-1": "在版本1和2中创建差异。", "apihelp-createaccount-description": "创建一个新用户账户。", + "apihelp-createaccount-param-preservestate": "如果[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]返回用于hasprimarypreservedstate的真值,标记为primary-required的请求应被忽略。如果它返回用于preservedusername的非空值,用户名必须用于username参数。", + "apihelp-createaccount-example-create": "开始创建用户Example和密码ExamplePassword的过程。", "apihelp-createaccount-param-name": "用户名。", "apihelp-createaccount-param-password": "密码(如果设置$1mailpassword则忽略)。", "apihelp-createaccount-param-domain": "外部身份验证域 (可选)。", @@ -87,7 +94,7 @@ "apihelp-delete-example-reason": "删除Main Page,原因Preparing for move。", "apihelp-disabled-description": "此模块已禁用。", "apihelp-edit-description": "创建和编辑页面。", - "apihelp-edit-param-title": "您希望编辑的页面标题。不能与$1pageid一起使用。", + "apihelp-edit-param-title": "要编辑的页面标题。不能与$1pageid一起使用。", "apihelp-edit-param-pageid": "要编辑的页面的页面 ID。不能与$1title一起使用。", "apihelp-edit-param-section": "段落数。0用于首段,new用于新的段落。", "apihelp-edit-param-sectiontitle": "新段落的标题。", @@ -211,7 +218,11 @@ "apihelp-import-param-namespace": "导入至此名字空间。不能与$1rootpage一起使用。", "apihelp-import-param-rootpage": "作为此页面的子页面导入。不能与$1namespace一起使用。", "apihelp-import-example-import": "将页面[[meta:Help:ParserFunctions]]连带完整历史导入至100名字空间。", - "apihelp-login-description": "登录并获得身份验证Cookie。\n\n在成功登录的情况下,所需的Cookie将包含在HTTP响应头中。在登录失败的情况下,进一步的尝试可能会被自动密码猜解攻击的限制所遏制。", + "apihelp-linkaccount-description": "将来自第三方提供商的账户链接至当前用户。", + "apihelp-linkaccount-example-link": "开始从Example链接至账户的过程。", + "apihelp-login-description": "登录并获取身份验证Cookie。\n\n此操作只应与[[Special:BotPasswords]]一起使用;用于主账户登录的方式已弃用,并可能在没有警告的情况下失败。要安全登录主账户,请使用[[Special:ApiHelp/clientlogin|action=clientlogin]]。", + "apihelp-login-description-nobotpasswords": "登录并获取身份验证Cookie。\n\n此操作已弃用,并可能在没有警告的情况下失败。要安全登录,请使用[[Special:ApiHelp/clientlogin|action=clientlogin]]。", + "apihelp-login-description-nonauthmanager": "登录并获取身份验证Cookie。\n\n在成功登录的情况下,所需的Cookie将包含在HTTP响应头中。在登录失败的情况下,进一步的尝试可能会被自动密码猜解攻击的限制所遏制。", "apihelp-login-param-name": "用户名。", "apihelp-login-param-password": "密码。", "apihelp-login-param-domain": "域名(可选)。", @@ -251,7 +262,7 @@ "apihelp-move-param-watchlist": "无条件地将页面加入至当前用户的监视列表或将其移除,使用设置或不更改监视。", "apihelp-move-param-ignorewarnings": "忽略任何警告。", "apihelp-move-example-move": "移动Badtitle到Goodtitle,不保留重定向。", - "apihelp-opensearch-description": "使用OpenSearch协议搜索本wiki。", + "apihelp-opensearch-description": "使用OpenSearch协议搜索wiki。", "apihelp-opensearch-param-search": "搜索字符串。", "apihelp-opensearch-param-limit": "要返回的结果最大数。", "apihelp-opensearch-param-namespace": "搜索的名字空间。", @@ -261,7 +272,7 @@ "apihelp-opensearch-param-warningsaserror": "如果警告通过format=json提升,返回一个API错误而不是忽略它们。", "apihelp-opensearch-example-te": "查找以Te开头的页面。", "apihelp-options-description": "更改当前用户的偏好设置。\n\n只有注册在核心或者已安装扩展中的选项,或者具有userjs-键值前缀(旨在被用户脚本使用)的选项可被设置。", - "apihelp-options-param-reset": "重置偏好设置到网站默认设置。", + "apihelp-options-param-reset": "将参数设置重置为网站默认值。", "apihelp-options-param-resetkinds": "当$1reset选项被设置时,要重置的选项类型列表。", "apihelp-options-param-change": "更改列表,以name=value格式化(例如skin=vector)。值不能包含管道字符。如果没提供值(甚至没有等号),例如optionname|otheroption|...,选项将重置为默认值。", "apihelp-options-param-optionname": "应设置为由$1optionvalue提供值的选项名称。", @@ -280,7 +291,7 @@ "apihelp-parse-description": "解析内容并返回解析器输出。\n\n参见[[Special:ApiHelp/query|action=query]]的各种prop-module以从页面的当前版本获得信息。\n\n这里有几种方法可以指定解析的文本:\n# 指定一个页面或修订,使用$1page、$1pageid或$1oldid。\n# 明确指定内容,使用$1text、$1title和$1contentmodel。\n# 只指定一段摘要解析。$1prop应提供一个空值。", "apihelp-parse-param-title": "文本属于的页面标题。如果省略,$1contentmodel就必须被指定,且[[API]]将作为标题使用。", "apihelp-parse-param-text": "要解析的文本。使用$1title或$1contentmodel以控制内容模型。", - "apihelp-parse-param-summary": "所要解析的摘要。", + "apihelp-parse-param-summary": "要解析的摘要。", "apihelp-parse-param-page": "解析此页的内容。不能与$1text和$1title一起使用。", "apihelp-parse-param-pageid": "解析此页的内容。覆盖$1page。", "apihelp-parse-param-redirects": "如果$1page或$1pageid被设置为一个重定向,则解析它。", @@ -543,6 +554,10 @@ "apihelp-query+allusers-param-activeusers": "只列出最近$1{{PLURAL:$1|天}}内活跃的用户。", "apihelp-query+allusers-param-attachedwiki": "与$1prop=centralids一起使用,也表明用户是否附加于此ID定义的wiki。", "apihelp-query+allusers-example-Y": "列出以Y开头的用户。", + "apihelp-query+authmanagerinfo-description": "检索有关当前身份验证状态的信息。", + "apihelp-query+filerepoinfo-example-login": "检索当开始登录时可能使用的请求。", + "apihelp-query+filerepoinfo-example-login-merged": "检索当开始登录时可能使用的请求,并合并表单字段。", + "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "测试身份验证对操作foo是否足够。", "apihelp-query+backlinks-description": "查找所有链接至指定页面的页面。", "apihelp-query+backlinks-param-title": "要搜索的标题。不能与$1pageid一起使用。", "apihelp-query+backlinks-param-pageid": "要搜索的页面ID。不能与$1title一起使用。", @@ -884,6 +899,7 @@ "apihelp-query+prefixsearch-param-limit": "要返回的结果最大数。", "apihelp-query+prefixsearch-param-offset": "跳过的结果数。", "apihelp-query+prefixsearch-example-simple": "搜索以meaning开头的页面标题。", + "apihelp-query+prefixsearch-param-profile": "搜索要使用的配置文件。", "apihelp-query+protectedtitles-description": "列出所有被限制创建的标题。", "apihelp-query+protectedtitles-param-namespace": "只列出这些名字空间的标题。", "apihelp-query+protectedtitles-param-level": "只列出带这些保护级别的标题。", @@ -996,19 +1012,20 @@ "apihelp-query+search-param-what": "要执行的搜索类型。", "apihelp-query+search-param-info": "要返回的元数据。", "apihelp-query+search-param-prop": "要返回的属性:", + "apihelp-query+search-param-qiprofile": "查询要使用的独立描述(影响排序算法)。", "apihelp-query+search-paramvalue-prop-size": "添加页面大小,单位为字节。", "apihelp-query+search-paramvalue-prop-wordcount": "添加页面的字数。", "apihelp-query+search-paramvalue-prop-timestamp": "添加页面上次编辑时的时间戳。", "apihelp-query+search-paramvalue-prop-snippet": "Adds a parsed snippet of the page.", "apihelp-query+search-paramvalue-prop-titlesnippet": "Adds a parsed snippet of the page title.", - "apihelp-query+search-paramvalue-prop-redirectsnippet": "Adds a parsed snippet of the redirect title.", + "apihelp-query+search-paramvalue-prop-redirectsnippet": "添加被解析的重定向标题的片段。", "apihelp-query+search-paramvalue-prop-redirecttitle": "Adds the title of the matching redirect.", "apihelp-query+search-paramvalue-prop-sectionsnippet": "Adds a parsed snippet of the matching section title.", "apihelp-query+search-paramvalue-prop-sectiontitle": "Adds the title of the matching section.", "apihelp-query+search-paramvalue-prop-categorysnippet": "Adds a parsed snippet of the matching category.", "apihelp-query+search-paramvalue-prop-isfilematch": "Adds a boolean indicating if the search matched file content.", "apihelp-query+search-paramvalue-prop-score": "已弃用并已忽略。", - "apihelp-query+search-paramvalue-prop-hasrelated": "Deprecated and ignored.", + "apihelp-query+search-paramvalue-prop-hasrelated": "已弃用并已忽略。", "apihelp-query+search-param-limit": "返回的总计页面数。", "apihelp-query+search-param-interwiki": "搜索结果中包含跨wiki结果,如果可用。", "apihelp-query+search-param-backend": "要使用的搜索后端,如果没有则为默认。", @@ -1040,6 +1057,7 @@ "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地图中的本地或非本地记录。", "apihelp-query+siteinfo-param-showalldb": "列出所有数据库服务器,不只是最落后的那个。", "apihelp-query+siteinfo-param-numberingroup": "列出用户组中的用户数。", @@ -1192,6 +1210,15 @@ "apihelp-query+watchlistraw-param-totitle": "要列举的最终标题(带名字空间前缀)。", "apihelp-query+watchlistraw-example-simple": "列出当前用户的监视列表中的页面。", "apihelp-query+watchlistraw-example-generator": "检索当前用户监视列表上的页面的页面信息。", + "apihelp-removeauthenticationdata-description": "从当前用户移除身份验证数据。", + "apihelp-removeauthenticationdata-example-simple": "尝试移除当前用户的FooAuthenticationRequest数据。", + "apihelp-resetpassword-description": "向用户发送密码重置邮件。", + "apihelp-resetpassword-description-noroutes": "没有密码重置路由可用。\n\n在[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]中启用路由以使用此模块。", + "apihelp-resetpassword-param-user": "正在重置的用户。", + "apihelp-resetpassword-param-email": "正在重置用户的电子邮件地址。", + "apihelp-resetpassword-param-capture": "返回已发送的临时密码。需要passwordreset用户权限。", + "apihelp-resetpassword-example-user": "向用户Example发送密码重置邮件。", + "apihelp-resetpassword-example-email": "向所有电子邮件地址为user@example.com的用户发送密码重置邮件。", "apihelp-revisiondelete-description": "删除和恢复修订版本。", "apihelp-revisiondelete-param-type": "正在执行的修订版本删除类型。", "apihelp-revisiondelete-param-target": "要进行修订版本删除的页面标题,如果对某一类型需要。", @@ -1260,6 +1287,8 @@ "apihelp-undelete-param-watchlist": "无条件地将页面加入至当前用户的监视列表或将其移除,使用设置或不更改监视。", "apihelp-undelete-example-page": "恢复页面Main Page。", "apihelp-undelete-example-revisions": "恢复Main Page的两个修订。", + "apihelp-unlinkaccount-description": "从当前用户移除已连接的第三方账户。", + "apihelp-unlinkaccount-example-simple": "尝试移除当前用户的,与FooAuthenticationRequest相关联提供方的链接。", "apihelp-upload-description": "上传一个文件,或获取正在等待中的上传的状态。\n\n可以使用的几种方法:\n* 直接上传文件内容,使用$1file参数。\n* 成批上传文件,使用$1filesize、$1chunk和$1offset参数。\n* 有MediaWiki服务器从URL检索一个文件,使用$1url参数。\n* 完成一次由于警告而失败的早前上传,使用$1filekey参数。\n需要注意,当发送$1file时,HTTP POST必须做为一次文件上传(也就是使用multipart/form-data)完成。", "apihelp-upload-param-filename": "目标文件名。", "apihelp-upload-param-comment": "上传注释。如果没有指定$1text,那么它也被用于新文件的初始页面文本。", @@ -1369,6 +1398,11 @@ "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# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying $1returnurl and any relevant fields.\n# Check the status in the response.\n#* If you received PASS or FAIL, you're done. The operation either succeeded or it didn't.\n#* If you received UI, present the new fields to the user and obtain their submission. Then post to this module with $1continue and the relevant fields set, and repeat step 4.\n#* If you received REDIRECT, direct the user to the redirecttarget and wait for the return to $1returnurl. Then post to this module with $1continue and any fields passed to the return URL, and repeat step 4.\n#* If you received RESTART, that means the authentication worked but we don't have a linked user account. You might treat this as UI or as FAIL.", + "api-help-authmanagerhelper-request": "使用此身份验证请求,通过返回自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]的id与amirequestsfor=$1。", + "api-help-authmanagerhelper-messageformat": "返回消息使用的格式。", + "api-help-authmanagerhelper-mergerequestfields": "合并用于所有身份验证请求的字段信息至一个数组中。", + "api-help-authmanagerhelper-additional-params": "此模块允许额外参数,取决于可用的身份验证请求。使用[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]与amirequestsfor=$1(或之前来自此模块的相应,如果可以)以决定可用请求及其使用的字段。", "api-credits-header": "制作人员", "api-credits": "API 开发人员:\n* Yuri Astrakhan(创建者,2006å¹´9月~2007å¹´9月的开发组领导)\n* Roan Kattouw(2007å¹´9月~2009年的开发组领导)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch(2013年至今的开发组领导)\n\n请将您的评论、建议和问题发送至mediawiki-api@lists.wikimedia.org,或提交错误请求至https://phabricator.wikimedia.org/。" } diff --git a/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php b/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php index 900d2e5c8e..f5bfc2a20b 100644 --- a/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php +++ b/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php @@ -34,7 +34,7 @@ abstract class AbstractPasswordPrimaryAuthenticationProvider extends AbstractPrimaryAuthenticationProvider { /** @var bool Whether this provider should ABSTAIN (false) or FAIL (true) on password failure */ - protected $authoritative = true; + protected $authoritative; private $passwordFactory = null; diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index efee53c6dc..2ed0d618c7 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -231,6 +231,17 @@ class AuthManager implements LoggerAwareInterface { /** * Start an authentication flow + * + * In addition to the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might include a + * CreateFromLoginAuthenticationRequest from a previous login attempt to + * preserve state. + * + * Instead of the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might pass a + * CreatedAccountAuthenticationRequest from an account creation that just + * succeeded to log in to the just-created account. + * * @param AuthenticationRequest[] $reqs * @param string $returnToUrl Url that REDIRECT responses should eventually * return to. @@ -344,8 +355,7 @@ class AuthManager implements LoggerAwareInterface { * Return values are interpreted as follows: * - status FAIL: Authentication failed. If $response->createRequest is * set, that may be passed to self::beginAuthentication() or to - * self::beginAccountCreation() (after adding a username, if necessary) - * to preserve state. + * self::beginAccountCreation() to preserve state. * - status REDIRECT: The client should be redirected to the contained URL, * new AuthenticationRequests should be made (if any), then * AuthManager::continueAuthentication() should be called. @@ -432,6 +442,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Primary login with $id returned $res->status" ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); @@ -494,6 +505,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Primary login with $id returned $res->status" ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); return $res; @@ -546,6 +558,7 @@ class AuthManager implements LoggerAwareInterface { ); $ret->neededRequests[] = $ret->createRequest; } + $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true ); $session->setSecret( 'AuthManager::authnState', [ 'reqs' => [], // Will be filled in later 'primary' => null, @@ -615,6 +628,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Secondary login with $id returned " . $res->status ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() ); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); @@ -950,6 +964,17 @@ class AuthManager implements LoggerAwareInterface { /** * Start an account creation flow + * + * In addition to the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might include a + * CreateFromLoginAuthenticationRequest from a previous login attempt. If + * + * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE ) + * + * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests + * should be omitted. If the CreateFromLoginAuthenticationRequest has a + * username set, that username must be used for all other requests. + * * @param User $creator User doing the account creation * @param AuthenticationRequest[] $reqs * @param string $returnToUrl Url that REDIRECT responses should eventually @@ -1038,44 +1063,10 @@ class AuthManager implements LoggerAwareInterface { if ( $req ) { $state['maybeLink'] = $req->maybeLink; - // If we get here, the user didn't submit a form with any of the - // usual AuthenticationRequests that are needed for an account - // creation. So we need to determine if there are any and return a - // UI response if so. if ( $req->createRequest ) { - // We have a createRequest from a - // PrimaryAuthenticationProvider, so don't ask. - $providers = $this->getPreAuthenticationProviders() + - $this->getSecondaryAuthenticationProviders(); - } else { - // We're only preserving maybeLink, so ask for primary fields - // too. - $providers = $this->getPreAuthenticationProviders() + - $this->getPrimaryAuthenticationProviders() + - $this->getSecondaryAuthenticationProviders(); - } - $reqs = $this->getAuthenticationRequestsInternal( - self::ACTION_CREATE, - [], - $providers - ); - // See if we need any requests to begin - foreach ( (array)$reqs as $r ) { - if ( !$r instanceof UsernameAuthenticationRequest && - !$r instanceof UserDataAuthenticationRequest && - !$r instanceof CreationReasonAuthenticationRequest - ) { - // Needs some reqs, so request them - $reqs[] = new CreateFromLoginAuthenticationRequest( $req->createRequest, [] ); - $state['continueRequests'] = $reqs; - $session->setSecret( 'AuthManager::accountCreationState', $state ); - $session->persist(); - return AuthenticationResponse::newUI( $reqs, wfMessage( 'authmanager-create-from-login' ) ); - } + $reqs[] = $req->createRequest; + $state['reqs'][] = $req->createRequest; } - // No reqs needed, so we can just continue. - $req->createRequest->returnToUrl = $returnToUrl; - $reqs = [ $req->createRequest ]; } $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1213,15 +1204,6 @@ class AuthManager implements LoggerAwareInterface { $req->username = $state['username']; } - // If we're coming in from a create-from-login UI response, we need - // to extract the createRequest (if any). - $req = AuthenticationRequest::getRequestByClass( - $reqs, CreateFromLoginAuthenticationRequest::class - ); - if ( $req && $req->createRequest ) { - $reqs[] = $req->createRequest; - } - // Run pre-creation tests, if we haven't already if ( !$state['ranPreTests'] ) { $providers = $this->getPreAuthenticationProviders() + @@ -1281,6 +1263,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1343,6 +1326,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); return $res; @@ -1438,6 +1422,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1806,6 +1791,7 @@ class AuthManager implements LoggerAwareInterface { $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [ 'user' => $user->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountLinkState', $state ); @@ -1908,6 +1894,7 @@ class AuthManager implements LoggerAwareInterface { $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [ 'user' => $user->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountLinkState', $state ); return $res; @@ -2069,12 +2056,7 @@ class AuthManager implements LoggerAwareInterface { } // Fill in reqs data - foreach ( $reqs as $req ) { - $req->action = $providerAction; - if ( $req->username === null ) { - $req->username = $options['username']; - } - } + $this->fillRequests( $reqs, $providerAction, $options['username'], true ); // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) { @@ -2086,6 +2068,24 @@ class AuthManager implements LoggerAwareInterface { return array_values( $reqs ); } + /** + * Set values in an array of requests + * @param AuthenticationRequest[] &$reqs + * @param string $action + * @param string|null $username + * @param boolean $forceAction + */ + private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) { + foreach ( $reqs as $req ) { + if ( !$req->action || $forceAction ) { + $req->action = $action; + } + if ( $req->username === null ) { + $req->username = $username; + } + } + } + /** * Determine whether a username exists * @param string $username @@ -2124,6 +2124,37 @@ class AuthManager implements LoggerAwareInterface { return true; } + /** + * Get a provider by ID + * @note This is public so extensions can check whether their own provider + * is installed and so they can read its configuration if necessary. + * Other uses are not recommended. + * @param string $id + * @return AuthenticationProvider|null + */ + public function getAuthenticationProvider( $id ) { + // Fast version + if ( isset( $this->allAuthenticationProviders[$id] ) ) { + return $this->allAuthenticationProviders[$id]; + } + + // Slow version: instantiate each kind and check + $providers = $this->getPrimaryAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + $providers = $this->getSecondaryAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + $providers = $this->getPreAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + + return null; + } + /**@}*/ /** @@ -2273,34 +2304,6 @@ class AuthManager implements LoggerAwareInterface { return $this->secondaryAuthenticationProviders; } - /** - * Get a provider by ID - * @param string $id - * @return AuthenticationProvider|null - */ - protected function getAuthenticationProvider( $id ) { - // Fast version - if ( isset( $this->allAuthenticationProviders[$id] ) ) { - return $this->allAuthenticationProviders[$id]; - } - - // Slow version: instantiate each kind and check - $providers = $this->getPrimaryAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - $providers = $this->getSecondaryAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - $providers = $this->getPreAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - - return null; - } - /** * @param User $user * @param bool|null $remember @@ -2310,6 +2313,7 @@ class AuthManager implements LoggerAwareInterface { $delay = $session->delaySave(); $session->resetId(); + $session->resetAllTokens(); if ( $session->canSetUser() ) { $session->setUser( $user ); } @@ -2332,7 +2336,7 @@ class AuthManager implements LoggerAwareInterface { private function setDefaultUserOptions( User $user, $useContextLang ) { global $wgContLang; - \MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $user ); + $user->setToken(); $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang; $user->setOption( 'language', $lang->getPreferredVariant() ); diff --git a/includes/auth/AuthManagerAuthPlugin.php b/includes/auth/AuthManagerAuthPlugin.php index bf1e0215bc..8d85b4411d 100644 --- a/includes/auth/AuthManagerAuthPlugin.php +++ b/includes/auth/AuthManagerAuthPlugin.php @@ -131,7 +131,7 @@ class AuthManagerAuthPlugin extends \AuthPlugin { $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data ); foreach ( $reqs as $req ) { $status = AuthManager::singleton()->allowsAuthenticationDataChange( $req ); - if ( !$status->isOk() ) { + if ( !$status->isGood() ) { $this->logger->info( __METHOD__ . ': Password change rejected: {reason}', [ 'username' => $data['username'], 'reason' => $status->getWikiText( null, null, 'en' ), diff --git a/includes/auth/AuthPluginPrimaryAuthenticationProvider.php b/includes/auth/AuthPluginPrimaryAuthenticationProvider.php index 9746637b00..b8e36bc4f3 100644 --- a/includes/auth/AuthPluginPrimaryAuthenticationProvider.php +++ b/includes/auth/AuthPluginPrimaryAuthenticationProvider.php @@ -329,7 +329,7 @@ class AuthPluginPrimaryAuthenticationProvider if ( $req->domain === null ) { return \StatusValue::newGood( 'ignored' ); } - if ( !$this->auth->validDomain( $domain ) ) { + if ( !$this->auth->validDomain( $req->domain ) ) { return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' ); } } diff --git a/includes/auth/AuthenticationRequest.php b/includes/auth/AuthenticationRequest.php index 3c19b87f17..ff4d52ed92 100644 --- a/includes/auth/AuthenticationRequest.php +++ b/includes/auth/AuthenticationRequest.php @@ -92,14 +92,12 @@ abstract class AuthenticationRequest { * - select: * - multiselect: More a grid of checkboxes than if 'image' is set, otherwise - * (uses 'label' as button text) + * - button: (uses 'label' as button text) * - hidden: Not visible to the user, but needs to be preserved for the next request * - null: No widget, just display the 'label' message. * - options: (array) Maps option values to Messages for the * 'select' and 'multiselect' types. * - value: (string) Value (for 'null' and 'hidden') or default value (for other types). - * - image: (string) URL of an image to use in connection with the input * - label: (Message) Text suitable for a label in an HTML form * - help: (Message) Text suitable as a description of what the field is * - optional: (bool) If set and truthy, the field may be left empty @@ -281,6 +279,21 @@ abstract class AuthenticationRequest { public static function mergeFieldInfo( array $reqs ) { $merged = []; + // fields that are required by some primary providers but not others are not actually required + $primaryRequests = array_filter( $reqs, function ( $req ) { + return $req->required === AuthenticationRequest::PRIMARY_REQUIRED; + } ); + $sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) { + $required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) { + return empty( $options['optional'] ); + } ) ); + if ( $shared === null ) { + return $required; + } else { + return array_intersect( $shared, $required ); + } + }, null ); + foreach ( $reqs as $req ) { $info = $req->getFieldInfo(); if ( !$info ) { @@ -288,8 +301,14 @@ abstract class AuthenticationRequest { } foreach ( $info as $name => $options ) { - if ( $req->required !== self::REQUIRED ) { + if ( // If the request isn't required, its fields aren't required either. + $req->required === self::OPTIONAL + // If there is a primary not requiring this field, no matter how many others do, + // authentication can proceed without it. + || $req->required === self::PRIMARY_REQUIRED + && !in_array( $name, $sharedRequiredPrimaryFields, true ) + ) { $options['optional'] = true; } else { $options['optional'] = !empty( $options['optional'] ); diff --git a/includes/auth/AuthenticationResponse.php b/includes/auth/AuthenticationResponse.php index db0182552d..5048cf84dd 100644 --- a/includes/auth/AuthenticationResponse.php +++ b/includes/auth/AuthenticationResponse.php @@ -83,13 +83,14 @@ class AuthenticationResponse { /** * @var AuthenticationRequest|null * - * Returned with a PrimaryAuthenticationProvider login FAIL, this holds a - * request that should result in a PASS when passed to that provider's - * PrimaryAuthenticationProvider::beginPrimaryAccountCreation(). + * Returned with a PrimaryAuthenticationProvider login FAIL or a PASS with + * no username, this holds a request that should result in a PASS when + * passed to that provider's PrimaryAuthenticationProvider::beginPrimaryAccountCreation(). * - * Returned with an AuthManager login FAIL or RESTART, this holds a request - * that may be passed to AuthManager::beginCreateAccount() after setting - * its ->returnToUrl property. It may also be passed to + * Returned with an AuthManager login FAIL or RESTART, this holds a + * CreateFromLoginAuthenticationRequest that may be passed to + * AuthManager::beginCreateAccount(), possibly in place of any + * "primary-required" requests. It may also be passed to * AuthManager::beginAuthentication() to preserve state. */ public $createRequest = null; diff --git a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php index 180aaae34e..32c8fd55de 100644 --- a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php +++ b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php @@ -50,7 +50,14 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen if ( !is_array( $state ) ) { return AuthenticationResponse::newAbstain(); } - $maybeLink = $state['maybeLink']; + + $maybeLink = array_filter( $state['maybeLink'], function ( $req ) use ( $user ) { + if ( !$req->action ) { + $req->action = AuthManager::ACTION_CHANGE; + } + $req->username = $user->getName(); + return $this->manager->allowsAuthenticationDataChange( $req )->isGood(); + } ); if ( !$maybeLink ) { return AuthenticationResponse::newAbstain(); } @@ -134,7 +141,7 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen $combinedStatus->error( wfMessage( 'authprovider-confirmlink-success-line', $description ) ); } else { $combinedStatus->error( wfMessage( - 'authprovider-confirmlink-failure-line', $description, $status->getMessage()->text() + 'authprovider-confirmlink-failed-line', $description, $status->getMessage()->text() ) ); } } diff --git a/includes/auth/CreateFromLoginAuthenticationRequest.php b/includes/auth/CreateFromLoginAuthenticationRequest.php index 949302d8bc..ddeb13d9d6 100644 --- a/includes/auth/CreateFromLoginAuthenticationRequest.php +++ b/includes/auth/CreateFromLoginAuthenticationRequest.php @@ -25,7 +25,8 @@ namespace MediaWiki\Auth; * This transfers state between the login and account creation flows. * * AuthManager::getAuthenticationRequests() won't return this type, but it - * may be passed to AuthManager::beginAccountCreation() anyway. + * may be passed to AuthManager::beginAuthentication() or + * AuthManager::beginAccountCreation() anyway. * * @ingroup Auth * @since 1.27 @@ -50,6 +51,7 @@ class CreateFromLoginAuthenticationRequest extends AuthenticationRequest { ) { $this->createRequest = $createRequest; $this->maybeLink = $maybeLink; + $this->username = $createRequest ? $createRequest->username : null; } public function getFieldInfo() { @@ -59,4 +61,36 @@ class CreateFromLoginAuthenticationRequest extends AuthenticationRequest { public function loadFromSubmission( array $data ) { return true; } + + /** + * Indicate whether this request contains any state for the specified + * action. + * @param string $action One of the AuthManager::ACTION_* constants + * @return boolean + */ + public function hasStateForAction( $action ) { + switch ( $action ) { + case AuthManager::ACTION_LOGIN: + return (bool)$this->maybeLink; + case AuthManager::ACTION_CREATE: + return $this->maybeLink || $this->createRequest; + default: + return false; + } + } + + /** + * Indicate whether this request contains state for the specified + * action sufficient to replace other primary-required requests. + * @param string $action One of the AuthManager::ACTION_* constants + * @return boolean + */ + public function hasPrimaryStateForAction( $action ) { + switch ( $action ) { + case AuthManager::ACTION_CREATE: + return (bool)$this->createRequest; + default: + return false; + } + } } diff --git a/includes/auth/CreationReasonAuthenticationRequest.php b/includes/auth/CreationReasonAuthenticationRequest.php index 1711aec974..146470ed8f 100644 --- a/includes/auth/CreationReasonAuthenticationRequest.php +++ b/includes/auth/CreationReasonAuthenticationRequest.php @@ -10,6 +10,8 @@ class CreationReasonAuthenticationRequest extends AuthenticationRequest { /** @var string Account creation reason (only used when creating for someone else) */ public $reason; + public $required = self::OPTIONAL; + public function getFieldInfo() { return [ 'reason' => [ diff --git a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php index 2e51cf22c1..dd97830dae 100644 --- a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php +++ b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php @@ -95,12 +95,12 @@ class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuth } } - if ( isset( $data->req ) ) { - $needReq = $data->req; - } else { - $needReq = new PasswordAuthenticationRequest(); + $needReq = isset( $data->req ) ? $data->req : new PasswordAuthenticationRequest(); + if ( !$needReq->action ) { $needReq->action = AuthManager::ACTION_CHANGE; } + $needReq->required = $data->hard ? AuthenticationRequest::REQUIRED + : AuthenticationRequest::OPTIONAL; $needReqs = [ $needReq ]; if ( !$data->hard ) { $needReqs[] = new ButtonAuthenticationRequest( diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php index 2d29d86513..a59ba97d0a 100644 --- a/includes/cache/CacheDependency.php +++ b/includes/cache/CacheDependency.php @@ -20,6 +20,7 @@ * @file * @ingroup Cache */ +use MediaWiki\MediaWikiServices; /** * This class stores an arbitrary value along with its dependencies. @@ -244,6 +245,34 @@ class GlobalDependency extends CacheDependency { } } +/** + * @ingroup Cache + */ +class MainConfigDependency extends CacheDependency { + private $name; + private $value; + + function __construct( $name ) { + $this->name = $name; + $this->value = $this->getConfig()->get( $this->name ); + } + + private function getConfig() { + return MediaWikiServices::getInstance()->getMainConfig(); + } + + /** + * @return bool + */ + function isExpired() { + if ( !$this->getConfig()->has( $this->name ) ) { + return true; + } + + return $this->getConfig()->get( $this->name ) != $this->value; + } +} + /** * @ingroup Cache */ diff --git a/includes/cache/HTMLFileCache.php b/includes/cache/HTMLFileCache.php index cdf00c7a58..bb78aa0dbd 100644 --- a/includes/cache/HTMLFileCache.php +++ b/includes/cache/HTMLFileCache.php @@ -124,11 +124,10 @@ class HTMLFileCache extends FileCacheBase { $user = $context->getUser(); // Check for non-standard user language; this covers uselang, // and extensions for auto-detecting user language. - $ulang = $context->getLanguage()->getCode(); - $clang = $wgContLang->getCode(); + $ulang = $context->getLanguage(); // Check that there are no other sources of variation - if ( $user->getId() || $user->getNewtalk() || $ulang != $clang ) { + if ( $user->getId() || $user->getNewtalk() || $ulang->equals( $wgContLang ) ) { return false; } // Allow extensions to disable caching diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index a7dd5709c2..ec37dd61c3 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -21,6 +21,7 @@ * @ingroup Cache */ use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; /** * Class representing a list of titles @@ -72,8 +73,8 @@ class LinkBatch { * @param string $dbkey */ public function add( $ns, $dbkey ) { - if ( $ns < 0 ) { - return; + if ( $ns < 0 || $dbkey === '' ) { + return; // T137083 } if ( !array_key_exists( $ns, $this->data ) ) { $this->data[$ns] = []; @@ -116,7 +117,7 @@ class LinkBatch { * @return array Mapping PDBK to ID */ public function execute() { - $linkCache = LinkCache::singleton(); + $linkCache = MediaWikiServices::getInstance()->getLinkCache(); return $this->executeInto( $linkCache ); } @@ -151,23 +152,26 @@ class LinkBatch { return []; } + $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter(); // For each returned entry, add it to the list of good links, and remove it from $remaining $ids = []; $remaining = $this->data; foreach ( $res as $row ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $title = new TitleValue( (int)$row->page_namespace, $row->page_title ); $cache->addGoodLinkObjFromRow( $title, $row ); - $ids[$title->getPrefixedDBkey()] = $row->page_id; + $pdbk = $titleFormatter->getPrefixedDBkey( $title ); + $ids[$pdbk] = $row->page_id; unset( $remaining[$row->page_namespace][$row->page_title] ); } // The remaining links in $data are bad links, register them as such foreach ( $remaining as $ns => $dbkeys ) { foreach ( $dbkeys as $dbkey => $unused ) { - $title = Title::makeTitle( $ns, $dbkey ); + $title = new TitleValue( (int)$ns, (string)$dbkey ); $cache->addBadLinkObj( $title ); - $ids[$title->getPrefixedDBkey()] = 0; + $pdbk = $titleFormatter->getPrefixedDBkey( $title ); + $ids[$pdbk] = 0; } } @@ -218,7 +222,7 @@ class LinkBatch { return false; } - $genderCache = GenderCache::singleton(); + $genderCache = MediaWikiServices::getInstance()->getGenderCache(); $genderCache->doLinkBatch( $this->data, $this->caller ); return true; diff --git a/includes/cache/LinkCache.php b/includes/cache/LinkCache.php index de44f9bd85..3fd29f3270 100644 --- a/includes/cache/LinkCache.php +++ b/includes/cache/LinkCache.php @@ -230,7 +230,9 @@ class LinkCache { */ public function addLinkObj( LinkTarget $nt ) { $key = $this->titleFormatter->getPrefixedDBkey( $nt ); - if ( $this->isBadLink( $key ) || $nt->isExternal() ) { + if ( $this->isBadLink( $key ) || $nt->isExternal() + || $nt->inNamespace( NS_SPECIAL ) + ) { return 0; } $id = $this->getGoodLinkID( $key ); diff --git a/includes/cache/localisation/LocalisationCache.php b/includes/cache/localisation/LocalisationCache.php index dd7d81a33c..0fb9ed8561 100644 --- a/includes/cache/localisation/LocalisationCache.php +++ b/includes/cache/localisation/LocalisationCache.php @@ -23,6 +23,7 @@ use Cdb\Reader as CdbReader; use Cdb\Writer as CdbWriter; use CLDRPluralRuleParser\Evaluator; +use MediaWiki\MediaWikiServices; /** * Class for caching the contents of localisation files, Messages*.php @@ -802,12 +803,15 @@ class LocalisationCache { * @return array */ public function getMessagesDirs() { - global $wgMessagesDirs, $IP; + global $IP; + + $config = MediaWikiServices::getInstance()->getMainConfig(); + $messagesDirs = $config->get( 'MessagesDirs' ); return [ 'core' => "$IP/languages/i18n", 'api' => "$IP/includes/api/i18n", 'oojs-ui' => "$IP/resources/lib/oojs-ui/i18n", - ] + $wgMessagesDirs; + ] + $messagesDirs; } /** @@ -958,8 +962,9 @@ class LocalisationCache { # Add cache dependencies for any referenced globals $deps['wgExtensionMessagesFiles'] = new GlobalDependency( 'wgExtensionMessagesFiles' ); - // $wgMessagesDirs is used in LocalisationCache::getMessagesDirs() - $deps['wgMessagesDirs'] = new GlobalDependency( 'wgMessagesDirs' ); + // The 'MessagesDirs' config setting is used in LocalisationCache::getMessagesDirs(). + // We use the key 'wgMessagesDirs' for historical reasons. + $deps['wgMessagesDirs'] = new MainConfigDependency( 'MessagesDirs' ); $deps['version'] = new ConstantDependency( 'LocalisationCache::VERSION' ); # Add dependencies to the cache entry diff --git a/includes/changes/ChangesList.php b/includes/changes/ChangesList.php index cf97afbb1e..9948040ea9 100644 --- a/includes/changes/ChangesList.php +++ b/includes/changes/ChangesList.php @@ -21,6 +21,8 @@ * * @file */ +use MediaWiki\Linker\LinkRenderer; +use MediaWiki\MediaWikiServices; class ChangesList extends ContextSource { /** @@ -39,6 +41,11 @@ class ChangesList extends ContextSource { /** @var BagOStuff */ protected $watchMsgCache; + /** + * @var LinkRenderer + */ + protected $linkRenderer; + /** * Changeslist constructor * @@ -54,6 +61,7 @@ class ChangesList extends ContextSource { } $this->preCacheMessages(); $this->watchMsgCache = new HashBagOStuff( [ 'maxKeys' => 50 ] ); + $this->linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer(); } /** @@ -337,8 +345,10 @@ class ChangesList extends ContextSource { */ public function insertLog( &$s, $title, $logtype ) { $page = new LogPage( $logtype ); - $logname = $page->getName()->setContext( $this->getContext() )->escaped(); - $s .= $this->msg( 'parentheses' )->rawParams( Linker::linkKnown( $title, $logname ) )->escaped(); + $logname = $page->getName()->setContext( $this->getContext() )->text(); + $s .= $this->msg( 'parentheses' )->rawParams( + $this->linkRenderer->makeKnownLink( $title, $logname ) + )->escaped(); } /** @@ -363,10 +373,10 @@ class ChangesList extends ContextSource { 'oldid' => $rc->mAttribs['rc_last_oldid'] ]; - $diffLink = Linker::linkKnown( + $diffLink = $this->linkRenderer->makeKnownLink( $rc->getTitle(), - $this->message['diff'], - [ 'tabindex' => $rc->counter ], + new HtmlArmor( $this->message['diff'] ), + [], $query ); } @@ -375,9 +385,9 @@ class ChangesList extends ContextSource { } else { $diffhist = $diffLink . $this->message['pipe-separator']; # History link - $diffhist .= Linker::linkKnown( + $diffhist .= $this->linkRenderer->makeKnownLink( $rc->getTitle(), - $this->message['hist'], + new HtmlArmor( $this->message['hist'] ), [], [ 'curid' => $rc->mAttribs['rc_cur_id'], @@ -415,7 +425,7 @@ class ChangesList extends ContextSource { $params = [ 'redirect' => 'no' ]; } - $articlelink = Linker::link( + $articlelink = $this->linkRenderer->makeLink( $rc->getTitle(), null, [ 'class' => 'mw-changeslist-title' ], diff --git a/includes/changes/EnhancedChangesList.php b/includes/changes/EnhancedChangesList.php index 1070877c5d..099a295c91 100644 --- a/includes/changes/EnhancedChangesList.php +++ b/includes/changes/EnhancedChangesList.php @@ -54,7 +54,8 @@ class EnhancedChangesList extends ChangesList { // message is set by the parent ChangesList class $this->cacheEntryFactory = new RCCacheEntryFactory( $context, - $this->message + $this->message, + $this->linkRenderer ); } @@ -390,9 +391,9 @@ class EnhancedChangesList extends ChangesList { } elseif ( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT, $this->getUser() ) ) { $link = '' . $rcObj->timestamp . ' '; } else { - $link = Linker::linkKnown( + $link = $this->linkRenderer->makeKnownLink( $rcObj->getTitle(), - $rcObj->timestamp, + new HtmlArmor( $rcObj->timestamp ), [], $params ); @@ -524,26 +525,24 @@ class EnhancedChangesList extends ChangesList { ) { $links['total-changes'] = $nchanges[$n]; } else { - $links['total-changes'] = Linker::link( + $links['total-changes'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), - $nchanges[$n], + new HtmlArmor( $nchanges[$n] ), [], $queryParams + [ 'diff' => $currentRevision, 'oldid' => $last->mAttribs['rc_last_oldid'], - ], - [ 'known', 'noclasses' ] + ] ); if ( $sinceLast > 0 && $sinceLast < $n ) { - $links['total-changes-since-last'] = Linker::link( + $links['total-changes-since-last'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), - $sinceLastVisitMsg[$sinceLast], + new HtmlArmor( $sinceLastVisitMsg[$sinceLast] ), [], $queryParams + [ 'diff' => $currentRevision, 'oldid' => $unvisitedOldid, - ], - [ 'known', 'noclasses' ] + ] ); } } @@ -558,9 +557,9 @@ class EnhancedChangesList extends ChangesList { $params = $queryParams; $params['action'] = 'history'; - $links['history'] = Linker::linkKnown( + $links['history'] = $this->linkRenderer->makeKnownLink( $block0->getTitle(), - $this->message['enhancedrc-history'], + new HtmlArmor( $this->message['enhancedrc-history'] ), [], $params ); @@ -618,9 +617,11 @@ class EnhancedChangesList extends ChangesList { if ( $logType ) { $logPage = new LogPage( $logType ); $logTitle = SpecialPage::getTitleFor( 'Log', $logType ); - $logName = $logPage->getName()->escaped(); + $logName = $logPage->getName()->text(); $data['logLink'] = $this->msg( 'parentheses' ) - ->rawParams( Linker::linkKnown( $logTitle, $logName ) )->escaped(); + ->rawParams( + $this->linkRenderer->makeKnownLink( $logTitle, $logName ) + )->escaped(); } else { $data['articleLink'] = $this->getArticleLink( $rcObj, $rcObj->unpatrolled, $rcObj->watched ); } @@ -710,9 +711,10 @@ class EnhancedChangesList extends ChangesList { } $retVal = ' ' . $this->msg( 'parentheses' ) - ->rawParams( $rc->difflink . $this->message['pipe-separator'] . Linker::linkKnown( + ->rawParams( $rc->difflink . $this->message['pipe-separator'] + . $this->linkRenderer->makeKnownLink( $pageTitle, - $this->message['hist'], + new HtmlArmor( $this->message['hist'] ), [], $query ) )->escaped(); diff --git a/includes/changes/RCCacheEntryFactory.php b/includes/changes/RCCacheEntryFactory.php index 4c003d32dc..2c5c8b128c 100644 --- a/includes/changes/RCCacheEntryFactory.php +++ b/includes/changes/RCCacheEntryFactory.php @@ -19,6 +19,7 @@ * * @file */ +use MediaWiki\Linker\LinkRenderer; class RCCacheEntryFactory { @@ -28,13 +29,22 @@ class RCCacheEntryFactory { /* @var string[] */ private $messages; + /** + * @var LinkRenderer + */ + private $linkRenderer; + /** * @param IContextSource $context * @param string[] $messages + * @param LinkRenderer $linkRenderer */ - public function __construct( IContextSource $context, $messages ) { + public function __construct( + IContextSource $context, $messages, LinkRenderer $linkRenderer + ) { $this->context = $context; $this->messages = $messages; + $this->linkRenderer = $linkRenderer; } /** @@ -99,7 +109,7 @@ class RCCacheEntryFactory { // New unpatrolled pages if ( $cacheEntry->unpatrolled && $type == RC_NEW ) { - $clink = Linker::linkKnown( $cacheEntry->getTitle() ); + $clink = $this->linkRenderer->makeKnownLink( $cacheEntry->getTitle() ); // Log entries } elseif ( $type == RC_LOG ) { $logType = $cacheEntry->mAttribs['rc_log_type']; @@ -108,7 +118,7 @@ class RCCacheEntryFactory { $clink = $this->getLogLink( $logType ); } else { wfDebugLog( 'recentchanges', 'Unexpected log entry with no log type in recent changes' ); - $clink = Linker::link( $cacheEntry->getTitle() ); + $clink = $this->linkRenderer->makeLink( $cacheEntry->getTitle() ); } // Log entries (old format) and special pages } elseif ( $cacheEntry->mAttribs['rc_namespace'] == NS_SPECIAL ) { @@ -116,7 +126,7 @@ class RCCacheEntryFactory { $clink = ''; // Edits } else { - $clink = Linker::linkKnown( $cacheEntry->getTitle() ); + $clink = $this->linkRenderer->makeKnownLink( $cacheEntry->getTitle() ); } return $clink; @@ -125,10 +135,12 @@ class RCCacheEntryFactory { private function getLogLink( $logType ) { $logtitle = SpecialPage::getTitleFor( 'Log', $logType ); $logpage = new LogPage( $logType ); - $logname = $logpage->getName()->escaped(); + $logname = $logpage->getName()->text(); $logLink = $this->context->msg( 'parentheses' ) - ->rawParams( Linker::linkKnown( $logtitle, $logname ) )->escaped(); + ->rawParams( + $this->linkRenderer->makeKnownLink( $logtitle, $logname ) + )->escaped(); return $logLink; } @@ -174,7 +186,7 @@ class RCCacheEntryFactory { $curLink = $curMessage; } else { $curUrl = htmlspecialchars( $cacheEntry->getTitle()->getLinkURL( $queryParams ) ); - $curLink = "$curMessage"; + $curLink = "$curMessage"; } return $curLink; @@ -217,10 +229,10 @@ class RCCacheEntryFactory { return $diffMessage; } $diffUrl = htmlspecialchars( $pageTitle->getLinkURL( $queryParams ) ); - $diffLink = "$diffMessage"; + $diffLink = "$diffMessage"; } else { $diffUrl = htmlspecialchars( $cacheEntry->getTitle()->getLinkURL( $queryParams ) ); - $diffLink = "$diffMessage"; + $diffLink = "$diffMessage"; } return $diffLink; @@ -242,9 +254,9 @@ class RCCacheEntryFactory { if ( !$showDiffLinks || !$lastOldid || in_array( $type, $logTypes ) ) { $lastLink = $lastMessage; } else { - $lastLink = Linker::linkKnown( + $lastLink = $this->linkRenderer->makeKnownLink( $cacheEntry->getTitle(), - $lastMessage, + new HtmlArmor( $lastMessage ), [], $this->buildDiffQueryParams( $cacheEntry ) ); diff --git a/includes/collation/IcuCollation.php b/includes/collation/IcuCollation.php index a374b13168..27f917bd76 100644 --- a/includes/collation/IcuCollation.php +++ b/includes/collation/IcuCollation.php @@ -155,6 +155,11 @@ class IcuCollation extends Collation { 'smn' => [ "Á", "Č", "Đ", "Ŋ", "Å ", "Ŧ", "Ž", "Æ", "Ø", "Å", "Ä", "Ö" ], 'sq' => [ "Ç", "Dh", "Ë", "Gj", "Ll", "Nj", "Rr", "Sh", "Th", "Xh", "Zh" ], 'sr' => [], + 'ta' => [ + "\xE0\xAE\x82", "ஃ", "க்ஷ", "க்", "ங்", "ச்", "ஞ்", "ட்", "ண்", "த்", "ந்", + "ப்", "ம்", "ய்", "ர்", "ல்", "வ்", "ழ்", "ள்", "ற்", "ன்", "ஜ்", "ஶ்", "ஷ்", + "ஸ்", "ஹ்", "க்ஷ்" + ], 'tk' => [ "Ç", "Ä", "Ž", "Ň", "Ö", "Ş", "Ü", "Ý" ], 'tl' => [ "Ñ", "Ng" ], 'tr' => [ "Ç", "Ğ", "Ä°", "Ö", "Ş", "Ü" ], @@ -188,20 +193,11 @@ class IcuCollation extends Collation { } public function getSortKey( $string ) { - // intl extension produces non null-terminated - // strings. Appending '' fixes it so that it doesn't generate - // a warning on each access in debug php. - MediaWiki\suppressWarnings(); - $key = $this->mainCollator->getSortKey( $string ) . ''; - MediaWiki\restoreWarnings(); - return $key; + return $this->mainCollator->getSortKey( $string ); } public function getPrimarySortKey( $string ) { - MediaWiki\suppressWarnings(); - $key = $this->primaryCollator->getSortKey( $string ) . ''; - MediaWiki\restoreWarnings(); - return $key; + return $this->primaryCollator->getSortKey( $string ); } public function getFirstLetter( $string ) { diff --git a/includes/config/ConfigFactory.php b/includes/config/ConfigFactory.php index 09b0baa7be..cd25352dac 100644 --- a/includes/config/ConfigFactory.php +++ b/includes/config/ConfigFactory.php @@ -20,13 +20,15 @@ * * @file */ +use MediaWiki\Services\SalvageableService; +use Wikimedia\Assert\Assert; /** * Factory class to create Config objects * * @since 1.23 */ -class ConfigFactory { +class ConfigFactory implements SalvageableService { /** * Map of config name => callback @@ -50,6 +52,41 @@ class ConfigFactory { return \MediaWiki\MediaWikiServices::getInstance()->getConfigFactory(); } + /** + * Re-uses existing Cache objects from $other. Cache objects are only re-used if the + * registered factory function for both is the same. Cache config is not copied, + * and only instances of caches defined on this instance with the same config + * are copied. + * + * @see SalvageableService::salvage() + * + * @param SalvageableService $other The object to salvage state from. $other must have the + * exact same type as $this. + */ + public function salvage( SalvageableService $other ) { + Assert::parameterType( self::class, $other, '$other' ); + + /** @var ConfigFactory $other */ + foreach ( $other->factoryFunctions as $name => $otherFunc ) { + if ( !isset( $this->factoryFunctions[$name] ) ) { + continue; + } + + // if the callback function is the same, salvage the Cache object + // XXX: Closures are never equal! + if ( isset( $other->configs[$name] ) + && $this->factoryFunctions[$name] == $otherFunc + ) { + $this->configs[$name] = $other->configs[$name]; + unset( $other->configs[$name] ); + } + } + + // disable $other + $other->factoryFunctions = []; + $other->configs = []; + } + /** * @return string[] */ @@ -67,23 +104,11 @@ class ConfigFactory { * @throws InvalidArgumentException If an invalid callback is provided */ public function register( $name, $callback ) { - if ( $callback instanceof Config ) { - $instance = $callback; - - // Register a callback anyway, for consistency. Note that getConfigNames() - // relies on $factoryFunctions to have all config names. - $callback = function() use ( $instance ) { - return $instance; - }; - } else { - $instance = null; - } - - if ( !is_callable( $callback ) ) { + if ( !is_callable( $callback ) && !( $callback instanceof Config ) ) { throw new InvalidArgumentException( 'Invalid callback provided' ); } - $this->configs[$name] = $instance; + unset( $this->configs[$name] ); $this->factoryFunctions[$name] = $callback; } @@ -105,7 +130,13 @@ class ConfigFactory { if ( !isset( $this->factoryFunctions[$key] ) ) { throw new ConfigException( "No registered builder available for $name." ); } - $conf = call_user_func( $this->factoryFunctions[$key], $this ); + + if ( $this->factoryFunctions[$key] instanceof Config ) { + $conf = $this->factoryFunctions[$key]; + } else { + $conf = call_user_func( $this->factoryFunctions[$key], $this ); + } + if ( $conf instanceof Config ) { $this->configs[$name] = $conf; } else { diff --git a/includes/content/ContentHandler.php b/includes/content/ContentHandler.php index f3d678108f..e225fb783f 100644 --- a/includes/content/ContentHandler.php +++ b/includes/content/ContentHandler.php @@ -641,7 +641,12 @@ abstract class ContentHandler { * * @since 1.21 * - * @return array Always an empty array. + * @return array An array mapping action names (typically "view", "edit", "history" etc.) to + * either the full qualified class name of an Action class, a callable taking ( Page $page, + * IContextSource $context = null ) as parameters and returning an Action object, or an actual + * Action object. An empty array in this default implementation. + * + * @see Action::factory */ public function getActionOverrides() { return []; diff --git a/includes/db/DBConnRef.php b/includes/db/DBConnRef.php index d73ba85fc0..af5f8f9fee 100644 --- a/includes/db/DBConnRef.php +++ b/includes/db/DBConnRef.php @@ -433,7 +433,7 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function doAtomicSection( $fname, $callback ) { + public function doAtomicSection( $fname, callable $callback ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/db/Database.php b/includes/db/Database.php index 92e89b0de1..6bdcb24cdb 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -2561,11 +2561,7 @@ abstract class DatabaseBase implements IDatabase { } } - final public function doAtomicSection( $fname, $callback ) { - if ( !is_callable( $callback ) ) { - throw new UnexpectedValueException( "Invalid callback." ); - }; - + final public function doAtomicSection( $fname, callable $callback ) { $this->startAtomic( $fname ); try { call_user_func_array( $callback, [ $this, $fname ] ); diff --git a/includes/db/DatabaseMysqlBase.php b/includes/db/DatabaseMysqlBase.php index 13be911686..3ebc3ecce1 100644 --- a/includes/db/DatabaseMysqlBase.php +++ b/includes/db/DatabaseMysqlBase.php @@ -782,8 +782,10 @@ abstract class DatabaseMysqlBase extends Database { throw new InvalidArgumentException( "Position not an instance of MySQLMasterPos" ); } - if ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) { - return 0; + if ( $this->getLBInfo( 'is static' ) === true ) { + return 0; // this is a copy of a read-only dataset with no master DB + } elseif ( $this->lastKnownSlavePos && $this->lastKnownSlavePos->hasReached( $pos ) ) { + return 0; // already reached this point for sure } # Commit any open transactions diff --git a/includes/db/IDatabase.php b/includes/db/IDatabase.php index 710efb2ca6..0a71df2312 100644 --- a/includes/db/IDatabase.php +++ b/includes/db/IDatabase.php @@ -1313,7 +1313,7 @@ interface IDatabase { * @throws UnexpectedValueException * @since 1.27 */ - public function doAtomicSection( $fname, $callback ); + public function doAtomicSection( $fname, callable $callback ); /** * Begin a transaction. If a transaction is already in progress, diff --git a/includes/db/loadbalancer/LBFactory.php b/includes/db/loadbalancer/LBFactory.php index b78793f8aa..5b048b53d8 100644 --- a/includes/db/loadbalancer/LBFactory.php +++ b/includes/db/loadbalancer/LBFactory.php @@ -452,6 +452,15 @@ abstract class LBFactory implements DestructibleService { } } ); } + + /** + * Close all open database connections on all open load balancers. + * @since 1.28 + */ + public function closeAll() { + $this->forEachLBCallMethod( 'closeAll', [] ); + } + } /** diff --git a/includes/db/loadbalancer/LoadBalancer.php b/includes/db/loadbalancer/LoadBalancer.php index 557809905c..d96c665e4f 100644 --- a/includes/db/loadbalancer/LoadBalancer.php +++ b/includes/db/loadbalancer/LoadBalancer.php @@ -1334,7 +1334,7 @@ class LoadBalancer { $lagTimes = $this->getLagTimes( $wiki ); foreach ( $lagTimes as $i => $lag ) { - if ( $lag > $maxLag ) { + if ( $this->mLoads[$i] > 0 && $lag > $maxLag ) { $maxLag = $lag; $host = $this->mServers[$i]['host']; $maxIndex = $i; @@ -1402,7 +1402,7 @@ class LoadBalancer { } $pos = $pos ?: $this->getConnection( DB_MASTER )->getMasterPos(); - if ( !$pos ) { + if ( !( $pos instanceof DBMasterPos ) ) { return false; // something is misconfigured } diff --git a/includes/debug/MWDebug.php b/includes/debug/MWDebug.php index 13d25a86b4..d90ef8a7d1 100644 --- a/includes/debug/MWDebug.php +++ b/includes/debug/MWDebug.php @@ -75,6 +75,15 @@ class MWDebug { self::$enabled = true; } + /** + * Disable the debugger. + * + * @since 1.28 + */ + public static function deinit() { + self::$enabled = false; + } + /** * Add ResourceLoader modules to the OutputPage object if debugging is * enabled. diff --git a/includes/deferred/LinksDeletionUpdate.php b/includes/deferred/LinksDeletionUpdate.php index 65a8c0e0b1..b8bd74722c 100644 --- a/includes/deferred/LinksDeletionUpdate.php +++ b/includes/deferred/LinksDeletionUpdate.php @@ -42,48 +42,95 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate } elseif ( $pageId ) { $this->pageId = $pageId; } else { - throw new MWException( "Page ID not known, perhaps the page doesn't exist?" ); + throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" ); } } public function doUpdate() { - # Page may already be deleted, so don't just getId() + $config = RequestContext::getMain()->getConfig(); + $batchSize = $config->get( 'UpdateRowsPerQuery' ); + + // Page may already be deleted, so don't just getId() $id = $this->pageId; // Make sure all links update threads see the changes of each other. // This handles the case when updates have to batched into several COMMITs. $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id ); - # Delete restrictions for it + // Delete restrictions for it $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ ); - # Fix category table counts + // Fix category table counts $cats = $this->mDb->selectFieldValues( 'categorylinks', 'cl_to', [ 'cl_from' => $id ], __METHOD__ ); - $this->page->updateCategoryCounts( [], $cats ); + $catBatches = array_chunk( $cats, $batchSize ); + foreach ( $catBatches as $catBatch ) { + $this->page->updateCategoryCounts( [], $catBatch ); + if ( count( $catBatches ) > 1 ) { + $this->mDb->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); + } + } - # If using cascading deletes, we can skip some explicit deletes + // If using cascading deletes, we can skip some explicit deletes if ( !$this->mDb->cascadingDeletes() ) { - # Delete outgoing links - $this->mDb->delete( 'pagelinks', [ 'pl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'imagelinks', [ 'il_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'categorylinks', [ 'cl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'templatelinks', [ 'tl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'externallinks', [ 'el_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'langlinks', [ 'll_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'iwlinks', [ 'iwl_from' => $id ], __METHOD__ ); + // Delete outgoing links + $this->batchDeleteByPK( + 'pagelinks', + [ 'pl_from' => $id ], + [ 'pl_from', 'pl_namespace', 'pl_title' ], + $batchSize + ); + $this->batchDeleteByPK( + 'imagelinks', + [ 'il_from' => $id ], + [ 'il_from', 'il_to' ], + $batchSize + ); + $this->batchDeleteByPK( + 'categorylinks', + [ 'cl_from' => $id ], + [ 'cl_from', 'cl_to' ], + $batchSize + ); + $this->batchDeleteByPK( + 'templatelinks', + [ 'tl_from' => $id ], + [ 'tl_from', 'tl_namespace', 'tl_title' ], + $batchSize + ); + $this->batchDeleteByPK( + 'externallinks', + [ 'el_from' => $id ], + [ 'el_id' ], + $batchSize + ); + $this->batchDeleteByPK( + 'langlinks', + [ 'll_from' => $id ], + [ 'll_from', 'll_lang' ], + $batchSize + ); + $this->batchDeleteByPK( + 'iwlinks', + [ 'iwl_from' => $id ], + [ 'iwl_from', 'iwl_prefix', 'iwl_title' ], + $batchSize + ); + // Delete any redirect entry or page props entries $this->mDb->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ ); $this->mDb->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ ); } - # If using cleanup triggers, we can skip some manual deletes + // If using cleanup triggers, we can skip some manual deletes if ( !$this->mDb->cleanupTriggers() ) { $title = $this->page->getTitle(); - # Find recentchanges entries to clean up... - $rcIdsForTitle = $this->mDb->selectFieldValues( 'recentchanges', + // Find recentchanges entries to clean up... + $rcIdsForTitle = $this->mDb->selectFieldValues( + 'recentchanges', 'rc_id', [ 'rc_type != ' . RC_LOG, @@ -92,16 +139,21 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate ], __METHOD__ ); - $rcIdsForPage = $this->mDb->selectFieldValues( 'recentchanges', + $rcIdsForPage = $this->mDb->selectFieldValues( + 'recentchanges', 'rc_id', [ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ], __METHOD__ ); - # T98706: delete PK to avoid lock contention with RC delete log insertions - $rcIds = array_merge( $rcIdsForTitle, $rcIdsForPage ); - if ( $rcIds ) { - $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ ); + // T98706: delete by PK to avoid lock contention with RC delete log insertions + $rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize ); + foreach ( $rcIdBatches as $rcIdBatch ) { + $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ ); + if ( count( $rcIdBatches ) > 1 ) { + $this->mDb->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); + } } } @@ -111,6 +163,26 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate } ); } + private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) { + $dbw = $this->mDb; // convenience + $res = $dbw->select( $table, $pk, $conds, __METHOD__ ); + + $pkDeleteConds = []; + foreach ( $res as $row ) { + $pkDeleteConds[] = $this->mDb->makeList( (array)$row, LIST_AND ); + if ( count( $pkDeleteConds ) >= $bSize ) { + $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ ); + $dbw->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $dbw->getWikiID() ] ); + $pkDeleteConds = []; + } + } + + if ( $pkDeleteConds ) { + $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ ); + } + } + public function getAsJobSpecification() { return [ 'wiki' => $this->mDb->getWikiID(), diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index ac08374350..d4a61faf86 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -84,8 +84,6 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { */ private $user; - const BATCH_SIZE = 500; // try to keep typical updates in a single transaction - /** * Constructor * @@ -157,10 +155,11 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { Hooks::run( 'LinksUpdate', [ &$this ] ); $this->doIncrementalUpdate(); - $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) { + // Commit and release the lock + ScopedCallback::consume( $scopedLock ); + // Run post-commit hooks without DBO_TRX + $this->mDb->onTransactionIdle( function() { Hooks::run( 'LinksUpdateComplete', [ &$this ] ); - // Release the lock *after* the final COMMIT for correctness - ScopedCallback::consume( $scopedLock ); } ); } @@ -245,15 +244,14 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing ); $this->invalidateProperties( $changed ); - # Update the links table freshness for this title - $this->updateLinksTimestamp(); - # Refresh links of all pages including this page # This will be in a separate transaction if ( $this->mRecursive ) { $this->queueRecursiveJobs(); } + # Update the links table freshness for this title + $this->updateLinksTimestamp(); } /** @@ -338,6 +336,8 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { * @param array $insertions Rows to insert */ private function incrTableUpdate( $table, $prefix, $deletions, $insertions ) { + $bSize = RequestContext::getMain()->getConfig()->get( 'UpdateRowsPerQuery' ); + if ( $table === 'page_props' ) { $fromField = 'pp_page'; } else { @@ -354,7 +354,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { foreach ( $deletions as $ns => $dbKeys ) { foreach ( $dbKeys as $dbKey => $unused ) { $curDeletionBatch[$ns][$dbKey] = 1; - if ( ++$curBatchSize >= self::BATCH_SIZE ) { + if ( ++$curBatchSize >= $bSize ) { $deletionBatches[] = $curDeletionBatch; $curDeletionBatch = []; $curBatchSize = 0; @@ -380,7 +380,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { $toField = $prefix . '_to'; } - $deletionBatches = array_chunk( array_keys( $deletions ), self::BATCH_SIZE ); + $deletionBatches = array_chunk( array_keys( $deletions ), $bSize ); foreach ( $deletionBatches as $deletionBatch ) { $deleteWheres[] = [ $fromField => $this->mId, $toField => $deletionBatch ]; } @@ -389,14 +389,14 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { foreach ( $deleteWheres as $deleteWhere ) { $this->mDb->delete( $table, $deleteWhere, __METHOD__ ); $this->mDb->commit( __METHOD__, 'flush' ); - wfGetLBFactory()->waitForReplication(); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); } - $insertBatches = array_chunk( $insertions, self::BATCH_SIZE ); + $insertBatches = array_chunk( $insertions, $bSize ); foreach ( $insertBatches as $insertBatch ) { $this->mDb->insert( $table, $insertBatch, __METHOD__, 'IGNORE' ); $this->mDb->commit( __METHOD__, 'flush' ); - wfGetLBFactory()->waitForReplication(); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); } if ( count( $insertions ) ) { @@ -933,6 +933,14 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { $this->mRevision = $revision; } + /** + * @since 1.28 + * @return null|Revision + */ + public function getRevision() { + return $this->mRevision; + } + /** * Set the User who triggered this LinksUpdate * diff --git a/includes/diff/ComplexityException.php b/includes/diff/ComplexityException.php new file mode 100644 index 0000000000..10ca964ac2 --- /dev/null +++ b/includes/diff/ComplexityException.php @@ -0,0 +1,30 @@ + 2, and some optimizations) - * are my own. - * - * Line length limits for robustness added by Tim Starling, 2005-08-31 - * Alternative implementation added by Guy Van den Broeck, 2008-07-30 - * - * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck - * @private - * @ingroup DifferenceEngine - */ -class DiffEngine { - const MAX_XREF_LENGTH = 10000; - - protected $xchanged, $ychanged; - - protected $xv = [], $yv = []; - protected $xind = [], $yind = []; - - protected $seq = [], $in_seq = []; - - protected $lcs = 0; - - /** - * @param string[] $from_lines - * @param string[] $to_lines - * - * @return DiffOp[] - */ - public function diff( $from_lines, $to_lines ) { - - // Diff and store locally - $this->diffLocal( $from_lines, $to_lines ); - - // Merge edits when possible - $this->shiftBoundaries( $from_lines, $this->xchanged, $this->ychanged ); - $this->shiftBoundaries( $to_lines, $this->ychanged, $this->xchanged ); - - // Compute the edit operations. - $n_from = count( $from_lines ); - $n_to = count( $to_lines ); - - $edits = []; - $xi = $yi = 0; - while ( $xi < $n_from || $yi < $n_to ) { - assert( $yi < $n_to || $this->xchanged[$xi] ); - assert( $xi < $n_from || $this->ychanged[$yi] ); - - // Skip matching "snake". - $copy = []; - while ( $xi < $n_from && $yi < $n_to - && !$this->xchanged[$xi] && !$this->ychanged[$yi] - ) { - $copy[] = $from_lines[$xi++]; - ++$yi; - } - if ( $copy ) { - $edits[] = new DiffOpCopy( $copy ); - } - - // Find deletes & adds. - $delete = []; - while ( $xi < $n_from && $this->xchanged[$xi] ) { - $delete[] = $from_lines[$xi++]; - } - - $add = []; - while ( $yi < $n_to && $this->ychanged[$yi] ) { - $add[] = $to_lines[$yi++]; - } - - if ( $delete && $add ) { - $edits[] = new DiffOpChange( $delete, $add ); - } elseif ( $delete ) { - $edits[] = new DiffOpDelete( $delete ); - } elseif ( $add ) { - $edits[] = new DiffOpAdd( $add ); - } - } - - return $edits; - } - - /** - * @param string[] $from_lines - * @param string[] $to_lines - */ - private function diffLocal( $from_lines, $to_lines ) { - $wikidiff3 = new WikiDiff3(); - $wikidiff3->diff( $from_lines, $to_lines ); - $this->xchanged = $wikidiff3->removed; - $this->ychanged = $wikidiff3->added; - } - - /** - * Adjust inserts/deletes of identical lines to join changes - * as much as possible. - * - * We do something when a run of changed lines include a - * line at one end and has an excluded, identical line at the other. - * We are free to choose which identical line is included. - * `compareseq' usually chooses the one at the beginning, - * but usually it is cleaner to consider the following identical line - * to be the "change". - * - * This is extracted verbatim from analyze.c (GNU diffutils-2.7). - */ - private function shiftBoundaries( $lines, &$changed, $other_changed ) { - $i = 0; - $j = 0; - - assert( count( $lines ) == count( $changed ) ); - $len = count( $lines ); - $other_len = count( $other_changed ); - - while ( 1 ) { - /* - * Scan forwards to find beginning of another run of changes. - * Also keep track of the corresponding point in the other file. - * - * Throughout this code, $i and $j are adjusted together so that - * the first $i elements of $changed and the first $j elements - * of $other_changed both contain the same number of zeros - * (unchanged lines). - * Furthermore, $j is always kept so that $j == $other_len or - * $other_changed[$j] == false. - */ - while ( $j < $other_len && $other_changed[$j] ) { - $j++; - } - - while ( $i < $len && !$changed[$i] ) { - assert( $j < $other_len && ! $other_changed[$j] ); - $i++; - $j++; - while ( $j < $other_len && $other_changed[$j] ) { - $j++; - } - } - - if ( $i == $len ) { - break; - } - - $start = $i; - - // Find the end of this run of changes. - while ( ++$i < $len && $changed[$i] ) { - continue; - } - - do { - /* - * Record the length of this run of changes, so that - * we can later determine whether the run has grown. - */ - $runlength = $i - $start; - - /* - * Move the changed region back, so long as the - * previous unchanged line matches the last changed one. - * This merges with previous changed regions. - */ - while ( $start > 0 && $lines[$start - 1] == $lines[$i - 1] ) { - $changed[--$start] = 1; - $changed[--$i] = false; - while ( $start > 0 && $changed[$start - 1] ) { - $start--; - } - assert( $j > 0 ); - while ( $other_changed[--$j] ) { - continue; - } - assert( $j >= 0 && !$other_changed[$j] ); - } - - /* - * Set CORRESPONDING to the end of the changed run, at the last - * point where it corresponds to a changed run in the other file. - * CORRESPONDING == LEN means no such point has been found. - */ - $corresponding = $j < $other_len ? $i : $len; - - /* - * Move the changed region forward, so long as the - * first changed line matches the following unchanged one. - * This merges with following changed regions. - * Do this second, so that if there are no merges, - * the changed region is moved forward as far as possible. - */ - while ( $i < $len && $lines[$start] == $lines[$i] ) { - $changed[$start++] = false; - $changed[$i++] = 1; - while ( $i < $len && $changed[$i] ) { - $i++; - } - - assert( $j < $other_len && ! $other_changed[$j] ); - $j++; - if ( $j < $other_len && $other_changed[$j] ) { - $corresponding = $i; - while ( $j < $other_len && $other_changed[$j] ) { - $j++; - } - } - } - } while ( $runlength != $i - $start ); - - /* - * If possible, move the fully-merged run of changes - * back to a corresponding run in the other file. - */ - while ( $corresponding < $i ) { - $changed[--$start] = 1; - $changed[--$i] = 0; - assert( $j > 0 ); - while ( $other_changed[--$j] ) { - continue; - } - assert( $j >= 0 && !$other_changed[$j] ); - } - } - } -} - /** * Class representing a 'diff' between two sequences of strings. * @todo document @@ -442,6 +204,12 @@ class Diff { */ public $edits; + /** + * @var int If this diff complexity is exceeded, a ComplexityException is thrown + * 0 means no limit. + */ + protected $bailoutComplexity = 0; + /** * Constructor. * Computes diff between sequences of strings. @@ -449,9 +217,11 @@ class Diff { * @param string[] $from_lines An array of strings. * Typically these are lines from a file. * @param string[] $to_lines An array of strings. + * @throws \MediaWiki\Diff\ComplexityException */ public function __construct( $from_lines, $to_lines ) { $eng = new DiffEngine; + $eng->setBailoutComplexity( $this->bailoutComplexity ); $this->edits = $eng->diff( $from_lines, $to_lines ); } @@ -559,236 +329,7 @@ class Diff { } /** - * @todo document, bad name. - * @private - * @ingroup DifferenceEngine - */ -class MappedDiff extends Diff { - /** - * Constructor. - * - * Computes diff between sequences of strings. - * - * This can be used to compute things like - * case-insensitve diffs, or diffs which ignore - * changes in white-space. - * - * @param string[] $from_lines An array of strings. - * Typically these are lines from a file. - * @param string[] $to_lines An array of strings. - * @param string[] $mapped_from_lines This array should - * have the same size number of elements as $from_lines. - * The elements in $mapped_from_lines and - * $mapped_to_lines are what is actually compared - * when computing the diff. - * @param string[] $mapped_to_lines This array should - * have the same number of elements as $to_lines. - */ - public function __construct( $from_lines, $to_lines, - $mapped_from_lines, $mapped_to_lines ) { - - assert( count( $from_lines ) == count( $mapped_from_lines ) ); - assert( count( $to_lines ) == count( $mapped_to_lines ) ); - - parent::__construct( $mapped_from_lines, $mapped_to_lines ); - - $xi = $yi = 0; - $editCount = count( $this->edits ); - for ( $i = 0; $i < $editCount; $i++ ) { - $orig = &$this->edits[$i]->orig; - if ( is_array( $orig ) ) { - $orig = array_slice( $from_lines, $xi, count( $orig ) ); - $xi += count( $orig ); - } - - $closing = &$this->edits[$i]->closing; - if ( is_array( $closing ) ) { - $closing = array_slice( $to_lines, $yi, count( $closing ) ); - $yi += count( $closing ); - } - } - } -} - -/** - * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3 - */ - -/** - * @todo document - * @private - * @ingroup DifferenceEngine - */ -class HWLDFWordAccumulator { - public $insClass = ' class="diffchange diffchange-inline"'; - public $delClass = ' class="diffchange diffchange-inline"'; - - private $lines = []; - private $line = ''; - private $group = ''; - private $tag = ''; - - /** - * @param string $new_tag - */ - private function flushGroup( $new_tag ) { - if ( $this->group !== '' ) { - if ( $this->tag == 'ins' ) { - $this->line .= "insClass}>" . - htmlspecialchars( $this->group ) . ''; - } elseif ( $this->tag == 'del' ) { - $this->line .= "delClass}>" . - htmlspecialchars( $this->group ) . ''; - } else { - $this->line .= htmlspecialchars( $this->group ); - } - } - $this->group = ''; - $this->tag = $new_tag; - } - - /** - * @param string $new_tag - */ - private function flushLine( $new_tag ) { - $this->flushGroup( $new_tag ); - if ( $this->line != '' ) { - array_push( $this->lines, $this->line ); - } else { - # make empty lines visible by inserting an NBSP - array_push( $this->lines, ' ' ); - } - $this->line = ''; - } - - /** - * @param string[] $words - * @param string $tag - */ - public function addWords( $words, $tag = '' ) { - if ( $tag != $this->tag ) { - $this->flushGroup( $tag ); - } - - foreach ( $words as $word ) { - // new-line should only come as first char of word. - if ( $word == '' ) { - continue; - } - if ( $word[0] == "\n" ) { - $this->flushLine( $tag ); - $word = substr( $word, 1 ); - } - assert( !strstr( $word, "\n" ) ); - $this->group .= $word; - } - } - - /** - * @return string[] - */ - public function getLines() { - $this->flushLine( '~done' ); - - return $this->lines; - } -} - -/** - * @todo document - * @private - * @ingroup DifferenceEngine + * @deprecated Alias for WordAccumulator, to be soon removed */ -class WordLevelDiff extends MappedDiff { - const MAX_LINE_LENGTH = 10000; - - /** - * @param string[] $orig_lines - * @param string[] $closing_lines - */ - public function __construct( $orig_lines, $closing_lines ) { - - list( $orig_words, $orig_stripped ) = $this->split( $orig_lines ); - list( $closing_words, $closing_stripped ) = $this->split( $closing_lines ); - - parent::__construct( $orig_words, $closing_words, - $orig_stripped, $closing_stripped ); - } - - /** - * @param string[] $lines - * - * @return array[] - */ - private function split( $lines ) { - - $words = []; - $stripped = []; - $first = true; - foreach ( $lines as $line ) { - # If the line is too long, just pretend the entire line is one big word - # This prevents resource exhaustion problems - if ( $first ) { - $first = false; - } else { - $words[] = "\n"; - $stripped[] = "\n"; - } - if ( strlen( $line ) > self::MAX_LINE_LENGTH ) { - $words[] = $line; - $stripped[] = $line; - } else { - $m = []; - if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', - $line, $m ) - ) { - foreach ( $m[0] as $word ) { - $words[] = $word; - } - foreach ( $m[1] as $stripped_word ) { - $stripped[] = $stripped_word; - } - } - } - } - - return [ $words, $stripped ]; - } - - /** - * @return string[] - */ - public function orig() { - $orig = new HWLDFWordAccumulator; - - foreach ( $this->edits as $edit ) { - if ( $edit->type == 'copy' ) { - $orig->addWords( $edit->orig ); - } elseif ( $edit->orig ) { - $orig->addWords( $edit->orig, 'del' ); - } - } - $lines = $orig->getLines(); - - return $lines; - } - - /** - * @return string[] - */ - public function closing() { - $closing = new HWLDFWordAccumulator; - - foreach ( $this->edits as $edit ) { - if ( $edit->type == 'copy' ) { - $closing->addWords( $edit->closing ); - } elseif ( $edit->closing ) { - $closing->addWords( $edit->closing, 'ins' ); - } - } - $lines = $closing->getLines(); - - return $lines; - } - +class HWLDFWordAccumulator extends MediaWiki\Diff\WordAccumulator { } diff --git a/includes/diff/DiffEngine.php b/includes/diff/DiffEngine.php new file mode 100644 index 0000000000..babd00b5d7 --- /dev/null +++ b/includes/diff/DiffEngine.php @@ -0,0 +1,842 @@ + + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + */ +use MediaWiki\Diff\ComplexityException; + +/** + * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which + * in turn is based on Myers' "An O(ND) difference algorithm and its variations" + * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s + * "An O(NP) Sequence Comparison Algorithm"). + * + * This implementation supports an upper bound on the execution time. + * + * Some ideas (and a bit of code) are from analyze.c, from GNU + * diffutils-2.7, which can be found at: + * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz + * + * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space + * + * @author Guy Van den Broeck, Geoffrey T. Dairiki, Tim Starling + * @ingroup DifferenceEngine + */ +class DiffEngine { + + // Input variables + private $from; + private $to; + private $m; + private $n; + + private $tooLong; + private $powLimit; + + protected $bailoutComplexity = 0; + + // State variables + private $maxDifferences; + private $lcsLengthCorrectedForHeuristic = false; + + // Output variables + public $length; + public $removed; + public $added; + public $heuristicUsed; + + function __construct( $tooLong = 2000000, $powLimit = 1.45 ) { + $this->tooLong = $tooLong; + $this->powLimit = $powLimit; + } + + /** + * Performs diff + * + * @param string[] $from_lines + * @param string[] $to_lines + * @throws ComplexityException + * + * @return DiffOp[] + */ + public function diff( $from_lines, $to_lines ) { + + // Diff and store locally + $this->diffInternal( $from_lines, $to_lines ); + + // Merge edits when possible + $this->shiftBoundaries( $from_lines, $this->removed, $this->added ); + $this->shiftBoundaries( $to_lines, $this->added, $this->removed ); + + // Compute the edit operations. + $n_from = count( $from_lines ); + $n_to = count( $to_lines ); + + $edits = []; + $xi = $yi = 0; + while ( $xi < $n_from || $yi < $n_to ) { + assert( $yi < $n_to || $this->removed[$xi] ); + assert( $xi < $n_from || $this->added[$yi] ); + + // Skip matching "snake". + $copy = []; + while ( $xi < $n_from && $yi < $n_to + && !$this->removed[$xi] && !$this->added[$yi] + ) { + $copy[] = $from_lines[$xi++]; + ++$yi; + } + if ( $copy ) { + $edits[] = new DiffOpCopy( $copy ); + } + + // Find deletes & adds. + $delete = []; + while ( $xi < $n_from && $this->removed[$xi] ) { + $delete[] = $from_lines[$xi++]; + } + + $add = []; + while ( $yi < $n_to && $this->added[$yi] ) { + $add[] = $to_lines[$yi++]; + } + + if ( $delete && $add ) { + $edits[] = new DiffOpChange( $delete, $add ); + } elseif ( $delete ) { + $edits[] = new DiffOpDelete( $delete ); + } elseif ( $add ) { + $edits[] = new DiffOpAdd( $add ); + } + } + + return $edits; + } + + /** + * Sets the complexity (in comparison operations) that can't be exceeded + * @param int $value + */ + public function setBailoutComplexity( $value ) { + $this->bailoutComplexity = $value; + } + + /** + * Adjust inserts/deletes of identical lines to join changes + * as much as possible. + * + * We do something when a run of changed lines include a + * line at one end and has an excluded, identical line at the other. + * We are free to choose which identical line is included. + * `compareseq' usually chooses the one at the beginning, + * but usually it is cleaner to consider the following identical line + * to be the "change". + * + * This is extracted verbatim from analyze.c (GNU diffutils-2.7). + * + * @param string[] $lines + * @param string[] $changed + * @param string[] $other_changed + */ + private function shiftBoundaries( array $lines, array &$changed, array $other_changed ) { + $i = 0; + $j = 0; + + assert( count( $lines ) == count( $changed ) ); + $len = count( $lines ); + $other_len = count( $other_changed ); + + while ( 1 ) { + /* + * Scan forwards to find beginning of another run of changes. + * Also keep track of the corresponding point in the other file. + * + * Throughout this code, $i and $j are adjusted together so that + * the first $i elements of $changed and the first $j elements + * of $other_changed both contain the same number of zeros + * (unchanged lines). + * Furthermore, $j is always kept so that $j == $other_len or + * $other_changed[$j] == false. + */ + while ( $j < $other_len && $other_changed[$j] ) { + $j++; + } + + while ( $i < $len && !$changed[$i] ) { + assert( $j < $other_len && ! $other_changed[$j] ); + $i++; + $j++; + while ( $j < $other_len && $other_changed[$j] ) { + $j++; + } + } + + if ( $i == $len ) { + break; + } + + $start = $i; + + // Find the end of this run of changes. + while ( ++$i < $len && $changed[$i] ) { + continue; + } + + do { + /* + * Record the length of this run of changes, so that + * we can later determine whether the run has grown. + */ + $runlength = $i - $start; + + /* + * Move the changed region back, so long as the + * previous unchanged line matches the last changed one. + * This merges with previous changed regions. + */ + while ( $start > 0 && $lines[$start - 1] == $lines[$i - 1] ) { + $changed[--$start] = 1; + $changed[--$i] = false; + while ( $start > 0 && $changed[$start - 1] ) { + $start--; + } + assert( $j > 0 ); + while ( $other_changed[--$j] ) { + continue; + } + assert( $j >= 0 && !$other_changed[$j] ); + } + + /* + * Set CORRESPONDING to the end of the changed run, at the last + * point where it corresponds to a changed run in the other file. + * CORRESPONDING == LEN means no such point has been found. + */ + $corresponding = $j < $other_len ? $i : $len; + + /* + * Move the changed region forward, so long as the + * first changed line matches the following unchanged one. + * This merges with following changed regions. + * Do this second, so that if there are no merges, + * the changed region is moved forward as far as possible. + */ + while ( $i < $len && $lines[$start] == $lines[$i] ) { + $changed[$start++] = false; + $changed[$i++] = 1; + while ( $i < $len && $changed[$i] ) { + $i++; + } + + assert( $j < $other_len && ! $other_changed[$j] ); + $j++; + if ( $j < $other_len && $other_changed[$j] ) { + $corresponding = $i; + while ( $j < $other_len && $other_changed[$j] ) { + $j++; + } + } + } + } while ( $runlength != $i - $start ); + + /* + * If possible, move the fully-merged run of changes + * back to a corresponding run in the other file. + */ + while ( $corresponding < $i ) { + $changed[--$start] = 1; + $changed[--$i] = 0; + assert( $j > 0 ); + while ( $other_changed[--$j] ) { + continue; + } + assert( $j >= 0 && !$other_changed[$j] ); + } + } + } + + /** + * @param string[] $from + * @param string[] $to + * @throws ComplexityException + */ + protected function diffInternal( array $from, array $to ) { + // remember initial lengths + $m = count( $from ); + $n = count( $to ); + + $this->heuristicUsed = false; + + // output + $removed = $m > 0 ? array_fill( 0, $m, true ) : []; + $added = $n > 0 ? array_fill( 0, $n, true ) : []; + + // reduce the complexity for the next step (intentionally done twice) + // remove common tokens at the start + $i = 0; + while ( $i < $m && $i < $n && $from[$i] === $to[$i] ) { + $removed[$i] = $added[$i] = false; + unset( $from[$i], $to[$i] ); + ++$i; + } + + // remove common tokens at the end + $j = 1; + while ( $i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j] ) { + $removed[$m - $j] = $added[$n - $j] = false; + unset( $from[$m - $j], $to[$n - $j] ); + ++$j; + } + + $this->from = $newFromIndex = $this->to = $newToIndex = []; + + // remove tokens not in both sequences + $shared = []; + foreach ( $from as $key ) { + $shared[$key] = false; + } + + foreach ( $to as $index => &$el ) { + if ( array_key_exists( $el, $shared ) ) { + // keep it + $this->to[] = $el; + $shared[$el] = true; + $newToIndex[] = $index; + } + } + foreach ( $from as $index => &$el ) { + if ( $shared[$el] ) { + // keep it + $this->from[] = $el; + $newFromIndex[] = $index; + } + } + + unset( $shared, $from, $to ); + + $this->m = count( $this->from ); + $this->n = count( $this->to ); + + if ( $this->bailoutComplexity > 0 && $this->m * $this->n > $this->bailoutComplexity ) { + throw new ComplexityException(); + } + + $this->removed = $this->m > 0 ? array_fill( 0, $this->m, true ) : []; + $this->added = $this->n > 0 ? array_fill( 0, $this->n, true ) : []; + + if ( $this->m == 0 || $this->n == 0 ) { + $this->length = 0; + } else { + $this->maxDifferences = ceil( ( $this->m + $this->n ) / 2.0 ); + if ( $this->m * $this->n > $this->tooLong ) { + // limit complexity to D^POW_LIMIT for long sequences + $this->maxDifferences = floor( pow( $this->maxDifferences, $this->powLimit - 1.0 ) ); + wfDebug( "Limiting max number of differences to $this->maxDifferences\n" ); + } + + /* + * The common prefixes and suffixes are always part of some LCS, include + * them now to reduce our search space + */ + $max = min( $this->m, $this->n ); + for ( $forwardBound = 0; $forwardBound < $max + && $this->from[$forwardBound] === $this->to[$forwardBound]; + ++$forwardBound + ) { + $this->removed[$forwardBound] = $this->added[$forwardBound] = false; + } + + $backBoundL1 = $this->m - 1; + $backBoundL2 = $this->n - 1; + + while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound + && $this->from[$backBoundL1] === $this->to[$backBoundL2] + ) { + $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false; + } + + $temp = array_fill( 0, $this->m + $this->n + 1, 0 ); + $V = [ $temp, $temp ]; + $snake = [ 0, 0, 0 ]; + + $this->length = $forwardBound + $this->m - $backBoundL1 - 1 + + $this->lcs_rec( + $forwardBound, + $backBoundL1, + $forwardBound, + $backBoundL2, + $V, + $snake + ); + } + + $this->m = $m; + $this->n = $n; + + $this->length += $i + $j - 1; + + foreach ( $this->removed as $key => &$removed_elem ) { + if ( !$removed_elem ) { + $removed[$newFromIndex[$key]] = false; + } + } + foreach ( $this->added as $key => &$added_elem ) { + if ( !$added_elem ) { + $added[$newToIndex[$key]] = false; + } + } + $this->removed = $removed; + $this->added = $added; + } + + function diff_range( $from_lines, $to_lines ) { + // Diff and store locally + $this->diff( $from_lines, $to_lines ); + unset( $from_lines, $to_lines ); + + $ranges = []; + $xi = $yi = 0; + while ( $xi < $this->m || $yi < $this->n ) { + // Matching "snake". + while ( $xi < $this->m && $yi < $this->n + && !$this->removed[$xi] + && !$this->added[$yi] + ) { + ++$xi; + ++$yi; + } + // Find deletes & adds. + $xstart = $xi; + while ( $xi < $this->m && $this->removed[$xi] ) { + ++$xi; + } + + $ystart = $yi; + while ( $yi < $this->n && $this->added[$yi] ) { + ++$yi; + } + + if ( $xi > $xstart || $yi > $ystart ) { + $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi ); + } + } + + return $ranges; + } + + private function lcs_rec( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) { + // check that both sequences are non-empty + if ( $bottoml1 > $topl1 || $bottoml2 > $topl2 ) { + return 0; + } + + $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2, + $topl2, $V, $snake ); + + // need to store these so we don't lose them when they're + // overwritten by the recursion + $len = $snake[2]; + $startx = $snake[0]; + $starty = $snake[1]; + + // the middle snake is part of the LCS, store it + for ( $i = 0; $i < $len; ++$i ) { + $this->removed[$startx + $i] = $this->added[$starty + $i] = false; + } + + if ( $d > 1 ) { + return $len + + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2, + $starty - 1, $V, $snake ) + + $this->lcs_rec( $startx + $len, $topl1, $starty + $len, + $topl2, $V, $snake ); + } elseif ( $d == 1 ) { + /* + * In this case the sequences differ by exactly 1 line. We have + * already saved all the lines after the difference in the for loop + * above, now we need to save all the lines before the difference. + */ + $max = min( $startx - $bottoml1, $starty - $bottoml2 ); + for ( $i = 0; $i < $max; ++$i ) { + $this->removed[$bottoml1 + $i] = + $this->added[$bottoml2 + $i] = false; + } + + return $max + $len; + } + + return $len; + } + + private function find_middle_snake( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) { + $from = &$this->from; + $to = &$this->to; + $V0 = &$V[0]; + $V1 = &$V[1]; + $snake0 = &$snake[0]; + $snake1 = &$snake[1]; + $snake2 = &$snake[2]; + $bottoml1_min_1 = $bottoml1 - 1; + $bottoml2_min_1 = $bottoml2 - 1; + $N = $topl1 - $bottoml1_min_1; + $M = $topl2 - $bottoml2_min_1; + $delta = $N - $M; + $maxabsx = $N + $bottoml1; + $maxabsy = $M + $bottoml2; + $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) ); + + // value_to_add_forward: a 0 or 1 that we add to the start + // offset to make it odd/even + if ( ( $M & 1 ) == 1 ) { + $value_to_add_forward = 1; + } else { + $value_to_add_forward = 0; + } + + if ( ( $N & 1 ) == 1 ) { + $value_to_add_backward = 1; + } else { + $value_to_add_backward = 0; + } + + $start_forward = -$M; + $end_forward = $N; + $start_backward = -$N; + $end_backward = $M; + + $limit_min_1 = $limit - 1; + $limit_plus_1 = $limit + 1; + + $V0[$limit_plus_1] = 0; + $V1[$limit_min_1] = $N; + $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) ); + + if ( ( $delta & 1 ) == 1 ) { + for ( $d = 0; $d <= $limit; ++$d ) { + $start_diag = max( $value_to_add_forward + $start_forward, -$d ); + $end_diag = min( $end_forward, $d ); + $value_to_add_forward = 1 - $value_to_add_forward; + + // compute forward furthest reaching paths + for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { + if ( $k == -$d || ( $k < $d + && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) + ) { + $x = $V0[$limit_plus_1 + $k]; + } else { + $x = $V0[$limit_min_1 + $k] + 1; + } + + $absx = $snake0 = $x + $bottoml1; + $absy = $snake1 = $x - $k + $bottoml2; + + while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) { + ++$absx; + ++$absy; + } + $x = $absx - $bottoml1; + + $snake2 = $absx - $snake0; + $V0[$limit + $k] = $x; + if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1 + && $x >= $V1[$limit + $k - $delta] + ) { + return 2 * $d - 1; + } + + // check to see if we can cut down the diagonal range + if ( $x >= $N && $end_forward > $k - 1 ) { + $end_forward = $k - 1; + } elseif ( $absy - $bottoml2 >= $M ) { + $start_forward = $k + 1; + $value_to_add_forward = 0; + } + } + + $start_diag = max( $value_to_add_backward + $start_backward, -$d ); + $end_diag = min( $end_backward, $d ); + $value_to_add_backward = 1 - $value_to_add_backward; + + // compute backward furthest reaching paths + for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { + if ( $k == $d + || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) + ) { + $x = $V1[$limit_min_1 + $k]; + } else { + $x = $V1[$limit_plus_1 + $k] - 1; + } + + $y = $x - $k - $delta; + + $snake2 = 0; + while ( $x > 0 && $y > 0 + && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] + ) { + --$x; + --$y; + ++$snake2; + } + $V1[$limit + $k] = $x; + + // check to see if we can cut down our diagonal range + if ( $x <= 0 ) { + $start_backward = $k + 1; + $value_to_add_backward = 0; + } elseif ( $y <= 0 && $end_backward > $k - 1 ) { + $end_backward = $k - 1; + } + } + } + } else { + for ( $d = 0; $d <= $limit; ++$d ) { + $start_diag = max( $value_to_add_forward + $start_forward, -$d ); + $end_diag = min( $end_forward, $d ); + $value_to_add_forward = 1 - $value_to_add_forward; + + // compute forward furthest reaching paths + for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { + if ( $k == -$d + || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) + ) { + $x = $V0[$limit_plus_1 + $k]; + } else { + $x = $V0[$limit_min_1 + $k] + 1; + } + + $absx = $snake0 = $x + $bottoml1; + $absy = $snake1 = $x - $k + $bottoml2; + + while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) { + ++$absx; + ++$absy; + } + $x = $absx - $bottoml1; + $snake2 = $absx - $snake0; + $V0[$limit + $k] = $x; + + // check to see if we can cut down the diagonal range + if ( $x >= $N && $end_forward > $k - 1 ) { + $end_forward = $k - 1; + } elseif ( $absy - $bottoml2 >= $M ) { + $start_forward = $k + 1; + $value_to_add_forward = 0; + } + } + + $start_diag = max( $value_to_add_backward + $start_backward, -$d ); + $end_diag = min( $end_backward, $d ); + $value_to_add_backward = 1 - $value_to_add_backward; + + // compute backward furthest reaching paths + for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { + if ( $k == $d + || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) + ) { + $x = $V1[$limit_min_1 + $k]; + } else { + $x = $V1[$limit_plus_1 + $k] - 1; + } + + $y = $x - $k - $delta; + + $snake2 = 0; + while ( $x > 0 && $y > 0 + && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] + ) { + --$x; + --$y; + ++$snake2; + } + $V1[$limit + $k] = $x; + + if ( $k >= -$delta - $d && $k <= $d - $delta + && $x <= $V0[$limit + $k + $delta] + ) { + $snake0 = $bottoml1 + $x; + $snake1 = $bottoml2 + $y; + + return 2 * $d; + } + + // check to see if we can cut down our diagonal range + if ( $x <= 0 ) { + $start_backward = $k + 1; + $value_to_add_backward = 0; + } elseif ( $y <= 0 && $end_backward > $k - 1 ) { + $end_backward = $k - 1; + } + } + } + } + /* + * computing the true LCS is too expensive, instead find the diagonal + * with the most progress and pretend a midle snake of length 0 occurs + * there. + */ + + $most_progress = self::findMostProgress( $M, $N, $limit, $V ); + + $snake0 = $bottoml1 + $most_progress[0]; + $snake1 = $bottoml2 + $most_progress[1]; + $snake2 = 0; + wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" ); + $this->heuristicUsed = true; + + return 5; /* + * HACK: since we didn't really finish the LCS computation + * we don't really know the length of the SES. We don't do + * anything with the result anyway, unless it's <=1. We know + * for a fact SES > 1 so 5 is as good a number as any to + * return here + */ + } + + private static function findMostProgress( $M, $N, $limit, $V ) { + $delta = $N - $M; + + if ( ( $M & 1 ) == ( $limit & 1 ) ) { + $forward_start_diag = max( -$M, -$limit ); + } else { + $forward_start_diag = max( 1 - $M, -$limit ); + } + + $forward_end_diag = min( $N, $limit ); + + if ( ( $N & 1 ) == ( $limit & 1 ) ) { + $backward_start_diag = max( -$N, -$limit ); + } else { + $backward_start_diag = max( 1 - $N, -$limit ); + } + + $backward_end_diag = -min( $M, $limit ); + + $temp = [ 0, 0, 0 ]; + + $max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag, + $backward_end_diag - $backward_start_diag ) / 2 ), $temp ); + $num_progress = 0; // the 1st entry is current, it is initialized + // with 0s + + // first search the forward diagonals + for ( $k = $forward_start_diag; $k <= $forward_end_diag; $k += 2 ) { + $x = $V[0][$limit + $k]; + $y = $x - $k; + if ( $x > $N || $y > $M ) { + continue; + } + + $progress = $x + $y; + if ( $progress > $max_progress[0][2] ) { + $num_progress = 0; + $max_progress[0][0] = $x; + $max_progress[0][1] = $y; + $max_progress[0][2] = $progress; + } elseif ( $progress == $max_progress[0][2] ) { + ++$num_progress; + $max_progress[$num_progress][0] = $x; + $max_progress[$num_progress][1] = $y; + $max_progress[$num_progress][2] = $progress; + } + } + + $max_progress_forward = true; // initially the maximum + // progress is in the forward + // direction + + // now search the backward diagonals + for ( $k = $backward_start_diag; $k <= $backward_end_diag; $k += 2 ) { + $x = $V[1][$limit + $k]; + $y = $x - $k - $delta; + if ( $x < 0 || $y < 0 ) { + continue; + } + + $progress = $N - $x + $M - $y; + if ( $progress > $max_progress[0][2] ) { + $num_progress = 0; + $max_progress_forward = false; + $max_progress[0][0] = $x; + $max_progress[0][1] = $y; + $max_progress[0][2] = $progress; + } elseif ( $progress == $max_progress[0][2] && !$max_progress_forward ) { + ++$num_progress; + $max_progress[$num_progress][0] = $x; + $max_progress[$num_progress][1] = $y; + $max_progress[$num_progress][2] = $progress; + } + } + + // return the middle diagonal with maximal progress. + return $max_progress[(int)floor( $num_progress / 2 )]; + } + + /** + * @return mixed + */ + public function getLcsLength() { + if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) { + $this->lcsLengthCorrectedForHeuristic = true; + $this->length = $this->m - array_sum( $this->added ); + } + + return $this->length; + } + +} + +/** + * Alternative representation of a set of changes, by the index + * ranges that are changed. + * + * @ingroup DifferenceEngine + */ +class RangeDifference { + + /** @var int */ + public $leftstart; + + /** @var int */ + public $leftend; + + /** @var int */ + public $leftlength; + + /** @var int */ + public $rightstart; + + /** @var int */ + public $rightend; + + /** @var int */ + public $rightlength; + + function __construct( $leftstart, $leftend, $rightstart, $rightend ) { + $this->leftstart = $leftstart; + $this->leftend = $leftend; + $this->leftlength = $leftend - $leftstart; + $this->rightstart = $rightstart; + $this->rightend = $rightend; + $this->rightlength = $rightend - $rightstart; + } + +} diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index e2345ca2ba..f35356ce06 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -474,16 +474,17 @@ class DifferenceEngine extends ContextSource { if ( !$linkInfo ) { $this->mMarkPatrolledLink = ''; } else { - $this->mMarkPatrolledLink = ' [' . Linker::linkKnown( - $this->mNewPage, - $this->msg( 'markaspatrolleddiff' )->escaped(), - [], - [ - 'action' => 'markpatrolled', - 'rcid' => $linkInfo['rcid'], - 'token' => $linkInfo['token'], - ] - ) . ']'; + $this->mMarkPatrolledLink = ' [' . + Linker::linkKnown( + $this->mNewPage, + $this->msg( 'markaspatrolleddiff' )->escaped(), + [], + [ + 'action' => 'markpatrolled', + 'rcid' => $linkInfo['rcid'], + 'token' => $linkInfo['token'], + ] + ) . ']'; } } return $this->mMarkPatrolledLink; @@ -842,19 +843,36 @@ class DifferenceEngine extends ContextSource { * @return bool|string */ public function generateTextDiffBody( $otext, $ntext ) { - $time = microtime( true ); + $diff = function() use ( $otext, $ntext ) { + $time = microtime( true ); - $result = $this->textDiff( $otext, $ntext ); + $result = $this->textDiff( $otext, $ntext ); - $time = intval( ( microtime( true ) - $time ) * 1000 ); - $this->getStats()->timing( 'diff_time', $time ); - // Log requests slower than 99th percentile - if ( $time > 100 && $this->mOldPage && $this->mNewPage ) { - wfDebugLog( 'diff', - "$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" ); + $time = intval( ( microtime( true ) - $time ) * 1000 ); + $this->getStats()->timing( 'diff_time', $time ); + // Log requests slower than 99th percentile + if ( $time > 100 && $this->mOldPage && $this->mNewPage ) { + wfDebugLog( 'diff', + "$time ms diff: {$this->mOldid} -> {$this->mNewid} {$this->mNewPage}" ); + } + + return $result; + }; + + $error = function( $status ) { + throw new FatalError( $status->getWikiText() ); + }; + + // Use PoolCounter if the diff looks like it can be expensive + if ( strlen( $otext ) + strlen( $ntext ) > 20000 ) { + $work = new PoolCounterWorkViaCallback( 'diff', + md5( $otext ) . md5( $ntext ), + [ 'doWork' => $diff, 'error' => $error ] + ); + return $work->execute(); } - return $result; + return $diff(); } /** diff --git a/includes/diff/WikiDiff3.php b/includes/diff/WikiDiff3.php deleted file mode 100644 index f35e30f325..0000000000 --- a/includes/diff/WikiDiff3.php +++ /dev/null @@ -1,621 +0,0 @@ - - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - * @ingroup DifferenceEngine - */ - -/** - * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which - * in turn is based on Myers' "An O(ND) difference algorithm and its variations" - * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s - * "An O(NP) Sequence Comparison Algorithm"). - * - * This implementation supports an upper bound on the execution time. - * - * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space - * - * @author Guy Van den Broeck - * @ingroup DifferenceEngine - */ -class WikiDiff3 { - - // Input variables - private $from; - private $to; - private $m; - private $n; - - private $tooLong; - private $powLimit; - - // State variables - private $maxDifferences; - private $lcsLengthCorrectedForHeuristic = false; - - // Output variables - public $length; - public $removed; - public $added; - public $heuristicUsed; - - function __construct( $tooLong = 2000000, $powLimit = 1.45 ) { - $this->tooLong = $tooLong; - $this->powLimit = $powLimit; - } - - public function diff( /*array*/ $from, /*array*/ $to ) { - // remember initial lengths - $m = count( $from ); - $n = count( $to ); - - $this->heuristicUsed = false; - - // output - $removed = $m > 0 ? array_fill( 0, $m, true ) : []; - $added = $n > 0 ? array_fill( 0, $n, true ) : []; - - // reduce the complexity for the next step (intentionally done twice) - // remove common tokens at the start - $i = 0; - while ( $i < $m && $i < $n && $from[$i] === $to[$i] ) { - $removed[$i] = $added[$i] = false; - unset( $from[$i], $to[$i] ); - ++$i; - } - - // remove common tokens at the end - $j = 1; - while ( $i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j] ) { - $removed[$m - $j] = $added[$n - $j] = false; - unset( $from[$m - $j], $to[$n - $j] ); - ++$j; - } - - $this->from = $newFromIndex = $this->to = $newToIndex = []; - - // remove tokens not in both sequences - $shared = []; - foreach ( $from as $key ) { - $shared[$key] = false; - } - - foreach ( $to as $index => &$el ) { - if ( array_key_exists( $el, $shared ) ) { - // keep it - $this->to[] = $el; - $shared[$el] = true; - $newToIndex[] = $index; - } - } - foreach ( $from as $index => &$el ) { - if ( $shared[$el] ) { - // keep it - $this->from[] = $el; - $newFromIndex[] = $index; - } - } - - unset( $shared, $from, $to ); - - $this->m = count( $this->from ); - $this->n = count( $this->to ); - - $this->removed = $this->m > 0 ? array_fill( 0, $this->m, true ) : []; - $this->added = $this->n > 0 ? array_fill( 0, $this->n, true ) : []; - - if ( $this->m == 0 || $this->n == 0 ) { - $this->length = 0; - } else { - $this->maxDifferences = ceil( ( $this->m + $this->n ) / 2.0 ); - if ( $this->m * $this->n > $this->tooLong ) { - // limit complexity to D^POW_LIMIT for long sequences - $this->maxDifferences = floor( pow( $this->maxDifferences, $this->powLimit - 1.0 ) ); - wfDebug( "Limiting max number of differences to $this->maxDifferences\n" ); - } - - /* - * The common prefixes and suffixes are always part of some LCS, include - * them now to reduce our search space - */ - $max = min( $this->m, $this->n ); - for ( $forwardBound = 0; $forwardBound < $max - && $this->from[$forwardBound] === $this->to[$forwardBound]; - ++$forwardBound - ) { - $this->removed[$forwardBound] = $this->added[$forwardBound] = false; - } - - $backBoundL1 = $this->m - 1; - $backBoundL2 = $this->n - 1; - - while ( $backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound - && $this->from[$backBoundL1] === $this->to[$backBoundL2] - ) { - $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false; - } - - $temp = array_fill( 0, $this->m + $this->n + 1, 0 ); - $V = [ $temp, $temp ]; - $snake = [ 0, 0, 0 ]; - - $this->length = $forwardBound + $this->m - $backBoundL1 - 1 - + $this->lcs_rec( - $forwardBound, - $backBoundL1, - $forwardBound, - $backBoundL2, - $V, - $snake - ); - } - - $this->m = $m; - $this->n = $n; - - $this->length += $i + $j - 1; - - foreach ( $this->removed as $key => &$removed_elem ) { - if ( !$removed_elem ) { - $removed[$newFromIndex[$key]] = false; - } - } - foreach ( $this->added as $key => &$added_elem ) { - if ( !$added_elem ) { - $added[$newToIndex[$key]] = false; - } - } - $this->removed = $removed; - $this->added = $added; - } - - function diff_range( $from_lines, $to_lines ) { - // Diff and store locally - $this->diff( $from_lines, $to_lines ); - unset( $from_lines, $to_lines ); - - $ranges = []; - $xi = $yi = 0; - while ( $xi < $this->m || $yi < $this->n ) { - // Matching "snake". - while ( $xi < $this->m && $yi < $this->n - && !$this->removed[$xi] - && !$this->added[$yi] - ) { - ++$xi; - ++$yi; - } - // Find deletes & adds. - $xstart = $xi; - while ( $xi < $this->m && $this->removed[$xi] ) { - ++$xi; - } - - $ystart = $yi; - while ( $yi < $this->n && $this->added[$yi] ) { - ++$yi; - } - - if ( $xi > $xstart || $yi > $ystart ) { - $ranges[] = new RangeDifference( $xstart, $xi, $ystart, $yi ); - } - } - - return $ranges; - } - - private function lcs_rec( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) { - // check that both sequences are non-empty - if ( $bottoml1 > $topl1 || $bottoml2 > $topl2 ) { - return 0; - } - - $d = $this->find_middle_snake( $bottoml1, $topl1, $bottoml2, - $topl2, $V, $snake ); - - // need to store these so we don't lose them when they're - // overwritten by the recursion - $len = $snake[2]; - $startx = $snake[0]; - $starty = $snake[1]; - - // the middle snake is part of the LCS, store it - for ( $i = 0; $i < $len; ++$i ) { - $this->removed[$startx + $i] = $this->added[$starty + $i] = false; - } - - if ( $d > 1 ) { - return $len - + $this->lcs_rec( $bottoml1, $startx - 1, $bottoml2, - $starty - 1, $V, $snake ) - + $this->lcs_rec( $startx + $len, $topl1, $starty + $len, - $topl2, $V, $snake ); - } elseif ( $d == 1 ) { - /* - * In this case the sequences differ by exactly 1 line. We have - * already saved all the lines after the difference in the for loop - * above, now we need to save all the lines before the difference. - */ - $max = min( $startx - $bottoml1, $starty - $bottoml2 ); - for ( $i = 0; $i < $max; ++$i ) { - $this->removed[$bottoml1 + $i] = - $this->added[$bottoml2 + $i] = false; - } - - return $max + $len; - } - - return $len; - } - - private function find_middle_snake( $bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake ) { - $from = &$this->from; - $to = &$this->to; - $V0 = &$V[0]; - $V1 = &$V[1]; - $snake0 = &$snake[0]; - $snake1 = &$snake[1]; - $snake2 = &$snake[2]; - $bottoml1_min_1 = $bottoml1 - 1; - $bottoml2_min_1 = $bottoml2 - 1; - $N = $topl1 - $bottoml1_min_1; - $M = $topl2 - $bottoml2_min_1; - $delta = $N - $M; - $maxabsx = $N + $bottoml1; - $maxabsy = $M + $bottoml2; - $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) ); - - // value_to_add_forward: a 0 or 1 that we add to the start - // offset to make it odd/even - if ( ( $M & 1 ) == 1 ) { - $value_to_add_forward = 1; - } else { - $value_to_add_forward = 0; - } - - if ( ( $N & 1 ) == 1 ) { - $value_to_add_backward = 1; - } else { - $value_to_add_backward = 0; - } - - $start_forward = -$M; - $end_forward = $N; - $start_backward = -$N; - $end_backward = $M; - - $limit_min_1 = $limit - 1; - $limit_plus_1 = $limit + 1; - - $V0[$limit_plus_1] = 0; - $V1[$limit_min_1] = $N; - $limit = min( $this->maxDifferences, ceil( ( $N + $M ) / 2 ) ); - - if ( ( $delta & 1 ) == 1 ) { - for ( $d = 0; $d <= $limit; ++$d ) { - $start_diag = max( $value_to_add_forward + $start_forward, -$d ); - $end_diag = min( $end_forward, $d ); - $value_to_add_forward = 1 - $value_to_add_forward; - - // compute forward furthest reaching paths - for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { - if ( $k == -$d || ( $k < $d - && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) - ) { - $x = $V0[$limit_plus_1 + $k]; - } else { - $x = $V0[$limit_min_1 + $k] + 1; - } - - $absx = $snake0 = $x + $bottoml1; - $absy = $snake1 = $x - $k + $bottoml2; - - while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) { - ++$absx; - ++$absy; - } - $x = $absx - $bottoml1; - - $snake2 = $absx - $snake0; - $V0[$limit + $k] = $x; - if ( $k >= $delta - $d + 1 && $k <= $delta + $d - 1 - && $x >= $V1[$limit + $k - $delta] - ) { - return 2 * $d - 1; - } - - // check to see if we can cut down the diagonal range - if ( $x >= $N && $end_forward > $k - 1 ) { - $end_forward = $k - 1; - } elseif ( $absy - $bottoml2 >= $M ) { - $start_forward = $k + 1; - $value_to_add_forward = 0; - } - } - - $start_diag = max( $value_to_add_backward + $start_backward, -$d ); - $end_diag = min( $end_backward, $d ); - $value_to_add_backward = 1 - $value_to_add_backward; - - // compute backward furthest reaching paths - for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { - if ( $k == $d - || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) - ) { - $x = $V1[$limit_min_1 + $k]; - } else { - $x = $V1[$limit_plus_1 + $k] - 1; - } - - $y = $x - $k - $delta; - - $snake2 = 0; - while ( $x > 0 && $y > 0 - && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] - ) { - --$x; - --$y; - ++$snake2; - } - $V1[$limit + $k] = $x; - - // check to see if we can cut down our diagonal range - if ( $x <= 0 ) { - $start_backward = $k + 1; - $value_to_add_backward = 0; - } elseif ( $y <= 0 && $end_backward > $k - 1 ) { - $end_backward = $k - 1; - } - } - } - } else { - for ( $d = 0; $d <= $limit; ++$d ) { - $start_diag = max( $value_to_add_forward + $start_forward, -$d ); - $end_diag = min( $end_forward, $d ); - $value_to_add_forward = 1 - $value_to_add_forward; - - // compute forward furthest reaching paths - for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { - if ( $k == -$d - || ( $k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k] ) - ) { - $x = $V0[$limit_plus_1 + $k]; - } else { - $x = $V0[$limit_min_1 + $k] + 1; - } - - $absx = $snake0 = $x + $bottoml1; - $absy = $snake1 = $x - $k + $bottoml2; - - while ( $absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy] ) { - ++$absx; - ++$absy; - } - $x = $absx - $bottoml1; - $snake2 = $absx - $snake0; - $V0[$limit + $k] = $x; - - // check to see if we can cut down the diagonal range - if ( $x >= $N && $end_forward > $k - 1 ) { - $end_forward = $k - 1; - } elseif ( $absy - $bottoml2 >= $M ) { - $start_forward = $k + 1; - $value_to_add_forward = 0; - } - } - - $start_diag = max( $value_to_add_backward + $start_backward, -$d ); - $end_diag = min( $end_backward, $d ); - $value_to_add_backward = 1 - $value_to_add_backward; - - // compute backward furthest reaching paths - for ( $k = $start_diag; $k <= $end_diag; $k += 2 ) { - if ( $k == $d - || ( $k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k] ) - ) { - $x = $V1[$limit_min_1 + $k]; - } else { - $x = $V1[$limit_plus_1 + $k] - 1; - } - - $y = $x - $k - $delta; - - $snake2 = 0; - while ( $x > 0 && $y > 0 - && $from[$x + $bottoml1_min_1] === $to[$y + $bottoml2_min_1] - ) { - --$x; - --$y; - ++$snake2; - } - $V1[$limit + $k] = $x; - - if ( $k >= -$delta - $d && $k <= $d - $delta - && $x <= $V0[$limit + $k + $delta] - ) { - $snake0 = $bottoml1 + $x; - $snake1 = $bottoml2 + $y; - - return 2 * $d; - } - - // check to see if we can cut down our diagonal range - if ( $x <= 0 ) { - $start_backward = $k + 1; - $value_to_add_backward = 0; - } elseif ( $y <= 0 && $end_backward > $k - 1 ) { - $end_backward = $k - 1; - } - } - } - } - /* - * computing the true LCS is too expensive, instead find the diagonal - * with the most progress and pretend a midle snake of length 0 occurs - * there. - */ - - $most_progress = self::findMostProgress( $M, $N, $limit, $V ); - - $snake0 = $bottoml1 + $most_progress[0]; - $snake1 = $bottoml2 + $most_progress[1]; - $snake2 = 0; - wfDebug( "Computing the LCS is too expensive. Using a heuristic.\n" ); - $this->heuristicUsed = true; - - return 5; /* - * HACK: since we didn't really finish the LCS computation - * we don't really know the length of the SES. We don't do - * anything with the result anyway, unless it's <=1. We know - * for a fact SES > 1 so 5 is as good a number as any to - * return here - */ - } - - private static function findMostProgress( $M, $N, $limit, $V ) { - $delta = $N - $M; - - if ( ( $M & 1 ) == ( $limit & 1 ) ) { - $forward_start_diag = max( -$M, -$limit ); - } else { - $forward_start_diag = max( 1 - $M, -$limit ); - } - - $forward_end_diag = min( $N, $limit ); - - if ( ( $N & 1 ) == ( $limit & 1 ) ) { - $backward_start_diag = max( -$N, -$limit ); - } else { - $backward_start_diag = max( 1 - $N, -$limit ); - } - - $backward_end_diag = -min( $M, $limit ); - - $temp = [ 0, 0, 0 ]; - - $max_progress = array_fill( 0, ceil( max( $forward_end_diag - $forward_start_diag, - $backward_end_diag - $backward_start_diag ) / 2 ), $temp ); - $num_progress = 0; // the 1st entry is current, it is initialized - // with 0s - - // first search the forward diagonals - for ( $k = $forward_start_diag; $k <= $forward_end_diag; $k += 2 ) { - $x = $V[0][$limit + $k]; - $y = $x - $k; - if ( $x > $N || $y > $M ) { - continue; - } - - $progress = $x + $y; - if ( $progress > $max_progress[0][2] ) { - $num_progress = 0; - $max_progress[0][0] = $x; - $max_progress[0][1] = $y; - $max_progress[0][2] = $progress; - } elseif ( $progress == $max_progress[0][2] ) { - ++$num_progress; - $max_progress[$num_progress][0] = $x; - $max_progress[$num_progress][1] = $y; - $max_progress[$num_progress][2] = $progress; - } - } - - $max_progress_forward = true; // initially the maximum - // progress is in the forward - // direction - - // now search the backward diagonals - for ( $k = $backward_start_diag; $k <= $backward_end_diag; $k += 2 ) { - $x = $V[1][$limit + $k]; - $y = $x - $k - $delta; - if ( $x < 0 || $y < 0 ) { - continue; - } - - $progress = $N - $x + $M - $y; - if ( $progress > $max_progress[0][2] ) { - $num_progress = 0; - $max_progress_forward = false; - $max_progress[0][0] = $x; - $max_progress[0][1] = $y; - $max_progress[0][2] = $progress; - } elseif ( $progress == $max_progress[0][2] && !$max_progress_forward ) { - ++$num_progress; - $max_progress[$num_progress][0] = $x; - $max_progress[$num_progress][1] = $y; - $max_progress[$num_progress][2] = $progress; - } - } - - // return the middle diagonal with maximal progress. - return $max_progress[(int)floor( $num_progress / 2 )]; - } - - /** - * @return mixed - */ - public function getLcsLength() { - if ( $this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic ) { - $this->lcsLengthCorrectedForHeuristic = true; - $this->length = $this->m - array_sum( $this->added ); - } - - return $this->length; - } - -} - -/** - * Alternative representation of a set of changes, by the index - * ranges that are changed. - * - * @ingroup DifferenceEngine - */ -class RangeDifference { - - /** @var int */ - public $leftstart; - - /** @var int */ - public $leftend; - - /** @var int */ - public $leftlength; - - /** @var int */ - public $rightstart; - - /** @var int */ - public $rightend; - - /** @var int */ - public $rightlength; - - function __construct( $leftstart, $leftend, $rightstart, $rightend ) { - $this->leftstart = $leftstart; - $this->leftend = $leftend; - $this->leftlength = $leftend - $leftstart; - $this->rightstart = $rightstart; - $this->rightend = $rightend; - $this->rightlength = $rightend - $rightstart; - } - -} diff --git a/includes/diff/WordAccumulator.php b/includes/diff/WordAccumulator.php new file mode 100644 index 0000000000..a26775ffa8 --- /dev/null +++ b/includes/diff/WordAccumulator.php @@ -0,0 +1,107 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + * @defgroup DifferenceEngine DifferenceEngine + */ + +namespace MediaWiki\Diff; + +/** + * Stores, escapes and formats the results of word-level diff + * + * @private + * @ingroup DifferenceEngine + */ +class WordAccumulator { + public $insClass = ' class="diffchange diffchange-inline"'; + public $delClass = ' class="diffchange diffchange-inline"'; + + private $lines = []; + private $line = ''; + private $group = ''; + private $tag = ''; + + /** + * @param string $new_tag + */ + private function flushGroup( $new_tag ) { + if ( $this->group !== '' ) { + if ( $this->tag == 'ins' ) { + $this->line .= "insClass}>" . + htmlspecialchars( $this->group ) . ''; + } elseif ( $this->tag == 'del' ) { + $this->line .= "delClass}>" . + htmlspecialchars( $this->group ) . ''; + } else { + $this->line .= htmlspecialchars( $this->group ); + } + } + $this->group = ''; + $this->tag = $new_tag; + } + + /** + * @param string $new_tag + */ + private function flushLine( $new_tag ) { + $this->flushGroup( $new_tag ); + if ( $this->line != '' ) { + array_push( $this->lines, $this->line ); + } else { + # make empty lines visible by inserting an NBSP + array_push( $this->lines, ' ' ); + } + $this->line = ''; + } + + /** + * @param string[] $words + * @param string $tag + */ + public function addWords( $words, $tag = '' ) { + if ( $tag != $this->tag ) { + $this->flushGroup( $tag ); + } + + foreach ( $words as $word ) { + // new-line should only come as first char of word. + if ( $word == '' ) { + continue; + } + if ( $word[0] == "\n" ) { + $this->flushLine( $tag ); + $word = substr( $word, 1 ); + } + assert( !strstr( $word, "\n" ) ); + $this->group .= $word; + } + } + + /** + * @return string[] + */ + public function getLines() { + $this->flushLine( '~done' ); + + return $this->lines; + } +} diff --git a/includes/diff/WordLevelDiff.php b/includes/diff/WordLevelDiff.php new file mode 100644 index 0000000000..296e3b7493 --- /dev/null +++ b/includes/diff/WordLevelDiff.php @@ -0,0 +1,142 @@ + + * You may copy this code freely under the conditions of the GPL. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @ingroup DifferenceEngine + * @defgroup DifferenceEngine DifferenceEngine + */ + +use MediaWiki\Diff\ComplexityException; +use MediaWiki\Diff\WordAccumulator; + +/** + * Performs a word-level diff on several lines + * + * @ingroup DifferenceEngine + */ +class WordLevelDiff extends \Diff { + /** + * @inheritdoc + */ + protected $bailoutComplexity = 40000000; // Roughly 6K x 6K words changed + + /** + * @param string[] $linesBefore + * @param string[] $linesAfter + */ + public function __construct( $linesBefore, $linesAfter ) { + + list( $wordsBefore, $wordsBeforeStripped ) = $this->split( $linesBefore ); + list( $wordsAfter, $wordsAfterStripped ) = $this->split( $linesAfter ); + + try { + parent::__construct( $wordsBeforeStripped, $wordsAfterStripped ); + } catch ( ComplexityException $ex ) { + // Too hard to diff, just show whole paragraph(s) as changed + $this->edits = [ new DiffOpChange( $linesBefore, $linesAfter ) ]; + } + + $xi = $yi = 0; + $editCount = count( $this->edits ); + for ( $i = 0; $i < $editCount; $i++ ) { + $orig = &$this->edits[$i]->orig; + if ( is_array( $orig ) ) { + $orig = array_slice( $wordsBefore, $xi, count( $orig ) ); + $xi += count( $orig ); + } + + $closing = &$this->edits[$i]->closing; + if ( is_array( $closing ) ) { + $closing = array_slice( $wordsAfter, $yi, count( $closing ) ); + $yi += count( $closing ); + } + } + + } + + /** + * @param string[] $lines + * + * @return array[] + */ + private function split( $lines ) { + + $words = []; + $stripped = []; + $first = true; + foreach ( $lines as $line ) { + if ( $first ) { + $first = false; + } else { + $words[] = "\n"; + $stripped[] = "\n"; + } + $m = []; + if ( preg_match_all( '/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs', + $line, $m ) ) { + foreach ( $m[0] as $word ) { + $words[] = $word; + } + foreach ( $m[1] as $stripped_word ) { + $stripped[] = $stripped_word; + } + } + } + + return [ $words, $stripped ]; + } + + /** + * @return string[] + */ + public function orig() { + $orig = new WordAccumulator; + + foreach ( $this->edits as $edit ) { + if ( $edit->type == 'copy' ) { + $orig->addWords( $edit->orig ); + } elseif ( $edit->orig ) { + $orig->addWords( $edit->orig, 'del' ); + } + } + $lines = $orig->getLines(); + + return $lines; + } + + /** + * @return string[] + */ + public function closing() { + $closing = new WordAccumulator; + + foreach ( $this->edits as $edit ) { + if ( $edit->type == 'copy' ) { + $closing->addWords( $edit->closing ); + } elseif ( $edit->closing ) { + $closing->addWords( $edit->closing, 'ins' ); + } + } + $lines = $closing->getLines(); + + return $lines; + } + +} diff --git a/includes/exception/BadRequestError.php b/includes/exception/BadRequestError.php new file mode 100644 index 0000000000..5fcf0e6217 --- /dev/null +++ b/includes/exception/BadRequestError.php @@ -0,0 +1,34 @@ +setStatusCode( 400 ); + parent::report(); + } +} diff --git a/includes/exception/BadTitleError.php b/includes/exception/BadTitleError.php index 3f4c2131f7..40c18a4202 100644 --- a/includes/exception/BadTitleError.php +++ b/includes/exception/BadTitleError.php @@ -20,13 +20,14 @@ /** * Show an error page on a badtitle. - * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile - * browser it is not really a valid content. + * + * Uses BadRequestError to emit a 400 HTTP error code to ensure caching proxies and + * mobile browsers know not to cache it as valid content. (T35646) * * @since 1.19 * @ingroup Exception */ -class BadTitleError extends ErrorPageError { +class BadTitleError extends BadRequestError { /** * @param string|Message|MalformedTitleException $msg A message key (default: 'badtitletext'), or * a MalformedTitleException to figure out things from @@ -45,17 +46,4 @@ class BadTitleError extends ErrorPageError { parent::__construct( 'badtitle', $msg, $params ); } } - - /** - * Just like ErrorPageError::report() but additionally set - * a 400 HTTP status code (bug 33646). - */ - public function report() { - global $wgOut; - - // bug 33646: a badtitle error page need to return an error code - // to let mobile browser now that it is not a normal page. - $wgOut->setStatusCode( 400 ); - parent::report(); - } } diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index 2a15fd7ab6..c7670785b2 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -1928,7 +1928,9 @@ class LocalFile extends File { $dbw->rollback( __METHOD__ ); } throw new LocalFileLockError( - "Could not acquire lock for '{$this->getName()}' ($waited sec)." ); + "Could not acquire lock for '{$this->getName()}' ($waited sec): " . + $status->getWikiText( false, false, 'en' ) + ); } // Release the lock *after* commit to avoid row-level contention $this->locked++; diff --git a/includes/gallery/TraditionalImageGallery.php b/includes/gallery/TraditionalImageGallery.php index f00e260e7b..2fb22815d5 100644 --- a/includes/gallery/TraditionalImageGallery.php +++ b/includes/gallery/TraditionalImageGallery.php @@ -59,6 +59,16 @@ class TraditionalImageGallery extends ImageGalleryBase { $output .= "\n\t
  • {$this->mCaption}
  • "; } + if ( $this->mShowFilename ) { + // Preload LinkCache info for when generating links + // of the filename below + $lb = new LinkBatch(); + foreach ( $this->mImages as $img ) { + $lb->addObj( $img[0] ); + } + $lb->execute(); + } + $lang = $this->getRenderLang(); # Output each image... foreach ( $this->mImages as $pair ) { @@ -176,6 +186,7 @@ class TraditionalImageGallery extends ImageGalleryBase { } $textlink = $this->mShowFilename ? + // Preloaded into LinkCache above Linker::linkKnown( $nt, htmlspecialchars( $lang->truncate( $nt->getText(), $this->mCaptionLength ) ) diff --git a/includes/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index 0dab3bb398..8ac4cf2a29 100644 --- a/includes/htmlform/HTMLForm.php +++ b/includes/htmlform/HTMLForm.php @@ -169,6 +169,8 @@ class HTMLForm extends ContextSource { protected $mShowReset = false; protected $mShowSubmit = true; protected $mSubmitFlags = [ 'constructive', 'primary' ]; + protected $mShowCancel = false; + protected $mCancelTarget; protected $mSubmitCallback; protected $mValidationErrorMessage; @@ -894,6 +896,7 @@ class HTMLForm extends ContextSource { * - id: (string, optional) DOM id for the button. * - attribs: (array, optional) Additional HTML attributes. * - flags: (string|string[], optional) OOUI flags. + * - framed: (boolean=true, optional) OOUI framed attribute. * @return HTMLForm $this for chaining calls (since 1.20) */ public function addButton( $data ) { @@ -922,6 +925,7 @@ class HTMLForm extends ContextSource { 'id' => null, 'attribs' => null, 'flags' => null, + 'framed' => true, ]; return $this; @@ -1106,6 +1110,21 @@ class HTMLForm extends ContextSource { ) . "\n"; } + if ( $this->mShowCancel ) { + $target = $this->mCancelTarget ?: Title::newMainPage(); + if ( $target instanceof Title ) { + $target = $target->getLocalURL(); + } + $buttons .= Html::element( + 'a', + [ + 'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null, + 'href' => $target, + ], + $this->msg( 'cancel' )->text() + ) . "\n"; + } + // IE<8 has bugs with