From: jenkins-bot Date: Thu, 2 Jun 2016 21:23:04 +0000 (+0000) Subject: Merge "Show ParserOutput warning instead of on the actual page output for ignored... X-Git-Tag: 1.31.0-rc.0~6721 X-Git-Url: http://git.cyclocoop.org/%40spipnet%40?a=commitdiff_plain;h=089612544da3abd89476f41476abc78b0cd98e98;hp=bacd87e4942baa34808a1b77d3b29bfdb566cc17;p=lhc%2Fweb%2Fwiklou.git Merge "Show ParserOutput warning instead of on the actual page output for ignored display titles" --- diff --git a/.stylelintrc b/.stylelintrc index e8e156708d..6b94db6722 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -11,6 +11,16 @@ "declaration-colon-space-before": [ "never" ], "font-family-name-quotes": [ "single-unless-keyword" ], - "font-weight-notation": [ "named-where-possible" ] + "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/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.28 b/RELEASE-NOTES-1.28 index e3654869c4..a650a50254 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -9,16 +9,17 @@ 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. === 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 ==== @@ -41,8 +42,8 @@ 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 fe37fe970b..27da2ca050 100644 --- a/autoload.php +++ b/autoload.php @@ -557,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', @@ -794,8 +795,6 @@ $wgAutoloadLocalClasses = [ 'MediaWikiSite' => __DIR__ . '/includes/site/MediaWikiSite.php', 'MediaWikiTitleCodec' => __DIR__ . '/includes/title/MediaWikiTitleCodec.php', 'MediaWikiVersionFetcher' => __DIR__ . '/includes/MediaWikiVersionFetcher.php', - 'MediaWiki\\Interwiki\\ClassicInterwikiLookup' => __DIR__ . '/includes/interwiki/ClassicInterwikiLookup.php', - 'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.php', 'MediaWiki\\Auth\\AbstractAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractAuthenticationProvider.php', 'MediaWiki\\Auth\\AbstractPasswordPrimaryAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPasswordPrimaryAuthenticationProvider.php', 'MediaWiki\\Auth\\AbstractPreAuthenticationProvider' => __DIR__ . '/includes/auth/AbstractPreAuthenticationProvider.php', @@ -831,9 +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', @@ -853,8 +857,8 @@ $wgAutoloadLocalClasses = [ 'MediaWiki\\Services\\CannotReplaceActiveServiceException' => __DIR__ . '/includes/Services/CannotReplaceActiveServiceException.php', 'MediaWiki\\Services\\ContainerDisabledException' => __DIR__ . '/includes/Services/ContainerDisabledException.php', 'MediaWiki\\Services\\DestructibleService' => __DIR__ . '/includes/Services/DestructibleService.php', - 'MediaWiki\\Services\\SalvageableService' => __DIR__ . '/includes/Services/SalvageableService.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', @@ -1151,7 +1155,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', @@ -1196,6 +1199,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', 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 9d2b3c2be1..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 diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c7a0c1525c..0fe3388550 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -1902,6 +1902,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 +2386,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 */ @@ -4457,7 +4465,7 @@ $wgPasswordPolicy = [ * @since 1.27 * @deprecated since 1.27, for use during development only */ -$wgDisableAuthManager = true; +$wgDisableAuthManager = false; /** * Configure AuthManager @@ -4545,9 +4553,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. */ @@ -4556,8 +4595,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 = [ @@ -7057,6 +7106,7 @@ $wgExtensionCredits = []; /** * Authentication plugin. * @var $wgAuth AuthPlugin + * @deprecated since 1.27 use $wgAuthManagerConfig instead */ $wgAuth = null; 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 8acd036039..f2403fe222 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -3501,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/Linker.php b/includes/Linker.php index 3baf865106..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' ] ); } /** @@ -1881,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 * @@ -2005,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 @@ -2034,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 4028aa2c44..6613db1602 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -11,11 +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 ObjectCache; -use ResourceLoader; use SearchEngine; use SearchEngineConfig; use SearchEngineFactory; @@ -517,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..c204aee032 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -447,12 +447,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/OutputPage.php b/includes/OutputPage.php index d8600c1ef5..6f62ae65d3 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -3101,12 +3101,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 ); } @@ -3672,7 +3666,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/ServiceWiring.php b/includes/ServiceWiring.php index 6bdacf082e..b076d07ef6 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -38,6 +38,7 @@ */ use MediaWiki\Interwiki\ClassicInterwikiLookup; +use MediaWiki\Linker\LinkRendererFactory; use MediaWiki\MediaWikiServices; return [ @@ -159,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/Setup.php b/includes/Setup.php index 5b19b5f9a4..2c78061750 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -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..d01f2693f3 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; diff --git a/includes/Title.php b/includes/Title.php index 6a5bbf71ca..4555f16d9b 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -1729,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] ); diff --git a/includes/WatchedItemStore.php b/includes/WatchedItemStore.php index f0619d69f5..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 ) { @@ -744,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/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/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/ApiMain.php b/includes/api/ApiMain.php index 60f2832320..ce9587f399 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -471,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 ); @@ -1390,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__ ); } } 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/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/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/ApiStashEdit.php b/includes/api/ApiStashEdit.php index 93003ccfc0..e739e51688 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -46,6 +46,10 @@ class ApiStashEdit extends ApiBase { $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 +127,8 @@ class ApiStashEdit extends ApiBase { $status = 'busy'; } + $this->getStats()->increment( "editstash.cache_stores.$status" ); + $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => $status ] ); } @@ -259,6 +265,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(); 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 938a61a46a..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).", @@ -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.", 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 4e9309e699..e4a2c2e0a7 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -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).", @@ -962,6 +963,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 +1084,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.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index 5d65c44be4..657fe3ee2e 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -429,6 +429,7 @@ "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.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index b6d69cd6fb..fa8aa03577 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -24,7 +24,8 @@ "Elfix", "Lbayle", "Verdy p", - "Yasten" + "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]].", @@ -64,7 +65,7 @@ "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-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.", @@ -74,6 +75,7 @@ "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).", @@ -906,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.", @@ -1018,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.", @@ -1404,7 +1408,7 @@ "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-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.", diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index be92dc2ad9..087d2e4a50 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,8 @@ "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-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 +59,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,6 +208,8 @@ "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-param-name": "Nome de usuario.", "apihelp-login-param-password": "Contrasinal", @@ -535,6 +542,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.", @@ -1032,6 +1045,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 +1146,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 +1199,14 @@ "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-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 +1275,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 +1386,9 @@ "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-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-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 c6ffe6ef1c..c85a62b8c7 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -51,7 +51,7 @@ "apihelp-clearhasmsg-example-1": "לנקות את דגל hasmsg עבור המשתמש הנוכחי.", "apihelp-clientlogin-description": "כניסה לוויקי באמצעות זרימה הידודית.", "apihelp-clientlogin-example-login": "תחילת תהליך כניסה לוויקי בתור משתמש Example עם הססמה ExamplePassword.", - "apihelp-clientlogin-example-login2": "המשך כניסה אחרי תשובת UI לאימות דו־גורמי, עם OATHToken של 987654.", + "apihelp-clientlogin-example-login2": "המשך כניסה אחרי תשובת UI לאימות דו־גורמי, עם OATHToken של 987654.", "apihelp-compare-description": "קבלת ההבדל בין 2 דפים.\n\nיש להעביר מספר גרסה, כותרת דף או מזהה דף גם ל־\"from\" וגם ל־\"to\".", "apihelp-compare-param-fromtitle": "כותרת ראשונה להשוואה.", "apihelp-compare-param-fromid": "מס׳ זיהוי של העמוד הראשון להשוואה.", @@ -61,6 +61,7 @@ "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).", @@ -893,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": "לרשום רק שמות עם רמת ההגנה הזאת.", @@ -1005,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": "הוספת חותם־הזמן של העריכה האחרונה של הדף.", @@ -1391,7 +1394,7 @@ "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-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": "תסדיר לשימוש בהחזרת הודעות.", 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/it.json b/includes/api/i18n/it.json index 6b0cab6948..47987c55ef 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,8 @@ "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-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 +55,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 +161,10 @@ "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-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 +319,10 @@ "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-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.", @@ -525,6 +540,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 +557,14 @@ "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-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 +583,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 +639,9 @@ "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-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-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/qqq.json b/includes/api/i18n/qqq.json index 6137457c1b..362a473c95 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}}", @@ -894,6 +895,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 +1008,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/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 389b218dcf..2eb6aba16d 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -58,7 +58,7 @@ "apihelp-clearhasmsg-example-1": "清除当前用户的hasmsg标记。", "apihelp-clientlogin-description": "使用交互式流登录wiki。", "apihelp-clientlogin-example-login": "开始作为用户Example和密码ExamplePassword登录至wiki的过程。", - "apihelp-clientlogin-example-login2": "在UI响应双因素验证后继续登录,补充OATHToken 987654。", + "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。", @@ -553,6 +553,9 @@ "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一起使用。", 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..402ea968e8 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 ); $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'] ); // 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,21 @@ 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 + */ + private function fillRequests( array &$reqs, $action, $username ) { + foreach ( $reqs as $req ) { + $req->action = $action; + if ( $req->username === null ) { + $req->username = $username; + } + } + } + /** * Determine whether a username exists * @param string $username @@ -2124,6 +2121,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 +2301,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 +2310,7 @@ class AuthManager implements LoggerAwareInterface { $delay = $session->delaySave(); $session->resetId(); + $session->resetAllTokens(); if ( $session->canSetUser() ) { $session->setUser( $user ); } @@ -2332,7 +2333,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/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..57f1e6bd51 100644 --- a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php +++ b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php @@ -50,7 +50,10 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen if ( !is_array( $state ) ) { return AuthenticationResponse::newAbstain(); } - $maybeLink = $state['maybeLink']; + + $maybeLink = array_filter( $state['maybeLink'], function ( $req ) { + return $this->manager->allowsAuthenticationDataChange( $req )->isGood(); + } ); if ( !$maybeLink ) { return AuthenticationResponse::newAbstain(); } @@ -134,7 +137,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/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..f48e0a5167 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 @@ -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/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/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/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/deferred/LinksDeletionUpdate.php b/includes/deferred/LinksDeletionUpdate.php index 65a8c0e0b1..d294fd2c42 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', + [ 'il_from' => $id ], + [ 'il_from', 'll_lang' ], + $batchSize + ); + $this->batchDeleteByPK( + 'iwlinks', + [ 'il_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 1f7f3b0cbb..07b5614424 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 * @@ -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 ]; } @@ -392,7 +392,7 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { 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' ); @@ -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 @@ +setBailoutComplexity( $this->bailoutComplexity ); $this->edits = $eng->diff( $from_lines, $to_lines ); } diff --git a/includes/diff/DiffEngine.php b/includes/diff/DiffEngine.php index 1853b865a6..babd00b5d7 100644 --- a/includes/diff/DiffEngine.php +++ b/includes/diff/DiffEngine.php @@ -22,6 +22,7 @@ * @file * @ingroup DifferenceEngine */ +use MediaWiki\Diff\ComplexityException; /** * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which @@ -51,6 +52,8 @@ class DiffEngine { private $tooLong; private $powLimit; + protected $bailoutComplexity = 0; + // State variables private $maxDifferences; private $lcsLengthCorrectedForHeuristic = false; @@ -71,6 +74,7 @@ class DiffEngine { * * @param string[] $from_lines * @param string[] $to_lines + * @throws ComplexityException * * @return DiffOp[] */ @@ -128,6 +132,14 @@ class DiffEngine { 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. @@ -265,6 +277,7 @@ class DiffEngine { /** * @param string[] $from * @param string[] $to + * @throws ComplexityException */ protected function diffInternal( array $from, array $to ) { // remember initial lengths @@ -323,6 +336,10 @@ class DiffEngine { $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 ) : []; diff --git a/includes/diff/WordLevelDiff.php b/includes/diff/WordLevelDiff.php index 12cf37671d..296e3b7493 100644 --- a/includes/diff/WordLevelDiff.php +++ b/includes/diff/WordLevelDiff.php @@ -23,6 +23,7 @@ * @defgroup DifferenceEngine DifferenceEngine */ +use MediaWiki\Diff\ComplexityException; use MediaWiki\Diff\WordAccumulator; /** @@ -31,7 +32,10 @@ use MediaWiki\Diff\WordAccumulator; * @ingroup DifferenceEngine */ class WordLevelDiff extends \Diff { - const MAX_LINE_LENGTH = 10000; + /** + * @inheritdoc + */ + protected $bailoutComplexity = 40000000; // Roughly 6K x 6K words changed /** * @param string[] $linesBefore @@ -42,7 +46,12 @@ class WordLevelDiff extends \Diff { list( $wordsBefore, $wordsBeforeStripped ) = $this->split( $linesBefore ); list( $wordsAfter, $wordsAfterStripped ) = $this->split( $linesAfter ); - parent::__construct( $wordsBeforeStripped, $wordsAfterStripped ); + 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 ); @@ -73,28 +82,20 @@ class WordLevelDiff extends \Diff { $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; - } + $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; } } } 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 e891c9c832..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