From: jenkins-bot Date: Fri, 10 Jun 2016 19:37:25 +0000 (+0000) Subject: Merge "Remove these two rights autoreview and torunblocked from mediawiki" X-Git-Tag: 1.31.0-rc.0~6649 X-Git-Url: http://git.cyclocoop.org/%40spipnet%40?a=commitdiff_plain;h=fab65808bd23c38b79a0d5da04df3ec55464419d;hp=5d8bb490803a45f69f8d289abe4b9eb6a118db83;p=lhc%2Fweb%2Fwiklou.git Merge "Remove these two rights autoreview and torunblocked from mediawiki" --- diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index e644ae4011..3aa5d7cce8 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -536,6 +536,8 @@ changes to languages because of Phabricator reports. * User::isPasswordReminderThrottled() was deprecated. * Bot-oriented parameters to Special:UserLogin (wpCookieCheck, wpSkipCookieCheck) were removed. +* Installer can now be customized without patching MediaWiki code, see + mw-config/overrides/README for details. == Compatibility == diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28 index d57c8ba9e0..7885f04f8f 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -9,6 +9,10 @@ production. * The load.php entry point now enforces the existing policy of not allowing access to session data, which includes the session user and the session user's language. If such access is attempted, an exception will be thrown. +* The number of internal PBKDF2 iterations used to derive the session secret + is configurable via $wgSessionPbkdf2Iterations. +* Upload dialog's file upload log comment can now be configured separately for + local and foreign uploads. === New features in 1.28 === * User::isBot() method for checking if an account is a bot role account. @@ -21,27 +25,25 @@ production. ==== New external libraries ==== - ==== Removed and replaced external libraries ==== - === Bug fixes in 1.28 === - === Action API changes in 1.28 === - === Action API internal changes in 1.28 === - === Languages updated in 1.28 === MediaWiki supports over 350 languages. Many localisations are updated regularly. Below only new and removed languages are listed, as well as changes to languages because of Phabricator reports. -=== Other changes in 1.28 === +* (T137411) ban (Balinese), thanks to translators Adi Mayndra, Andru, + BASAbali, M. Adiputra, Naval Scene, Nemo bis, NoiX180, and 아라. +=== Other changes in 1.28 === +* (T128697) Improved handling of large diffs. == Compatibility == diff --git a/autoload.php b/autoload.php index b7e3419639..f40cc89434 100644 --- a/autoload.php +++ b/autoload.php @@ -603,7 +603,7 @@ $wgAutoloadLocalClasses = [ 'InitSiteStats' => __DIR__ . '/maintenance/initSiteStats.php', 'InstallDocFormatter' => __DIR__ . '/includes/installer/InstallDocFormatter.php', 'Installer' => __DIR__ . '/includes/installer/Installer.php', - 'InstallerOverrides' => __DIR__ . '/mw-config/overrides.php', + 'InstallerOverrides' => __DIR__ . '/includes/installer/InstallerOverrides.php', 'InstallerSessionProvider' => __DIR__ . '/includes/installer/InstallerSessionProvider.php', 'Interwiki' => __DIR__ . '/includes/interwiki/Interwiki.php', 'InvalidPassword' => __DIR__ . '/includes/password/InvalidPassword.php', @@ -830,6 +830,7 @@ $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', @@ -935,7 +936,6 @@ $wgAutoloadLocalClasses = [ 'MutableConfig' => __DIR__ . '/includes/config/MutableConfig.php', 'MutableContext' => __DIR__ . '/includes/context/MutableContext.php', 'MwSql' => __DIR__ . '/maintenance/sql.php', - 'MyLocalSettingsGenerator' => __DIR__ . '/mw-config/overrides.php', 'MySQLField' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', 'MySQLMasterPos' => __DIR__ . '/includes/db/DatabaseMysqlBase.php', 'MySqlLockManager' => __DIR__ . '/includes/filebackend/lockmanager/DBLockManager.php', @@ -1198,6 +1198,7 @@ $wgAutoloadLocalClasses = [ 'SavepointPostgres' => __DIR__ . '/includes/db/DatabasePostgres.php', 'ScopedCallback' => __DIR__ . '/includes/libs/ScopedCallback.php', 'ScopedLock' => __DIR__ . '/includes/filebackend/lockmanager/ScopedLock.php', + 'SearchApi' => __DIR__ . '/includes/api/SearchApi.php', 'SearchDatabase' => __DIR__ . '/includes/search/SearchDatabase.php', 'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php', 'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php', diff --git a/composer.json b/composer.json index 31d8036d4a..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.3", + "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", diff --git a/includes/Category.php b/includes/Category.php index 6209a1a9ea..28b566a7f9 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -95,7 +95,11 @@ class Category { # and should not be kept, and 2) we *probably* don't have to scan many # rows to obtain the correct figure, so let's risk a one-time recount. if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { - $this->refreshCounts(); + $this->mPages = max( $this->mPages, 0 ); + $this->mSubcats = max( $this->mSubcats, 0 ); + $this->mFiles = max( $this->mFiles, 0 ); + + DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] ); } return true; diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 4a9d8023bf..260779768f 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -567,10 +567,14 @@ $wgUploadDialog = [ // * upload-form-label-not-own-work-local-generic-foreign 'foreign' => 'generic-foreign', ], - // Upload comment to use. Available replacements: + // Upload comments to use for 'local' and 'foreign' uploads. This can also be set to a single + // string value, in which case it is used for both kinds of uploads. Available replacements: // * $HOST - domain name from which a cross-wiki upload originates // * $PAGENAME - wiki page name from which an upload originates - 'comment' => '', + 'comment' => [ + 'local' => '', + 'foreign' => '', + ], // Format of the file page wikitext to be generated from the fields input by the user. 'format' => [ // Wrapper for the whole page. Available replacements: @@ -2386,6 +2390,13 @@ $wgSessionHandler = null; */ $wgPHPSessionHandling = 'enable'; +/** + * Number of internal PBKDF2 iterations to use when deriving session secrets. + * + * @since 1.28 + */ +$wgSessionPbkdf2Iterations = 10001; + /** * If enabled, will send MemCached debugging information to $wgDebugLogFile */ @@ -5969,6 +5980,12 @@ $wgTrxProfilerLimits = [ 'writes' => 0, 'readQueryTime' => 5 ], + // Deferred updates that run after HTTP response is sent + 'PostSend' => [ + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 500 + ], // Background job runner 'JobRunner' => [ 'readQueryTime' => 30, diff --git a/includes/Defines.php b/includes/Defines.php index d2b3443ea1..fe5083e1be 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -183,6 +183,7 @@ define( 'EDIT_SUPPRESS_RC', 8 ); define( 'EDIT_FORCE_BOT', 16 ); define( 'EDIT_DEFER_UPDATES', 32 ); // Unused since 1.27 define( 'EDIT_AUTOSUMMARY', 64 ); +define( 'EDIT_INTERNAL', 128 ); /**@}*/ /**@{ 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/MediaWiki.php b/includes/MediaWiki.php index 55f9e9ea8d..21857b9e13 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -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 @@ -762,9 +764,13 @@ class MediaWiki { // Assure deferred updates are not in the main transaction wfGetLBFactory()->commitMasterChanges( __METHOD__ ); - // Ignore things like master queries/connections on GET requests - // as long as they are in deferred updates (which catch errors). - Profiler::instance()->getTransactionProfiler()->resetExpectations(); + // Loosen DB query expectations since the HTTP client is unblocked + $trxProfiler = Profiler::instance()->getTransactionProfiler(); + $trxProfiler->resetExpectations(); + $trxProfiler->setExpectations( + $this->config->get( 'TrxProfilerLimits' )['PostSend'], + __METHOD__ + ); // Do any deferred jobs DeferredUpdates::doUpdates( 'enqueue' ); diff --git a/includes/Message.php b/includes/Message.php index c204aee032..712d3f17fb 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -384,22 +384,30 @@ class Message implements MessageSpecifier, Serializable { /** * Transform a MessageSpecifier or a primitive value used interchangeably with - * specifiers (a message key string, or a key + params array) into a proper Message + * specifiers (a message key string, or a key + params array) into a proper Message. + * + * Also accepts a MessageSpecifier inside an array: that's not considered a valid format + * but is an easy error to make due to how StatusValue stores messages internally. + * Further array elements are ignored in that case. + * * @param string|array|MessageSpecifier $value * @return Message * @throws InvalidArgumentException * @since 1.27 */ public static function newFromSpecifier( $value ) { + $params = []; + if ( is_array( $value ) ) { + $params = $value; + $value = array_shift( $params ); + } + if ( $value instanceof RawMessage ) { $message = new RawMessage( $value->getKey(), $value->getParams() ); } elseif ( $value instanceof MessageSpecifier ) { $message = new Message( $value ); - } elseif ( is_array( $value ) ) { - $key = array_shift( $value ); - $message = new Message( $key, $value ); } elseif ( is_string( $value ) ) { - $message = new Message( $value ); + $message = new Message( $value, $params ); } else { throw new InvalidArgumentException( __METHOD__ . ': invalid argument type ' . gettype( $value ) ); diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 6f62ae65d3..ad7c97603b 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -1277,15 +1277,10 @@ class OutputPage extends ContextSource { # Fetch existence plus the hiddencat property $dbr = wfGetDB( DB_SLAVE ); - $fields = [ 'page_id', 'page_namespace', 'page_title', 'page_len', - 'page_is_redirect', 'page_latest', 'pp_value' ]; - - if ( $this->getConfig()->get( 'ContentHandlerUseDB' ) ) { - $fields[] = 'page_content_model'; - } - if ( $this->getConfig()->get( 'PageLanguageUseDB' ) ) { - $fields[] = 'page_lang'; - } + $fields = array_merge( + LinkCache::getSelectFields(), + [ 'page_namespace', 'page_title', 'pp_value' ] + ); $res = $dbr->select( [ 'page', 'page_props' ], $fields, diff --git a/includes/Preferences.php b/includes/Preferences.php index 9a55ae3487..3083a8d215 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -296,7 +296,7 @@ class Preferences { $allowPasswordChange = $wgDisableAuthManager ? $wgAuth->allowPasswordChange() : AuthManager::singleton()->allowsAuthenticationDataChange( - new PasswordAuthenticationRequest(), false ); + new PasswordAuthenticationRequest(), false )->isGood(); if ( $canEditPrivateInfo && $allowPasswordChange ) { $link = Linker::link( SpecialPage::getTitleFor( 'ChangePassword' ), $context->msg( 'prefs-resetpass' )->escaped(), [], diff --git a/includes/Status.php b/includes/Status.php index d01f2693f3..45d8bed2fa 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -251,12 +251,22 @@ class Status { } /** - * Get the error list as a Message object + * Get a bullet list of the errors as a Message object. * - * @param string|string[] $shortContext A short enclosing context message name (or an array of - * message names), to be used when there is a single error. - * @param string|string[] $longContext A long enclosing context message name (or an array of - * message names), for a list. + * $shortContext and $longContext can be used to wrap the error list in some text. + * $shortContext will be preferred when there is a single error; $longContext will be + * preferred when there are multiple ones. In either case, $1 will be replaced with + * the list of errors. + * + * $shortContext is assumed to use $1 as an inline parameter: if there is a single item, + * it will not be made into a list; if there are multiple items, newlines will be inserted + * around the list. + * $longContext is assumed to use $1 as a standalone parameter; it will always receive a list. + * + * If both parameters are missing, and there is only one error, no bullet will be added. + * + * @param string|string[] $shortContext A message name or an array of message names. + * @param string|string[] $longContext A message name or an array of message names. * @param string|Language $lang Language to use for processing messages * @return Message */ @@ -287,10 +297,6 @@ class Status { $msgs = $this->getErrorMessageArray( $rawErrors, $lang ); $msgCount = count( $msgs ); - if ( $shortContext ) { - $msgCount++; - } - $s = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) ); $s->params( $msgs )->parse(); diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index b5f7ff2536..7be2aa7566 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -478,16 +478,18 @@ class InfoAction extends FormlessAction { if ( $firstRev ) { $firstRevUser = $firstRev->getUserText( Revision::FOR_THIS_USER ); if ( $firstRevUser !== '' ) { - $batch->add( NS_USER, $firstRevUser ); - $batch->add( NS_USER_TALK, $firstRevUser ); + $firstRevUserTitle = Title::makeTitle( NS_USER, $firstRevUser ); + $batch->addObj( $firstRevUserTitle ); + $batch->addObj( $firstRevUserTitle->getTalkPage() ); } } if ( $lastRev ) { $lastRevUser = $lastRev->getUserText( Revision::FOR_THIS_USER ); if ( $lastRevUser !== '' ) { - $batch->add( NS_USER, $lastRevUser ); - $batch->add( NS_USER_TALK, $lastRevUser ); + $lastRevUserTitle = Title::makeTitle( NS_USER, $lastRevUser ); + $batch->addObj( $lastRevUserTitle ); + $batch->addObj( $lastRevUserTitle->getTalkPage() ); } } diff --git a/includes/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/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php index 54547efe4b..aea28195f0 100644 --- a/includes/api/ApiChangeAuthenticationData.php +++ b/includes/api/ApiChangeAuthenticationData.php @@ -56,6 +56,7 @@ class ApiChangeAuthenticationData extends ApiBase { // Make the change $status = $manager->allowsAuthenticationDataChange( $req, true ); + Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] ); if ( !$status->isGood() ) { $this->dieStatus( $status ); } diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php index 711234a65b..cffccb15b5 100644 --- a/includes/api/ApiClientLogin.php +++ b/includes/api/ApiClientLogin.php @@ -23,6 +23,7 @@ use MediaWiki\Auth\AuthManager; use MediaWiki\Auth\AuthenticationRequest; use MediaWiki\Auth\AuthenticationResponse; +use MediaWiki\Auth\CreateFromLoginAuthenticationRequest; /** * Log in to the wiki with AuthManager @@ -90,6 +91,13 @@ class ApiClientLogin extends ApiBase { $res = $manager->beginAuthentication( $reqs, $params['returnurl'] ); } + // Remove CreateFromLoginAuthenticationRequest from $res->neededRequests. + // It's there so a RESTART treated as UI will work right, but showing + // it to the API client is just confusing. + $res->neededRequests = ApiAuthManagerHelper::blacklistAuthenticationRequests( + $res->neededRequests, [ CreateFromLoginAuthenticationRequest::class ] + ); + $this->getResult()->addValue( null, 'clientlogin', $helper->formatAuthenticationResponse( $res ) ); } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index e28b0684d5..c7dc303ada 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -79,6 +79,7 @@ class ApiFeedContributions extends ApiBase { 'deletedOnly' => $params['deletedonly'], 'topOnly' => $params['toponly'], 'newOnly' => $params['newonly'], + 'hideMinor' => $params['hideminor'], 'showSizeDiff' => $params['showsizediff'], ] ); @@ -208,6 +209,7 @@ class ApiFeedContributions extends ApiBase { 'deletedonly' => false, 'toponly' => false, 'newonly' => false, + 'hideminor' => false, 'showsizediff' => [ ApiBase::PARAM_DFLT => false, ], diff --git a/includes/api/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/ApiQueryAuthManagerInfo.php b/includes/api/ApiQueryAuthManagerInfo.php index b591f9c00a..1d250e97d1 100644 --- a/includes/api/ApiQueryAuthManagerInfo.php +++ b/includes/api/ApiQueryAuthManagerInfo.php @@ -43,7 +43,6 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { 'canauthenticatenow' => $manager->canAuthenticateNow(), 'cancreateaccounts' => $manager->canCreateAccounts(), 'canlinkaccounts' => $manager->canLinkAccounts(), - 'haspreservedstate' => $helper->getPreservedRequest() !== null, ]; if ( $params['securitysensitiveoperation'] !== null ) { @@ -53,10 +52,27 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { } if ( $params['requestsfor'] ) { - $reqs = $manager->getAuthenticationRequests( $params['requestsfor'], $this->getUser() ); + $action = $params['requestsfor']; + + $preservedReq = $helper->getPreservedRequest(); + if ( $preservedReq ) { + $ret += [ + 'haspreservedstate' => $preservedReq->hasStateForAction( $action ), + 'hasprimarypreservedstate' => $preservedReq->hasPrimaryStateForAction( $action ), + 'preservedusername' => (string)$preservedReq->username, + ]; + } else { + $ret += [ + 'haspreservedstate' => false, + 'hasprimarypreservedstate' => false, + 'preservedusername' => '', + ]; + } + + $reqs = $manager->getAuthenticationRequests( $action, $this->getUser() ); // Filter out blacklisted requests, depending on the action - switch ( $params['requestsfor'] ) { + switch ( $action ) { case AuthManager::ACTION_CHANGE: $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests( $reqs, $this->getConfig()->get( 'ChangeCredentialsBlacklist' ) @@ -75,8 +91,8 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { $this->getResult()->addValue( [ 'query' ], $this->getModuleName(), $ret ); } - public function getCacheMode( $params ) { - return 'public'; + public function isReadMode() { + return false; } public function getAllowedParams() { @@ -95,7 +111,7 @@ class ApiQueryAuthManagerInfo extends ApiQueryBase { AuthManager::ACTION_UNLINK, ], ], - ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields' ); + ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields', 'messageformat' ); } protected function getExamplesMessages() { diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php index 5c50273261..46538e0eb1 100644 --- a/includes/api/ApiQueryPrefixSearch.php +++ b/includes/api/ApiQueryPrefixSearch.php @@ -25,6 +25,11 @@ use MediaWiki\MediaWikiServices; * @ingroup API */ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { + use SearchApi; + + /** @var array list of api allowed params */ + private $allowedParams; + public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ps' ); } @@ -44,12 +49,9 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; - $namespaces = $params['namespace']; $offset = $params['offset']; - $searchEngine = MediaWikiServices::getInstance()->newSearchEngine(); - $searchEngine->setLimitOffset( $limit + 1, $offset ); - $searchEngine->setNamespaces( $namespaces ); + $searchEngine = $this->buildSearchEngine( $params ); $titles = $searchEngine->extractTitles( $searchEngine->completionSearchWithVariants( $search ) ); if ( $resultPageSet ) { @@ -60,7 +62,7 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { return $current; } ); if ( count( $titles ) > $limit ) { - $this->setContinueEnumParameter( 'offset', $offset + $params['limit'] ); + $this->setContinueEnumParameter( 'offset', $offset + $limit ); array_pop( $titles ); } $resultPageSet->populateFromTitles( $titles ); @@ -72,7 +74,7 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { $count = 0; foreach ( $titles as $title ) { if ( ++$count > $limit ) { - $this->setContinueEnumParameter( 'offset', $offset + $params['limit'] ); + $this->setContinueEnumParameter( 'offset', $offset + $limit ); break; } $vals = [ @@ -101,29 +103,45 @@ class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - return [ - 'search' => [ - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true, - ], - 'namespace' => [ - ApiBase::PARAM_DFLT => NS_MAIN, - ApiBase::PARAM_TYPE => 'namespace', - ApiBase::PARAM_ISMULTI => true, - ], - 'limit' => [ - ApiBase::PARAM_DFLT => 10, - ApiBase::PARAM_TYPE => 'limit', - ApiBase::PARAM_MIN => 1, - // Non-standard value for compatibility with action=opensearch - ApiBase::PARAM_MAX => 100, - ApiBase::PARAM_MAX2 => 200, - ], - 'offset' => [ - ApiBase::PARAM_DFLT => 0, - ApiBase::PARAM_TYPE => 'integer', - ], - ]; + if ( $this->allowedParams !== null ) { + return $this->allowedParams; + } + $this->allowedParams = [ + 'search' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ], + 'namespace' => [ + ApiBase::PARAM_DFLT => NS_MAIN, + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_ISMULTI => true, + ], + 'limit' => [ + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + // Non-standard value for compatibility with action=opensearch + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 200, + ], + 'offset' => [ + ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_TYPE => 'integer', + ], + ]; + $profileParam = $this->buildProfileApiParam( SearchEngine::COMPLETION_PROFILE_TYPE, + 'apihelp-query+prefixsearch-param-profile' ); + if ( $profileParam ) { + $this->allowedParams['profile'] = $profileParam; + } + return $this->allowedParams; + } + + public function getSearchProfileParams() { + if ( isset( $this->getAllowedParams()['profile'] ) ) { + return [ SearchEngine::COMPLETION_PROFILE_TYPE => 'profile' ]; + } + return []; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 64022ff2fa..b816f43842 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -80,8 +80,7 @@ class ApiQueryRevisions extends ApiQueryRevisionsBase { return false; } - return $wgUser->getEditToken( - [ $title->getPrefixedText(), $rev->getUserText() ] ); + return $wgUser->getEditToken( 'rollback' ); } protected function run( ApiPageSet $resultPageSet = null ) { diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index f57d3a30cf..80798a10cd 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -32,6 +32,10 @@ use MediaWiki\MediaWikiServices; * @ingroup API */ class ApiQuerySearch extends ApiQueryGeneratorBase { + use SearchApi; + + /** @var array list of api allowed params */ + private $allowedParams; /** * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't @@ -61,8 +65,11 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { global $wgContLang; $params = $this->extractRequestParams(); + if ( isset( $params['backend'] ) && $params['backend'] == self::BACKEND_NULL_PARAM ) { + unset( $params['backend'] ); + } + // Extract parameters - $limit = $params['limit']; $query = $params['search']; $what = $params['what']; $interwiki = $params['interwiki']; @@ -80,11 +87,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } // Create search engine instance and set options - $type = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? - $params['backend'] : null; - $search = MediaWikiServices::getInstance()->getSearchEngineFactory()->create( $type ); - $search->setLimitOffset( $limit + 1, $params['offset'] ); - $search->setNamespaces( $params['namespace'] ); + $search = $this->buildSearchEngine( $params ); $search->setFeatureData( 'rewrite', (bool)$params['enablerewrites'] ); $query = $search->transformSearchTerm( $query ); @@ -152,6 +155,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $titles = []; $count = 0; $result = $matches->next(); + $limit = $params['limit']; while ( $result ) { if ( ++$count > $limit ) { @@ -301,7 +305,11 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - $params = [ + if ( $this->allowedParams !== null ) { + return $this->allowedParams; + } + + $this->allowedParams = [ 'search' => [ ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true @@ -368,13 +376,31 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { if ( $alternatives[0] === null ) { $alternatives[0] = self::BACKEND_NULL_PARAM; } - $params['backend'] = [ + $this->allowedParams['backend'] = [ ApiBase::PARAM_DFLT => $searchConfig->getSearchType(), ApiBase::PARAM_TYPE => $alternatives, ]; + // @todo: support profile selection when multiple + // backends are available. The solution could be to + // merge all possible profiles and let ApiBase + // subclasses do the check. Making ApiHelp and ApiSandbox + // comprehensive might be more difficult. + } else { + $profileParam = $this->buildProfileApiParam( SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE, + 'apihelp-query+search-param-qiprofile' ); + if ( $profileParam ) { + $this->allowedParams['qiprofile'] = $profileParam; + } } - return $params; + return $this->allowedParams; + } + + public function getSearchProfileParams() { + if ( isset( $this->getAllowedParams()['qiprofile'] ) ) { + return [ SearchEngine::FT_QUERY_INDEP_PROFILE_TYPE => 'qiprofile' ]; + } + return []; } protected function getExamplesMessages() { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 0774651c11..590a71265e 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -245,7 +245,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['uploadsenabled'] = UploadBase::isEnabled(); $data['maxuploadsize'] = UploadBase::getMaxUploadSize(); - $data['minuploadchunksize'] = (int)$this->getConfig()->get( 'MinUploadChunkSize' ); + $data['minuploadchunksize'] = (int)$config->get( 'MinUploadChunkSize' ); $data['thumblimits'] = $config->get( 'ThumbLimits' ); ApiResult::setArrayType( $data['thumblimits'], 'BCassoc' ); @@ -264,10 +264,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['favicon'] = wfExpandUrl( $favicon, PROTO_RELATIVE ); } - $data['centralidlookupprovider'] = $this->getConfig()->get( 'CentralIdLookupProvider' ); - $providerIds = array_keys( $this->getConfig()->get( 'CentralIdLookupProviders' ) ); + $data['centralidlookupprovider'] = $config->get( 'CentralIdLookupProvider' ); + $providerIds = array_keys( $config->get( 'CentralIdLookupProviders' ) ); $data['allcentralidlookupproviders'] = $providerIds; + $data['interwikimagic'] = (bool)$config->get( 'InterwikiMagic' ); + Hooks::run( 'APIQuerySiteInfoGeneralInfo', [ $this, &$data ] ); return $this->getResult()->addValue( 'query', $property, $data ); diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 68ec38dd9f..5afb66f225 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -262,8 +262,11 @@ class ApiQueryUsers extends ApiQueryBase { } else { $data[$u]['missing'] = true; if ( isset( $this->prop['cancreate'] ) && !$this->getConfig()->get( 'DisableAuthManager' ) ) { - $data[$u]['cancreate'] = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u ) - ->isGood(); + $status = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u ); + $data[$u]['cancreate'] = $status->isGood(); + if ( !$status->isGood() ) { + $data[$u]['cancreateerror'] = $this->getErrorFormatter()->arrayFromStatus( $status ); + } } } } else { diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php index 30e40fb2b6..d72c8a407e 100644 --- a/includes/api/ApiRemoveAuthenticationData.php +++ b/includes/api/ApiRemoveAuthenticationData.php @@ -73,6 +73,7 @@ class ApiRemoveAuthenticationData extends ApiBase { // Perform the removal $status = $manager->allowsAuthenticationDataChange( $req, true ); + Hooks::run( 'ChangeAuthenticationDataAudit', [ $req, $status ] ); if ( !$status->isGood() ) { $this->dieStatus( $status ); } diff --git a/includes/api/ApiStashEdit.php b/includes/api/ApiStashEdit.php index e739e51688..dd911d0ff9 100644 --- a/includes/api/ApiStashEdit.php +++ b/includes/api/ApiStashEdit.php @@ -41,6 +41,7 @@ class ApiStashEdit extends ApiBase { const ERROR_UNCACHEABLE = 'uncacheable'; const PRESUME_FRESH_TTL_SEC = 30; + const MAX_CACHE_TTL = 300; // 5 minutes public function execute() { $user = $this->getUser(); @@ -145,14 +146,15 @@ class ApiStashEdit extends ApiBase { $format = $content->getDefaultFormat(); $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false ); + $title = $page->getTitle(); if ( $editInfo && $editInfo->output ) { - $key = self::getStashKey( $page->getTitle(), $content, $user ); + $key = self::getStashKey( $title, $content, $user ); // Let extensions add ParserOutput metadata or warm other caches Hooks::run( 'ParserOutputStashForEdit', [ $page, $content, $editInfo->output ] ); - list( $stashInfo, $ttl ) = self::buildStashValue( + list( $stashInfo, $ttl, $code ) = self::buildStashValue( $editInfo->pstContent, $editInfo->output, $editInfo->timestamp, @@ -162,14 +164,14 @@ class ApiStashEdit extends ApiBase { if ( $stashInfo ) { $ok = $cache->set( $key, $stashInfo, $ttl ); if ( $ok ) { - $logger->debug( "Cached parser output for key '$key'." ); + $logger->debug( "Cached parser output for key '$key' ('$title')." ); return self::ERROR_NONE; } else { - $logger->error( "Failed to cache parser output for key '$key'." ); + $logger->error( "Failed to cache parser output for key '$key' ('$title')." ); return self::ERROR_CACHE; } } else { - $logger->info( "Uncacheable parser output for key '$key'." ); + $logger->info( "Uncacheable parser output for key '$key' ('$title') [$code]." ); return self::ERROR_UNCACHEABLE; } } @@ -215,7 +217,8 @@ class ApiStashEdit extends ApiBase { // PST parser options are for the user (handles signatures, etc...) $user = $pstOpts->getUser(); // Get a key based on the source text, format, and user preferences - $key = self::getStashKey( $page->getTitle(), $content, $user ); + $title = $page->getTitle(); + $key = self::getStashKey( $title, $content, $user ); // Parser output options must match cannonical options. // Treat some options as matching that are different but don't matter. @@ -223,7 +226,7 @@ class ApiStashEdit extends ApiBase { $canonicalPOpts->setIsPreview( true ); // force match $canonicalPOpts->setTimestamp( $pOpts->getTimestamp() ); // force match if ( !$pOpts->matches( $canonicalPOpts ) ) { - $logger->info( "Uncacheable preview output for key '$key' (options)." ); + $logger->info( "Uncacheable preview output for key '$key' ('$title') [options]." ); return false; } @@ -233,13 +236,13 @@ class ApiStashEdit extends ApiBase { // Build a value to cache with a proper TTL list( $stashInfo, $ttl ) = self::buildStashValue( $pstContent, $pOut, $timestamp, $user ); if ( !$stashInfo ) { - $logger->info( "Uncacheable parser output for key '$key' (rev/TTL)." ); + $logger->info( "Uncacheable parser output for key '$key' ('$title') [rev/TTL]." ); return false; } $ok = $cache->set( $key, $stashInfo, $ttl ); if ( !$ok ) { - $logger->error( "Failed to cache preview parser output for key '$key'." ); + $logger->error( "Failed to cache preview parser output for key '$key' ('$title')." ); } else { $logger->debug( "Cached preview output for key '$key'." ); } @@ -294,7 +297,7 @@ class ApiStashEdit extends ApiBase { if ( !is_object( $editInfo ) || !$editInfo->output ) { $stats->increment( 'editstash.cache_misses.no_stash' ); - $logger->debug( "No cache value for key '$key'." ); + $logger->debug( "Empty cache for key '$key' ('$title'); user '{$user->getName()}'." ); return false; } @@ -317,64 +320,10 @@ class ApiStashEdit extends ApiBase { return $editInfo; } - $dbr = wfGetDB( DB_SLAVE ); - - $templates = []; // conditions to find changes/creations - $templateUses = 0; // expected existing templates - foreach ( $editInfo->output->getTemplateIds() as $ns => $stuff ) { - foreach ( $stuff as $dbkey => $revId ) { - $templates[(string)$ns][$dbkey] = (int)$revId; - ++$templateUses; - } - } - // Check that no templates used in the output changed... - if ( count( $templates ) ) { - $res = $dbr->select( - 'page', - [ 'ns' => 'page_namespace', 'dbk' => 'page_title', 'page_latest' ], - $dbr->makeWhereFrom2d( $templates, 'page_namespace', 'page_title' ), - __METHOD__ - ); - $changed = false; - foreach ( $res as $row ) { - $changed = $changed || ( $row->page_latest != $templates[$row->ns][$row->dbk] ); - } - - if ( $changed || $res->numRows() != $templateUses ) { - $stats->increment( 'editstash.cache_misses.proven_stale' ); - $logger->info( "Stale cache for key '$key'; template changed. (age: $age sec)" ); - return false; - } - } + $stats->increment( 'editstash.cache_misses.proven_stale' ); + $logger->info( "Stale cache for key '$key'; old key with outside edits. (age: $age sec)" ); - $files = []; // conditions to find changes/creations - foreach ( $editInfo->output->getFileSearchOptions() as $name => $options ) { - $files[$name] = (string)$options['sha1']; - } - // Check that no files used in the output changed... - if ( count( $files ) ) { - $res = $dbr->select( - 'image', - [ 'name' => 'img_name', 'img_sha1' ], - [ 'img_name' => array_keys( $files ) ], - __METHOD__ - ); - $changed = false; - foreach ( $res as $row ) { - $changed = $changed || ( $row->img_sha1 != $files[$row->name] ); - } - - if ( $changed || $res->numRows() != count( $files ) ) { - $stats->increment( 'editstash.cache_misses.proven_stale' ); - $logger->info( "Stale cache for key '$key'; file changed. (age: $age sec)" ); - return false; - } - } - - $stats->increment( 'editstash.cache_hits.proven_fresh' ); - $logger->debug( "Verified cache hit for key '$key' (age: $age sec)." ); - - return $editInfo; + return false; } /** @@ -406,11 +355,13 @@ class ApiStashEdit extends ApiBase { */ private static function getStashKey( Title $title, Content $content, User $user ) { $hash = sha1( implode( ':', [ + // Account for the edit model/text $content->getModel(), $content->getDefaultFormat(), sha1( $content->serialize( $content->getDefaultFormat() ) ), - $user->getId() ?: md5( $user->getName() ), // account for user parser options - $user->getId() ? $user->getDBTouched() : '-' // handle preference change races + // Account for user name related variables like signatures + $user->getId(), + md5( $user->getName() ) ] ) ); return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash ); @@ -425,7 +376,7 @@ class ApiStashEdit extends ApiBase { * @param ParserOutput $parserOutput * @param string $timestamp TS_MW * @param User $user - * @return array (stash info array, TTL in seconds) or (null, 0) + * @return array (stash info array, TTL in seconds, info code) or (null, 0, info code) */ private static function buildStashValue( Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user @@ -433,20 +384,22 @@ class ApiStashEdit extends ApiBase { // If an item is renewed, mind the cache TTL determined by config and parser functions. // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness. $since = time() - wfTimestamp( TS_UNIX, $parserOutput->getTimestamp() ); - $ttl = min( $parserOutput->getCacheExpiry() - $since, 5 * 60 ); - - if ( $ttl > 0 && !$parserOutput->getFlag( 'vary-revision' ) ) { - // Only store what is actually needed - $stashInfo = (object)[ - 'pstContent' => $pstContent, - 'output' => $parserOutput, - 'timestamp' => $timestamp, - 'edits' => $user->getEditCount() - ]; - return [ $stashInfo, $ttl ]; + $ttl = min( $parserOutput->getCacheExpiry() - $since, self::MAX_CACHE_TTL ); + if ( $ttl <= 0 ) { + return [ null, 0, 'no_ttl' ]; + } elseif ( $parserOutput->getFlag( 'vary-revision' ) ) { + return [ null, 0, 'vary_revision' ]; } - return [ null, 0 ]; + // Only store what is actually needed + $stashInfo = (object)[ + 'pstContent' => $pstContent, + 'output' => $parserOutput, + 'timestamp' => $timestamp, + 'edits' => $user->getEditCount() + ]; + + return [ $stashInfo, $ttl, 'ok' ]; } public function getAllowedParams() { 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/en.json b/includes/api/i18n/en.json index 4e9309e699..82a83496c6 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).", @@ -154,6 +155,7 @@ "apihelp-feedcontributions-param-deletedonly": "Show only deleted contributions.", "apihelp-feedcontributions-param-toponly": "Only show edits that are latest revisions.", "apihelp-feedcontributions-param-newonly": "Only show edits that are page creations.", + "apihelp-feedcontributions-param-hideminor": "Hide minor edits.", "apihelp-feedcontributions-param-showsizediff": "Show the size difference between revisions.", "apihelp-feedcontributions-example-simple": "Return contributions for user Example.", @@ -962,6 +964,7 @@ "apihelp-query+prefixsearch-param-limit": "Maximum number of results to return.", "apihelp-query+prefixsearch-param-offset": "Number of results to skip.", "apihelp-query+prefixsearch-example-simple": "Search for page titles beginning with meaning.", + "apihelp-query+prefixsearch-param-profile": "Search profile to use.", "apihelp-query+protectedtitles-description": "List all titles protected from creation.", "apihelp-query+protectedtitles-param-namespace": "Only list titles in these namespaces.", @@ -1082,6 +1085,7 @@ "apihelp-query+search-param-what": "Which type of search to perform.", "apihelp-query+search-param-info": "Which metadata to return.", "apihelp-query+search-param-prop": "Which properties to return:", + "apihelp-query+search-param-qiprofile": "Query independent profile to use (affects ranking algorithm).", "apihelp-query+search-paramvalue-prop-size": "Adds the size of the page in bytes.", "apihelp-query+search-paramvalue-prop-wordcount": "Adds the word count of the page.", "apihelp-query+search-paramvalue-prop-timestamp": "Adds the timestamp of when the page was last edited.", diff --git a/includes/api/i18n/es.json b/includes/api/i18n/es.json index 657fe3ee2e..10f6c7f537 100644 --- a/includes/api/i18n/es.json +++ b/includes/api/i18n/es.json @@ -20,7 +20,8 @@ "Lemondoge", "Mgpena", "Rubentl134", - "2axterix2" + "2axterix2", + "Dgstranz" ] }, "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentación]]\n* [[mw:API:FAQ|Preguntas frecuentes]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de correos]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce API de anuncios]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Errores y peticiones]\n
\nEstado: Todas las características que se muestran en esta página debería funcionar, pero la API aún está en desarrollo activo y puede cambiar en cualquier momento. Suscríbete a [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ la lista de correo de mediawiki-api-announce] para estar al día de las actualizaciones.\n\nSolicitudes erróneas: Cuando se envían solicitudes erróneas a la API, se envía un encabezado HTTP con la clave \"MediaWiki-API-Error\" y ambos valores, del encabezado y el código de error, se establecerán en el mismo valor. Para más información, véase [[mw:API:Errors_and_warnings|API: Errores y advertencias]].\n\nPruebas: para facilitar las pruebas de solicitudes a la API, consulta [[Special:ApiSandbox]].", @@ -130,6 +131,7 @@ "apihelp-expandtemplates-paramvalue-prop-ttl": "El tiempo máximo tras el cual deberían invalidarse los resultados en caché.", "apihelp-expandtemplates-paramvalue-prop-jsconfigvars": "Da las variables de configuración JavaScript específicas para la página.", "apihelp-expandtemplates-paramvalue-prop-encodedjsconfigvars": "Da las variables de configuración JavaScript específicas para la página como una cadena JSON.", + "apihelp-expandtemplates-param-includecomments": "Incluir o no los comentarios HTML en la salida.", "apihelp-expandtemplates-param-generatexml": "Generar un árbol de análisis XML (remplazado por $1prop=parsetree).", "apihelp-expandtemplates-example-simple": "Expandir el wikitexto {{Project:Sandbox}}.", "apihelp-feedcontributions-description": "Devuelve el canal de contribuciones de un usuario.", @@ -308,6 +310,7 @@ "apihelp-protect-example-protect": "Proteger una página", "apihelp-protect-example-unprotect": "Desproteger una página estableciendo la restricción a all.", "apihelp-protect-example-unprotect2": "Desproteger una página anulando las restricciones.", + "apihelp-purge-description": "Purgar la caché de los títulos proporcionados.\n\nSe requiere una solicitud POST si el usuario no ha iniciado sesión.", "apihelp-purge-param-forcelinkupdate": "Actualizar las tablas de enlaces.", "apihelp-purge-param-forcerecursivelinkupdate": "Actualizar la tabla de enlaces y todas las tablas de enlaces de cualquier página que use esta página como una plantilla.", "apihelp-purge-example-simple": "Purgar la Main Page y la página API.", @@ -415,6 +418,7 @@ "apihelp-query+mystashedfiles-param-limit": "Cuántos archivos obtener.", "apihelp-query+alltransclusions-param-prefix": "Buscar todos los títulos transcluidos que comiencen con este valor.", "apihelp-query+alltransclusions-param-prop": "Qué piezas de información incluir:", + "apihelp-query+alltransclusions-paramvalue-prop-title": "Añade el título de la transclusión.", "apihelp-query+alltransclusions-example-unique": "Listar títulos transcluidos de forma única.", "apihelp-query+alltransclusions-example-unique-generator": "Obtiene todos los títulos transcluidos, marcando los que faltan.", "apihelp-query+allusers-description": "Enumerar todos los usuarios registrados.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index 44725a8f54..fa8aa03577 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -75,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).", @@ -907,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.", @@ -1019,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.", diff --git a/includes/api/i18n/gl.json b/includes/api/i18n/gl.json index 087d2e4a50..a7ecabd63a 100644 --- a/includes/api/i18n/gl.json +++ b/includes/api/i18n/gl.json @@ -50,6 +50,7 @@ "apihelp-clearhasmsg-example-1": "Limpar a bandeira hasmsg para o usuario actual", "apihelp-clientlogin-description": "Conectarse á wiki usando o fluxo interactivo.", "apihelp-clientlogin-example-login": "Comezar o proceso de conexión á wiki como o usuario Exemplo con contrasinal ExemploContrasinal.", + "apihelp-clientlogin-example-login2": "Continuar a conexión despois dunha resposta de UI para unha autenticación de dous factores, proporcionando un OATHToken con valor 987654.", "apihelp-compare-description": "Obter as diferencias entre dúas páxinas.\n\nDebe indicar un número de revisión, un título de páxina, ou un ID de páxina tanto para \"from\" como para \"to\".", "apihelp-compare-param-fromtitle": "Primeiro título para comparar.", "apihelp-compare-param-fromid": "Identificador da primeira páxina a comparar.", @@ -211,6 +212,8 @@ "apihelp-linkaccount-description": "Vincular unha conta dun provedor externo ó usuario actual.", "apihelp-linkaccount-example-link": "Comezar o proceso de vincular a unha conta de Exemplo.", "apihelp-login-description": "No caso dunha conexión correcta, as cookies necesarias incluiranse nas cabeceiras HTTP de resposta. No caso dunha conexión fallida, os intentos posteriores poden ser reducidos para limitar ataques automaticos de roubo de contrasinais.", + "apihelp-login-description-nobotpasswords": "Conectarse e obter as cookies de autenticación. \n\nEsta acción está obsoleta e pode fallar sen avisar. Para conectarse sen problema use [[Special:ApiHelp/clientlogin|action=clientlogin]].", + "apihelp-login-description-nonauthmanager": "Conectarse e obter as cookies de autenticación. \n\nNo caso dunha conexión correcta, as cookies necesarias incluiranse nas cabeceiras HTTP de resposta. No caso dunha conexión fallida, os intentos posteriores poden ser reducidos para limitar ataques automaticos de roubo de contrasinais.", "apihelp-login-param-name": "Nome de usuario.", "apihelp-login-param-password": "Contrasinal", "apihelp-login-param-domain": "Dominio (opcional).", @@ -889,6 +892,7 @@ "apihelp-query+prefixsearch-param-limit": "Número máximo de resultados a visualizar.", "apihelp-query+prefixsearch-param-offset": "Número de resultados a saltar.", "apihelp-query+prefixsearch-example-simple": "Buscar títulos de páxina que comecen con meaning.", + "apihelp-query+prefixsearch-param-profile": "Buscar o perfil a usar.", "apihelp-query+protectedtitles-description": "Listar todos os títulos protexidos en creación.", "apihelp-query+protectedtitles-param-namespace": "Só listar títulos nestes espazos de nomes.", "apihelp-query+protectedtitles-param-level": "Só listar títulos con estos niveis de protección.", @@ -1001,6 +1005,7 @@ "apihelp-query+search-param-what": "Que tipo de busca lanzar.", "apihelp-query+search-param-info": "Que metadatos devolver.", "apihelp-query+search-param-prop": "Que propiedades devolver:", + "apihelp-query+search-param-qiprofile": "Perfil independente das consultas a usar (afecta ó algoritmo de clasificación).", "apihelp-query+search-paramvalue-prop-size": "Engade o tamaño da páxina en bytes.", "apihelp-query+search-paramvalue-prop-wordcount": "Engade o número de palabras da páxina.", "apihelp-query+search-paramvalue-prop-timestamp": "Engade o selo de tempo da última vez que foi editada a páxina.", @@ -1202,6 +1207,7 @@ "apihelp-removeauthenticationdata-description": "Elimina os datos de autenticación do usuario actual.", "apihelp-removeauthenticationdata-example-simple": "Intenta eliminar os datos de usuario actual para FooAuthenticationRequest.", "apihelp-resetpassword-description": "Envía un correo de inicialización de contrasinal a un usuario.", + "apihelp-resetpassword-description-noroutes": "Non están dispoñibles as rutas de reinicio de contrasinal \n\nActive as rutas en [[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]] para usar este módulo.", "apihelp-resetpassword-param-user": "Usuario sendo reinicializado.", "apihelp-resetpassword-param-email": "Está reinicializándose o enderezo de correo electrónico do usuario.", "apihelp-resetpassword-param-capture": "Devolve os contrasinais temporais que se enviaron. Require o dereito de usuario passwordreset .", @@ -1386,9 +1392,12 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|Concedida a|Concedidas a}}: $2", "api-help-right-apihighlimits": "Usar os valores superiores das consultas da API (consultas lentas: $1; consultas rápidas: $2). Os límites para as consultas lentas tamén se aplican ós parámetros multivaluados.", "api-help-open-in-apisandbox": "[abrir en zona de probas]", + "api-help-authmanagerhelper-requests": "Só usar estas peticións de autenticación, co id devolto por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1 ou dunha resposta previa deste módulo.", + "api-help-authmanagerhelper-request": "Usar esta petición de autenticación, co id devolto por [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1.", "api-help-authmanagerhelper-messageformat": "Formato a usar para devolver as mensaxes.", "api-help-authmanagerhelper-mergerequestfields": "Fusionar os campos de información para todas as peticións de autenticación nunha táboa.", "api-help-authmanagerhelper-preservestate": "Conservar o estado dun intento previo de conexión fallida, se é posible.", + "api-help-authmanagerhelper-continue": "Esta petición é unha continucación despois dun resposta precedente UI ou REDIRECT. Esta ou $1returnurl é requirida.", "api-credits-header": "Créditos", "api-credits": "Desenvolvedores da API:\n* Roan Kattouw (desenvolvedor principal, set. 2007-2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (creador e desenvolvedor principal, set. 2006-sep. 2007)\n* Brad Jorsch (desenvolvedor principal, 2013-actualidade)\n\nEnvía comentarios, suxerencias e preguntas a mediawiki-api@lists.wikimedia.org\nou informa dun erro en https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/he.json b/includes/api/i18n/he.json index 080e0ddbe3..c85a62b8c7 100644 --- a/includes/api/i18n/he.json +++ b/includes/api/i18n/he.json @@ -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": "הוספת חותם־הזמן של העריכה האחרונה של הדף.", diff --git a/includes/api/i18n/ia.json b/includes/api/i18n/ia.json index 7a7675d025..147a832a73 100644 --- a/includes/api/i18n/ia.json +++ b/includes/api/i18n/ia.json @@ -26,6 +26,7 @@ "apihelp-checktoken-param-type": "Typo de indicio a testar.", "apihelp-checktoken-param-token": "Indicio a testar.", "apihelp-createaccount-param-name": "Nomine de usator.", + "apihelp-query+prefixsearch-param-profile": "Le profilo de recerca a usar.", "apihelp-query+revisions-example-first5-not-localhost": "Obtener le prime 5 versiones del \"Pagina principal\" que non ha essite facite per le usator anonyme \"127.0.0.1\"", "api-credits": "Programmatores del API:\n* Roan Kattouw (programmator dirigente Sept. 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Yuri Astrakhan (creator, programmator dirigente Sept. 2006–Sept. 2007)\n* Brad Jorsch (programmator dirigente 2013–presente)\n\nInvia tu commentos, suggestiones e questiones a mediawiki-api@lists.wikimedia.org\no insere un reportage de bug a https://phabricator.wikimedia.org/." } diff --git a/includes/api/i18n/id.json b/includes/api/i18n/id.json index dae55de349..d93dc234d1 100644 --- a/includes/api/i18n/id.json +++ b/includes/api/i18n/id.json @@ -7,9 +7,92 @@ "Kenrick95" ] }, + "apihelp-main-param-action": "Tindakan manakah yang akan dilakukan.", + "apihelp-main-param-format": "Format keluaran.", "apihelp-block-description": "Blokir pengguna.", "apihelp-block-param-user": "Nama pengguna, alamat IP, atau rentang alamat IP untuk diblokir.", + "apihelp-block-param-expiry": "Waktu kedaluwarsa. Dapat berupa waktu relatif (seperti 5 bulan atau 2 minggu) atau waktu absolut (seperti 2014-09-18T12:34:56Z). Jika diatur ke selamanya, tak terbatas, atau tidak pernah, pemblokiran itu tidak akan berakhir.", + "apihelp-block-param-reason": "Alasan pemblokiran.", + "apihelp-block-param-anononly": "Blokir hanya pengguna anonim (seperti menonaktifkan suntingan anonim untuk alamat IP ini).", + "apihelp-block-param-nocreate": "Cegah pembuatan akun.", + "apihelp-block-param-autoblock": "Blokir alamat IP terakhir yang digunakan pengguna ini, dan semua alamat IP berikutnya yang mereka coba gunakan untuk menyunting.", + "apihelp-block-param-noemail": "Cegah pengguna mengirimkan surel melalui wiki. (Membutuhkan hak blockemail).", + "apihelp-block-param-reblock": "Jika pengguna tersebut sudah diblokir, atur ulang setelah pemblokirannya.", + "apihelp-block-example-ip-simple": "Blokir alamat IP 192.0.2.5 selama tiga hari dengan alasan Serangan pertama.", + "apihelp-compare-param-fromtitle": "Judul pertama untuk dibandingkan.", + "apihelp-compare-param-fromid": "ID halaman pertama untuk dibandingkan.", + "apihelp-compare-param-fromrev": "Revisi pertama untuk dibandingkan.", + "apihelp-compare-param-toid": "ID halaman kedua untuk dibandingkan.", + "apihelp-compare-param-torev": "Revisi kedua untuk dibandingkan.", + "apihelp-compare-example-1": "Buat perbedaan antara revisi 1 dan 2.", + "apihelp-createaccount-description": "Buat akun pengguna baru.", + "apihelp-createaccount-example-create": "Mulai proses pembuatan pengguna Contoh dengan kata sandi ContohKataSandi.", "apihelp-createaccount-param-name": "Nama pengguna", + "apihelp-createaccount-param-password": "Kata sandi (diabaikan jika $1mailpassword diatur).", + "apihelp-createaccount-param-domain": "Domain untuk otentikasi eksternal (opsional).", + "apihelp-createaccount-param-token": "Token pembuatan akun yang diperoleh pada permintaan pertama.", + "apihelp-createaccount-param-email": "Alamat surel pengguna (opsional).", + "apihelp-createaccount-param-realname": "Nama asli pengguna (opsional).", + "apihelp-createaccount-param-mailpassword": "Jika diberikan nilai, kata sandi acak akan dikirimkan melalui surel kepada pengguna.", + "apihelp-createaccount-param-reason": "Alasan tambahan untuk membuat akun yang akan dicatat dalam log.", + "apihelp-createaccount-param-language": "Kode bahasa untuk diatur sebagai baku kepada pengguna (opsional, nilai bakunya adalah bahasa isi).", + "apihelp-createaccount-example-pass": "Buat pengguna testuser dengan kata sandi test123.", + "apihelp-createaccount-example-mail": "Buat pengguna testmailuser dan kirim surel berisi kata sandi acak.", + "apihelp-delete-description": "Hapus halaman", + "apihelp-delete-param-title": "Judul halaman untuk dihapus. Tidak dapat digunakan bersama dengan $1pageid.", + "apihelp-delete-param-pageid": "ID halaman dari halaman yang akan dihapus. Tidak dapat digunakan bersama dengan $1title.", + "apihelp-delete-param-reason": "Alasan penghapusan. Jika tidak diberikan, alasan yang dihasilkan secara otomatis akan digunakan.", + "apihelp-delete-param-tags": "Ganti tag untuk diterapkan ke entri di log penghapusan.", + "apihelp-delete-param-watch": "Tambahkan halaman ke daftar pantauan pengguna saat ini.", + "apihelp-delete-param-watchlist": "Buat atau hapus halaman tanpa syarat dari daftar pantauan pengguna saat ini, gunakan preferensi atau jangan ganti pantauan.", + "apihelp-delete-param-unwatch": "Hapus halaman dari daftar pantauan pengguna saat ini.", + "apihelp-delete-param-oldimage": "Nama gambar lama untuk dihapus seperti yang disebutkan oleh [[Special:ApiHelp/query+imageinfo|action=query&prop=imageinfo&iiprop=archivename]].", + "apihelp-delete-example-simple": "Hapus Halaman Utama.", + "apihelp-delete-example-reason": "Hapus Halaman Utama dengan alasan Persiapan untuk dialihkan.", + "apihelp-disabled-description": "Modul ini telah dimatikan.", + "apihelp-edit-description": "Buat dan sunting halaman.", + "apihelp-edit-param-title": "Judul halaman untuk dibuat. Tidak dapat digunakan bersama dengan $1pageid.", + "apihelp-edit-param-pageid": "ID halaman dari halaman yang akan disunting. Tidak dapat digunakan bersama dengan $1title.", + "apihelp-edit-param-section": "Nomor bagian. 0 untuk bagian atas, baru untuk bagian baru.", + "apihelp-edit-param-sectiontitle": "Judul untuk bagian baru.", + "apihelp-edit-param-text": "Isi halaman.", + "apihelp-edit-param-summary": "Ringkasan suntingan. Juga tajuk bagian ketika $1section=new dan $1sectiontitle tidak diatur.", + "apihelp-edit-param-tags": "Ganti tag untuk menerapkan ke revisi.", + "apihelp-edit-param-minor": "Suntingan kecil.", + "apihelp-edit-param-notminor": "Bukan suntingan kecil.", + "apihelp-edit-param-bot": "Tandai suntingan ini sebagai bot.", + "apihelp-edit-param-basetimestamp": "Stempel waktu dari revisi asal, digunakan untuk mendeteksi konflik penyuntingan. Dapat ditemukan di [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", + "apihelp-edit-param-starttimestamp": "Stempel waktu ketika proses penyuntingan dimulai, digunakan untuk mendeteksi konflik penyuntingan. Nilai yang cocok dapat ditemukan dengan menggunakan [[Special:ApiHelp/main|curtimestamp]] ketika memulai proses penyuntingan (seperti ketika memuat isi konten yang akan disunting).", + "apihelp-edit-param-recreate": "Batalkan galat yang terjadi tentang halaman yang sudah dihapus pada saat itu.", + "apihelp-edit-param-createonly": "Jangan sunting halaman itu jika sudah ada.", + "apihelp-edit-param-nocreate": "Berikan galat jika halaman belum ada.", + "apihelp-edit-param-watch": "Tambahkan halaman ke daftar pantauan pengguna saat ini.", + "apihelp-edit-param-unwatch": "Hapus halaman dari daftar pantauan pengguna saat ini.", + "apihelp-edit-param-watchlist": "Buat atau hapus halaman tanpa syarat dari daftar pantauan pengguna saat ini, gunakan preferensi atau jangan ganti pantauan.", + "apihelp-edit-param-md5": "Hash MD5 dari parameter $1text, atau parameter $1prependtext dan $1appendtext digabungkan. Jika diatur, suntingan itu tidak akan dilakukan kecuali hash tidak benar.", + "apihelp-edit-param-prependtext": "Tambahkan teks berikut ke bagian awal halaman. Abaikan $1text.", + "apihelp-edit-param-appendtext": "Tambahkan teks berikut ke bagian akhir halaman. Abaikan $1text.\n\nGunakan $1section=new untuk menambahkan sebuah bagian baru, daripada parameter ini.", + "apihelp-edit-param-undo": "Batalkan revisi ini. Abaikan $1text, $1prependtext dan $1appendtext.", + "apihelp-edit-param-undoafter": "Batalkan semua revisi dari $1undo ke revisi ini. Jika tidak diatur, batalkan satu revisi saja.", + "apihelp-edit-param-redirect": "Selesaikan pengalihan secara otomatis.", + "apihelp-edit-param-contentformat": "Format serialisasi isi digunakan untuk teks masukan.", + "apihelp-edit-param-contentmodel": "Model konten dari konten baru.", + "apihelp-edit-param-token": "Token harus selalu dikirim sebagai parameter terakhir, atau setidaknya sesudah parameter $1text.", + "apihelp-edit-example-edit": "Sunting halaman.", + "apihelp-edit-example-prepend": "Tambahkan __NOTOC__ ke halaman.", + "apihelp-edit-example-undo": "Batalkan revisi 13579 melalui 13585 dengan ringkasan otomatis.", + "apihelp-emailuser-description": "Kirim surel ke pengguna ini.", + "apihelp-emailuser-param-target": "Pengguna yang akan dikirimi surel.", + "apihelp-emailuser-param-subject": "Tajuk subjek.", + "apihelp-emailuser-param-text": "Badan pesan.", + "apihelp-emailuser-param-ccme": "Kirimkan salinan pesan ini kepada saya.", + "apihelp-expandtemplates-description": "Tambahkan semua templat dalam teks wiki.", + "apihelp-expandtemplates-param-title": "Judul halaman.", + "apihelp-expandtemplates-param-text": "Teks wiki yang akan diubah.", + "apihelp-expandtemplates-param-revid": "ID revisi, untuk {{REVISIONID}} dan variabel serupa.", + "apihelp-expandtemplates-param-prop": "Bagian informasi manakah yang ingin didapatkan.\n\nPerhatikan bahwa jika tidak ada nilai yang dipilih, hasilnya akan mengandung teks wiki, namun keluaran akan berupa format usang.", "apihelp-login-example-login": "Masuk log.", + "apihelp-query+prefixsearch-param-profile": "Cari profil untuk digunakan.", + "apihelp-query+search-param-qiprofile": "Meminta profil independen untuk digunakan (berefek pada algoritma peringkat).", "apihelp-revisiondelete-param-ids": "Penanda untuk perubahan yang akan dihapus" } diff --git a/includes/api/i18n/it.json b/includes/api/i18n/it.json index 33e8f5efa0..54373a4b5f 100644 --- a/includes/api/i18n/it.json +++ b/includes/api/i18n/it.json @@ -44,6 +44,7 @@ "apihelp-checktoken-example-simple": "Verifica la validità di un token csrf.", "apihelp-clearhasmsg-description": "Cancella il flag hasmsg per l'utente corrente.", "apihelp-clearhasmsg-example-1": "Cancella il flag hasmsg per l'utente corrente.", + "apihelp-clientlogin-description": "Accedi al wiki utilizzando il flusso interattivo.", "apihelp-clientlogin-example-login": "Avvia il processo di accesso alla wiki come utente Example con password ExamplePassword.", "apihelp-clientlogin-example-login2": "Continua l'accesso dopo una risposta dell'UI per l'autenticazione a due fattori, fornendo un OATHToken di 987654.", "apihelp-compare-description": "Ottieni le differenze tra 2 pagine.\n\nUn numero di revisione, il titolo di una pagina, o un ID di pagina deve essere indicato sia per il \"da\" che per lo \"a\".", @@ -55,6 +56,7 @@ "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).", @@ -161,6 +163,7 @@ "apihelp-import-param-rootpage": "Importa come sottopagina di questa pagina. Non può essere usato insieme a $1namespace.", "apihelp-import-example-import": "Importa [[meta:Help:ParserFunctions]] nel namespace 100 con cronologia completa.", "apihelp-linkaccount-description": "Collegamento di un'utenza di un provider di terze parti all'utente corrente.", + "apihelp-linkaccount-example-link": "Avvia il processo di collegamento ad un'utenza da Example.", "apihelp-login-description": "Accedi e ottieni i cookie di autenticazione.\n\nQuesta azione deve essere usata esclusivamente in combinazione con [[Special:BotPasswords]]; utilizzarla per l'accesso all'account principale è deprecato e può fallire senza preavviso. Per accedere in modo sicuro all'utenza principale, usa [[Special:ApiHelp/clientlogin|action=clientlogin]].", "apihelp-login-description-nobotpasswords": "Accedi e ottieni i cookies di autenticazione.\n\nQuesta azione è deprecata e può fallire senza preavviso. Per accedere in modo sicuro, usa [[Special:ApiHelp/clientlogin|action=clientlogin]].", "apihelp-login-description-nonauthmanager": "Accedi e ottieni i cookie di autenticazione.\n\nIn caso di accesso riuscito, i cookies necessari saranno inclusi nella intestazioni di risposta HTTP. In caso di accesso fallito, ulteriori tentativi potrebbero essere limitati, in modo da contenere gli attacchi automatizzati per indovinare le password.", @@ -321,6 +324,8 @@ "apihelp-query+authmanagerinfo-description": "Recupera informazioni circa l'attuale stato di autenticazione.", "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Verifica se lo stato di autenticazione dell'utente attuale è sufficiente per la specifica operazione sensibile alla sicurezza.", "apihelp-query+authmanagerinfo-param-requestsfor": "Recupera informazioni circa le richieste di autenticazione necessarie per la specifica azione di autenticazione.", + "apihelp-query+filerepoinfo-example-login": "Recupera le richieste che possono essere utilizzate quando si inizia l'accesso.", + "apihelp-query+filerepoinfo-example-login-merged": "Recupera le richieste che possono essere utilizzate quando si inizia l'accesso, con i campi del modulo uniti.", "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Verificare se l'autenticazione è sufficiente per l'azione foo.", "apihelp-query+backlinks-description": "Trova tutte le pagine che puntano a quella specificata.", "apihelp-query+backlinks-param-namespace": "Il namespace da elencare.", @@ -462,6 +467,7 @@ "apihelp-query+prefixsearch-param-search": "Stringa di ricerca.", "apihelp-query+prefixsearch-param-limit": "Numero massimo di risultati da restituire.", "apihelp-query+prefixsearch-param-offset": "Numero di risultati da saltare", + "apihelp-query+prefixsearch-param-profile": "Profilo di ricerca da utilizzare.", "apihelp-query+protectedtitles-description": "Elenca tutti i titoli protetti dalla creazione.", "apihelp-query+protectedtitles-param-namespace": "Elenca solo i titoli in questi namespace.", "apihelp-query+protectedtitles-param-level": "Elenca solo i titoli con questi livelli di protezione.", @@ -564,6 +570,7 @@ "apihelp-resetpassword-param-email": "Indirizzo di posta elettronica dell'utente in corso di ripristino.", "apihelp-resetpassword-param-capture": "Restituisce le password temporanee che erano state inviate. Richiede il diritto utente passwordreset.", "apihelp-resetpassword-example-user": "Invia una mail per reimpostare la password all'utente Example.", + "apihelp-resetpassword-example-email": "Invia una mail per reimpostare la password a tutti gli utenti con indirizzo di posta elettronica user@example.com.", "apihelp-revisiondelete-description": "Cancella e ripristina le versioni.", "apihelp-revisiondelete-param-type": "Tipo di cancellazione della versione effettuata.", "apihelp-revisiondelete-param-hide": "Cosa nascondere per ogni versione.", @@ -639,8 +646,10 @@ "api-help-permissions": "{{PLURAL:$1|Permesso|Permessi}}:", "api-help-open-in-apisandbox": "[apri in una sandbox]", "api-help-authmanager-general-usage": "La procedura generale per usare questo modulo é:\n# Ottenere i campi disponibili da [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$4, e un token $5 da [[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Mostra i campi all'utente e ottieni i dati che invia.\n# Esegui un post a questo modulo, fornendo $1returnurl e ogni campo rilevante.\n# Controlla status nella response.\n#* Se hai ricevuto PASS o FAIL, hai finito. L'operazione nel primo caso è andata a buon fine, nel secondo no.\n#* Se hai ricevuto UI, mostra i nuovi campi all'utente e ottieni i dati che invia. Esegui un post a questo modulo con $1continue e i campi rilevanti settati, quindi ripeti il punto 4.\n#* Se hai ricevuto REDIRECT, dirigi l'utente a redirecttarget e aspetta che ritorni a $1returnurl. A quel punto esegui un post a questo modulo con $1continue e ogni campo passato all'URL di ritorno, e ripeti il punto 4.\n#* Se hai ricevuto RESTART, vuol dire che l'autenticazione ha funzionato ma non abbiamo un account collegato. Potresti considerare questo caso come UI o come FAIL.", + "api-help-authmanagerhelper-messageformat": "Formato da utilizzare per per la restituzione dei messaggi.", "api-help-authmanagerhelper-preservestate": "Conserva lo stato da un precedente tentativo di accesso non riuscito, se possibile.", "api-help-authmanagerhelper-returnurl": "URL di ritorno per i flussi di autenticazione di terze parti, deve essere assoluto. E' necessario fornirlo, oppure va fornito $1continue.\n\nAlla ricezione di una risposta REDIRECT, in genere si apre un browser o una vista web all'URL specificato redirecttarget per un flusso di autenticazione di terze parti. Quando questo è completato, la terza parte invierà il browser o la vista web a questo URL. Dovresti estrarre qualsiasi parametro POST o della richiesta dall'URL e passarli come un request $1continue a questo modulo API.", + "api-help-authmanagerhelper-continue": "Questa richiesta è una continuazione dopo una precedente risposta UI o REDIRECT. E' necessario fornire questo oppure $1returnurl.", "api-help-authmanagerhelper-additional-params": "Questo modulo accetta parametri aggiuntivi a seconda delle richieste di autenticazione disponibili. Utilizza [[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]] con amirequestsfor=$1 (o una precedente risposta da questo modulo, se applicabile) per determinare le richieste disponibili e i campi usati da queste.", "api-credits-header": "Crediti" } diff --git a/includes/api/i18n/ko.json b/includes/api/i18n/ko.json index ccfc0055ad..a3a2db4953 100644 --- a/includes/api/i18n/ko.json +++ b/includes/api/i18n/ko.json @@ -19,8 +19,8 @@ "apihelp-main-param-action": "수행할 동작", "apihelp-main-param-format": "출력값의 형식.", "apihelp-main-param-maxlag": "최대 랙은 미디어위키가 데이터베이스 복제된 클러스터에 설치되었을 때 사용될 수 있습니다. 특정한 행동이 사이트 복제 랙을 유발할 때, 이 변수는 클라이언트가 복제 랙이 설정된 숫자 아래로 내려갈 때까지 기다리도록 지시합니다. 과도한 랙의 경우, maxlag 오류 코드와 Waiting for $host: $lag seconds lagged 메시지가 제공됩니다.
[[mw:Manual:Maxlag_parameter|매뉴얼: Maxlag 변수]] 에서 더 많은 정보를 얻을 수 있습니다.", - "apihelp-main-param-smaxage": "s-maxage HTTP 캐시 컨트롤 헤더를 설정합니다. 에러는 캐시되지 않습니다.", - "apihelp-main-param-maxage": "max-age HTTP 캐시 컨트롤 헤더를 설정합니다. 에러는 캐시되지 않습니다.", + "apihelp-main-param-smaxage": "s-maxage HTTP 캐시 컨트롤 헤더를 설정합니다. 오류는 캐시되지 않습니다.", + "apihelp-main-param-maxage": "max-age HTTP 캐시 컨트롤 헤더를 설정합니다. 오류는 캐시되지 않습니다.", "apihelp-main-param-assert": "user 플래그가 설정되어 있다면 로그인 여부를 체크하며, bot 플래그가 설정되어 있다면 봇 사용자 권한이 설정되어 있는지 확인합니다.", "apihelp-main-param-requestid": "주어진 요청 값은 응답에 포함됩니다. 요청을 구분하기 위해 사용될 수 있습니다.", "apihelp-main-param-servedby": "결과에 요청을 처리한 호스트네임을 포함합니다.", diff --git a/includes/api/i18n/oc.json b/includes/api/i18n/oc.json index ad54d42051..32e227d3e2 100644 --- a/includes/api/i18n/oc.json +++ b/includes/api/i18n/oc.json @@ -51,7 +51,7 @@ "apihelp-login-param-password": "Senhal.", "apihelp-login-param-domain": "Domeni (facultatiu).", "apihelp-login-example-login": "Se connectar.", - "apihelp-managetags-description": "Efectuar de prètzfaches de gestion relatius a la modificacion de las balisas.", + "apihelp-managetags-description": "Efectuar de prètzfaits de gestion relatius a la modificacion de las balisas.", "apihelp-move-description": "Desplaçar una pagina.", "apihelp-opensearch-param-search": "Cadena de recèrca.", "apihelp-parse-example-page": "Analisar una pagina.", diff --git a/includes/api/i18n/ps.json b/includes/api/i18n/ps.json index 9f77e10078..89fa617adf 100644 --- a/includes/api/i18n/ps.json +++ b/includes/api/i18n/ps.json @@ -12,7 +12,7 @@ "apihelp-block-param-nocreate": "د گڼون جوړولو مخ نيول.", "apihelp-createaccount-param-name": "کارن-نوم.", "apihelp-delete-description": "يو مخ ړنگول.", - "apihelp-delete-example-simple": "Main Page ړنگول.", + "apihelp-delete-example-simple": "لومړی مخ ړنگول.", "apihelp-edit-description": "مخونه جوړول او سمول.", "apihelp-edit-param-sectiontitle": "د يوې نوې برخې سرليک.", "apihelp-edit-param-text": "مخ مېنځپانگه.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 6137457c1b..11efd46461 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -62,6 +62,7 @@ "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}", "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}", "apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}", + "apihelp-createaccount-param-preservestate": "{{doc-apihelp-param|createaccount|preservestate|info=This message is displayed in addition to {{msg-mw|api-help-authmanagerhelper-preservestate}}.}}", "apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}", "apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}", "apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}", @@ -150,6 +151,7 @@ "apihelp-feedcontributions-param-deletedonly": "{{doc-apihelp-param|feedcontributions|deletedonly}}", "apihelp-feedcontributions-param-toponly": "{{doc-apihelp-param|feedcontributions|toponly}}", "apihelp-feedcontributions-param-newonly": "{{doc-apihelp-param|feedcontributions|newonly}}", + "apihelp-feedcontributions-param-hideminor": "{{doc-apihelp-param|feedcontributions|hideminor}}", "apihelp-feedcontributions-param-showsizediff": "{{doc-apihelp-param|feedcontributions|showsizediff}}", "apihelp-feedcontributions-example-simple": "{{doc-apihelp-example|feedcontributions}}", "apihelp-feedrecentchanges-description": "{{doc-apihelp-description|feedrecentchanges}}", @@ -894,6 +896,7 @@ "apihelp-query+prefixsearch-param-limit": "{{doc-apihelp-param|query+prefixsearch|limit}}", "apihelp-query+prefixsearch-param-offset": "{{doc-apihelp-param|query+prefixsearch|offset}}", "apihelp-query+prefixsearch-example-simple": "{{doc-apihelp-example|query+prefixsearch}}", + "apihelp-query+prefixsearch-param-profile": "{{doc-apihelp-param|query+prefixsearch|profile|paramvalues=1}}", "apihelp-query+protectedtitles-description": "{{doc-apihelp-description|query+protectedtitles}}", "apihelp-query+protectedtitles-param-namespace": "{{doc-apihelp-param|query+protectedtitles|namespace}}", "apihelp-query+protectedtitles-param-level": "{{doc-apihelp-param|query+protectedtitles|level}}", @@ -1006,6 +1009,7 @@ "apihelp-query+search-param-what": "{{doc-apihelp-param|query+search|what}}", "apihelp-query+search-param-info": "{{doc-apihelp-param|query+search|info}}", "apihelp-query+search-param-prop": "{{doc-apihelp-param|query+search|prop|paramvalues=1}}", + "apihelp-query+search-param-qiprofile": "{{doc-apihelp-param|query+search|qiprofile|paramvalues=1}}", "apihelp-query+search-paramvalue-prop-size": "{{doc-apihelp-paramvalue|query+search|prop|size}}", "apihelp-query+search-paramvalue-prop-wordcount": "{{doc-apihelp-paramvalue|query+search|prop|wordcount}}", "apihelp-query+search-paramvalue-prop-timestamp": "{{doc-apihelp-paramvalue|query+search|prop|timestamp}}", diff --git a/includes/api/i18n/zh-hans.json b/includes/api/i18n/zh-hans.json index 65ae10e439..46e5c85798 100644 --- a/includes/api/i18n/zh-hans.json +++ b/includes/api/i18n/zh-hans.json @@ -68,6 +68,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]]返回用于hasprimarypreservedstate的真值,标记为primary-required的请求应被忽略。如果它返回用于preservedusername的非空值,用户名必须用于username参数。", "apihelp-createaccount-example-create": "开始创建用户Example和密码ExamplePassword的过程。", "apihelp-createaccount-param-name": "用户名。", "apihelp-createaccount-param-password": "密码(如果设置$1mailpassword则忽略)。", @@ -93,7 +94,7 @@ "apihelp-delete-example-reason": "删除Main Page,原因Preparing for move。", "apihelp-disabled-description": "此模块已禁用。", "apihelp-edit-description": "创建和编辑页面。", - "apihelp-edit-param-title": "您希望编辑的页面标题。不能与$1pageid一起使用。", + "apihelp-edit-param-title": "要编辑的页面标题。不能与$1pageid一起使用。", "apihelp-edit-param-pageid": "要编辑的页面的页面 ID。不能与$1title一起使用。", "apihelp-edit-param-section": "段落数。0用于首段,new用于新的段落。", "apihelp-edit-param-sectiontitle": "新段落的标题。", @@ -553,6 +554,7 @@ "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是否足够。", @@ -897,6 +899,7 @@ "apihelp-query+prefixsearch-param-limit": "要返回的结果最大数。", "apihelp-query+prefixsearch-param-offset": "跳过的结果数。", "apihelp-query+prefixsearch-example-simple": "搜索以meaning开头的页面标题。", + "apihelp-query+prefixsearch-param-profile": "搜索要使用的配置文件。", "apihelp-query+protectedtitles-description": "列出所有被限制创建的标题。", "apihelp-query+protectedtitles-param-namespace": "只列出这些名字空间的标题。", "apihelp-query+protectedtitles-param-level": "只列出带这些保护级别的标题。", @@ -1009,6 +1012,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": "添加页面上次编辑时的时间戳。", @@ -1394,8 +1398,11 @@ "api-help-permissions-granted-to": "{{PLURAL:$1|授予}}:$2", "api-help-right-apihighlimits": "在API查询中使用更高的上限(慢查询:$1;快查询:$2)。慢查询的限制也适用于多值参数。", "api-help-open-in-apisandbox": "[在沙盒中打开]", + "api-help-authmanager-general-usage": "使用此模块的一般程序是:\n# 通过amirequestsfor=$4取得来自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]的可用字段,和来自[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]的$5令牌。\n# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying $1returnurl and any relevant fields.\n# Check the status in the response.\n#* If you received PASS or FAIL, you're done. The operation either succeeded or it didn't.\n#* If you received UI, present the new fields to the user and obtain their submission. Then post to this module with $1continue and the relevant fields set, and repeat step 4.\n#* If you received REDIRECT, direct the user to the redirecttarget and wait for the return to $1returnurl. Then post to this module with $1continue and any fields passed to the return URL, and repeat step 4.\n#* If you received RESTART, that means the authentication worked but we don't have a linked user account. You might treat this as UI or as FAIL.", "api-help-authmanagerhelper-request": "使用此身份验证请求,通过返回自[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]的id与amirequestsfor=$1。", "api-help-authmanagerhelper-messageformat": "返回消息使用的格式。", + "api-help-authmanagerhelper-mergerequestfields": "合并用于所有身份验证请求的字段信息至一个数组中。", + "api-help-authmanagerhelper-additional-params": "此模块允许额外参数,取决于可用的身份验证请求。使用[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]与amirequestsfor=$1(或之前来自此模块的相应,如果可以)以决定可用请求及其使用的字段。", "api-credits-header": "制作人员", "api-credits": "API 开发人员:\n* Yuri Astrakhan(创建者,2006年9月~2007年9月的开发组领导)\n* Roan Kattouw(2007年9月~2009年的开发组领导)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch(2013年至今的开发组领导)\n\n请将您的评论、建议和问题发送至mediawiki-api@lists.wikimedia.org,或提交错误请求至https://phabricator.wikimedia.org/。" } diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index efee53c6dc..2ed0d618c7 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -231,6 +231,17 @@ class AuthManager implements LoggerAwareInterface { /** * Start an authentication flow + * + * In addition to the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might include a + * CreateFromLoginAuthenticationRequest from a previous login attempt to + * preserve state. + * + * Instead of the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might pass a + * CreatedAccountAuthenticationRequest from an account creation that just + * succeeded to log in to the just-created account. + * * @param AuthenticationRequest[] $reqs * @param string $returnToUrl Url that REDIRECT responses should eventually * return to. @@ -344,8 +355,7 @@ class AuthManager implements LoggerAwareInterface { * Return values are interpreted as follows: * - status FAIL: Authentication failed. If $response->createRequest is * set, that may be passed to self::beginAuthentication() or to - * self::beginAccountCreation() (after adding a username, if necessary) - * to preserve state. + * self::beginAccountCreation() to preserve state. * - status REDIRECT: The client should be redirected to the contained URL, * new AuthenticationRequests should be made (if any), then * AuthManager::continueAuthentication() should be called. @@ -432,6 +442,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Primary login with $id returned $res->status" ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); @@ -494,6 +505,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Primary login with $id returned $res->status" ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); return $res; @@ -546,6 +558,7 @@ class AuthManager implements LoggerAwareInterface { ); $ret->neededRequests[] = $ret->createRequest; } + $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true ); $session->setSecret( 'AuthManager::authnState', [ 'reqs' => [], // Will be filled in later 'primary' => null, @@ -615,6 +628,7 @@ class AuthManager implements LoggerAwareInterface { case AuthenticationResponse::REDIRECT; case AuthenticationResponse::UI; $this->logger->debug( "Secondary login with $id returned " . $res->status ); + $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() ); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::authnState', $state ); @@ -950,6 +964,17 @@ class AuthManager implements LoggerAwareInterface { /** * Start an account creation flow + * + * In addition to the AuthenticationRequests returned by + * $this->getAuthenticationRequests(), a client might include a + * CreateFromLoginAuthenticationRequest from a previous login attempt. If + * + * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE ) + * + * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests + * should be omitted. If the CreateFromLoginAuthenticationRequest has a + * username set, that username must be used for all other requests. + * * @param User $creator User doing the account creation * @param AuthenticationRequest[] $reqs * @param string $returnToUrl Url that REDIRECT responses should eventually @@ -1038,44 +1063,10 @@ class AuthManager implements LoggerAwareInterface { if ( $req ) { $state['maybeLink'] = $req->maybeLink; - // If we get here, the user didn't submit a form with any of the - // usual AuthenticationRequests that are needed for an account - // creation. So we need to determine if there are any and return a - // UI response if so. if ( $req->createRequest ) { - // We have a createRequest from a - // PrimaryAuthenticationProvider, so don't ask. - $providers = $this->getPreAuthenticationProviders() + - $this->getSecondaryAuthenticationProviders(); - } else { - // We're only preserving maybeLink, so ask for primary fields - // too. - $providers = $this->getPreAuthenticationProviders() + - $this->getPrimaryAuthenticationProviders() + - $this->getSecondaryAuthenticationProviders(); - } - $reqs = $this->getAuthenticationRequestsInternal( - self::ACTION_CREATE, - [], - $providers - ); - // See if we need any requests to begin - foreach ( (array)$reqs as $r ) { - if ( !$r instanceof UsernameAuthenticationRequest && - !$r instanceof UserDataAuthenticationRequest && - !$r instanceof CreationReasonAuthenticationRequest - ) { - // Needs some reqs, so request them - $reqs[] = new CreateFromLoginAuthenticationRequest( $req->createRequest, [] ); - $state['continueRequests'] = $reqs; - $session->setSecret( 'AuthManager::accountCreationState', $state ); - $session->persist(); - return AuthenticationResponse::newUI( $reqs, wfMessage( 'authmanager-create-from-login' ) ); - } + $reqs[] = $req->createRequest; + $state['reqs'][] = $req->createRequest; } - // No reqs needed, so we can just continue. - $req->createRequest->returnToUrl = $returnToUrl; - $reqs = [ $req->createRequest ]; } $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1213,15 +1204,6 @@ class AuthManager implements LoggerAwareInterface { $req->username = $state['username']; } - // If we're coming in from a create-from-login UI response, we need - // to extract the createRequest (if any). - $req = AuthenticationRequest::getRequestByClass( - $reqs, CreateFromLoginAuthenticationRequest::class - ); - if ( $req && $req->createRequest ) { - $reqs[] = $req->createRequest; - } - // Run pre-creation tests, if we haven't already if ( !$state['ranPreTests'] ) { $providers = $this->getPreAuthenticationProviders() + @@ -1281,6 +1263,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1343,6 +1326,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); return $res; @@ -1438,6 +1422,7 @@ class AuthManager implements LoggerAwareInterface { 'user' => $user->getName(), 'creator' => $creator->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null ); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountCreationState', $state ); @@ -1806,6 +1791,7 @@ class AuthManager implements LoggerAwareInterface { $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [ 'user' => $user->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() ); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountLinkState', $state ); @@ -1908,6 +1894,7 @@ class AuthManager implements LoggerAwareInterface { $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [ 'user' => $user->getName(), ] ); + $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() ); $state['continueRequests'] = $res->neededRequests; $session->setSecret( 'AuthManager::accountLinkState', $state ); return $res; @@ -2069,12 +2056,7 @@ class AuthManager implements LoggerAwareInterface { } // Fill in reqs data - foreach ( $reqs as $req ) { - $req->action = $providerAction; - if ( $req->username === null ) { - $req->username = $options['username']; - } - } + $this->fillRequests( $reqs, $providerAction, $options['username'], true ); // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) { @@ -2086,6 +2068,24 @@ class AuthManager implements LoggerAwareInterface { return array_values( $reqs ); } + /** + * Set values in an array of requests + * @param AuthenticationRequest[] &$reqs + * @param string $action + * @param string|null $username + * @param boolean $forceAction + */ + private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) { + foreach ( $reqs as $req ) { + if ( !$req->action || $forceAction ) { + $req->action = $action; + } + if ( $req->username === null ) { + $req->username = $username; + } + } + } + /** * Determine whether a username exists * @param string $username @@ -2124,6 +2124,37 @@ class AuthManager implements LoggerAwareInterface { return true; } + /** + * Get a provider by ID + * @note This is public so extensions can check whether their own provider + * is installed and so they can read its configuration if necessary. + * Other uses are not recommended. + * @param string $id + * @return AuthenticationProvider|null + */ + public function getAuthenticationProvider( $id ) { + // Fast version + if ( isset( $this->allAuthenticationProviders[$id] ) ) { + return $this->allAuthenticationProviders[$id]; + } + + // Slow version: instantiate each kind and check + $providers = $this->getPrimaryAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + $providers = $this->getSecondaryAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + $providers = $this->getPreAuthenticationProviders(); + if ( isset( $providers[$id] ) ) { + return $providers[$id]; + } + + return null; + } + /**@}*/ /** @@ -2273,34 +2304,6 @@ class AuthManager implements LoggerAwareInterface { return $this->secondaryAuthenticationProviders; } - /** - * Get a provider by ID - * @param string $id - * @return AuthenticationProvider|null - */ - protected function getAuthenticationProvider( $id ) { - // Fast version - if ( isset( $this->allAuthenticationProviders[$id] ) ) { - return $this->allAuthenticationProviders[$id]; - } - - // Slow version: instantiate each kind and check - $providers = $this->getPrimaryAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - $providers = $this->getSecondaryAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - $providers = $this->getPreAuthenticationProviders(); - if ( isset( $providers[$id] ) ) { - return $providers[$id]; - } - - return null; - } - /** * @param User $user * @param bool|null $remember @@ -2310,6 +2313,7 @@ class AuthManager implements LoggerAwareInterface { $delay = $session->delaySave(); $session->resetId(); + $session->resetAllTokens(); if ( $session->canSetUser() ) { $session->setUser( $user ); } @@ -2332,7 +2336,7 @@ class AuthManager implements LoggerAwareInterface { private function setDefaultUserOptions( User $user, $useContextLang ) { global $wgContLang; - \MediaWiki\Session\SessionManager::singleton()->invalidateSessionsForUser( $user ); + $user->setToken(); $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang; $user->setOption( 'language', $lang->getPreferredVariant() ); diff --git a/includes/auth/AuthManagerAuthPlugin.php b/includes/auth/AuthManagerAuthPlugin.php index bf1e0215bc..8d85b4411d 100644 --- a/includes/auth/AuthManagerAuthPlugin.php +++ b/includes/auth/AuthManagerAuthPlugin.php @@ -131,7 +131,7 @@ class AuthManagerAuthPlugin extends \AuthPlugin { $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data ); foreach ( $reqs as $req ) { $status = AuthManager::singleton()->allowsAuthenticationDataChange( $req ); - if ( !$status->isOk() ) { + if ( !$status->isGood() ) { $this->logger->info( __METHOD__ . ': Password change rejected: {reason}', [ 'username' => $data['username'], 'reason' => $status->getWikiText( null, null, 'en' ), diff --git a/includes/auth/AuthPluginPrimaryAuthenticationProvider.php b/includes/auth/AuthPluginPrimaryAuthenticationProvider.php index 9746637b00..b8e36bc4f3 100644 --- a/includes/auth/AuthPluginPrimaryAuthenticationProvider.php +++ b/includes/auth/AuthPluginPrimaryAuthenticationProvider.php @@ -329,7 +329,7 @@ class AuthPluginPrimaryAuthenticationProvider if ( $req->domain === null ) { return \StatusValue::newGood( 'ignored' ); } - if ( !$this->auth->validDomain( $domain ) ) { + if ( !$this->auth->validDomain( $req->domain ) ) { return \StatusValue::newFatal( 'authmanager-authplugin-setpass-bad-domain' ); } } diff --git a/includes/auth/AuthenticationRequest.php b/includes/auth/AuthenticationRequest.php index 3c19b87f17..ff4d52ed92 100644 --- a/includes/auth/AuthenticationRequest.php +++ b/includes/auth/AuthenticationRequest.php @@ -92,14 +92,12 @@ abstract class AuthenticationRequest { * - select: * - multiselect: More a grid of checkboxes than if 'image' is set, otherwise - * (uses 'label' as button text) + * - button: (uses 'label' as button text) * - hidden: Not visible to the user, but needs to be preserved for the next request * - null: No widget, just display the 'label' message. * - options: (array) Maps option values to Messages for the * 'select' and 'multiselect' types. * - value: (string) Value (for 'null' and 'hidden') or default value (for other types). - * - image: (string) URL of an image to use in connection with the input * - label: (Message) Text suitable for a label in an HTML form * - help: (Message) Text suitable as a description of what the field is * - optional: (bool) If set and truthy, the field may be left empty @@ -281,6 +279,21 @@ abstract class AuthenticationRequest { public static function mergeFieldInfo( array $reqs ) { $merged = []; + // fields that are required by some primary providers but not others are not actually required + $primaryRequests = array_filter( $reqs, function ( $req ) { + return $req->required === AuthenticationRequest::PRIMARY_REQUIRED; + } ); + $sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) { + $required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) { + return empty( $options['optional'] ); + } ) ); + if ( $shared === null ) { + return $required; + } else { + return array_intersect( $shared, $required ); + } + }, null ); + foreach ( $reqs as $req ) { $info = $req->getFieldInfo(); if ( !$info ) { @@ -288,8 +301,14 @@ abstract class AuthenticationRequest { } foreach ( $info as $name => $options ) { - if ( $req->required !== self::REQUIRED ) { + if ( // If the request isn't required, its fields aren't required either. + $req->required === self::OPTIONAL + // If there is a primary not requiring this field, no matter how many others do, + // authentication can proceed without it. + || $req->required === self::PRIMARY_REQUIRED + && !in_array( $name, $sharedRequiredPrimaryFields, true ) + ) { $options['optional'] = true; } else { $options['optional'] = !empty( $options['optional'] ); diff --git a/includes/auth/AuthenticationResponse.php b/includes/auth/AuthenticationResponse.php index db0182552d..5048cf84dd 100644 --- a/includes/auth/AuthenticationResponse.php +++ b/includes/auth/AuthenticationResponse.php @@ -83,13 +83,14 @@ class AuthenticationResponse { /** * @var AuthenticationRequest|null * - * Returned with a PrimaryAuthenticationProvider login FAIL, this holds a - * request that should result in a PASS when passed to that provider's - * PrimaryAuthenticationProvider::beginPrimaryAccountCreation(). + * Returned with a PrimaryAuthenticationProvider login FAIL or a PASS with + * no username, this holds a request that should result in a PASS when + * passed to that provider's PrimaryAuthenticationProvider::beginPrimaryAccountCreation(). * - * Returned with an AuthManager login FAIL or RESTART, this holds a request - * that may be passed to AuthManager::beginCreateAccount() after setting - * its ->returnToUrl property. It may also be passed to + * Returned with an AuthManager login FAIL or RESTART, this holds a + * CreateFromLoginAuthenticationRequest that may be passed to + * AuthManager::beginCreateAccount(), possibly in place of any + * "primary-required" requests. It may also be passed to * AuthManager::beginAuthentication() to preserve state. */ public $createRequest = null; diff --git a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php index d84f9900f2..32c8fd55de 100644 --- a/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php +++ b/includes/auth/ConfirmLinkSecondaryAuthenticationProvider.php @@ -50,7 +50,14 @@ class ConfirmLinkSecondaryAuthenticationProvider extends AbstractSecondaryAuthen if ( !is_array( $state ) ) { return AuthenticationResponse::newAbstain(); } - $maybeLink = $state['maybeLink']; + + $maybeLink = array_filter( $state['maybeLink'], function ( $req ) use ( $user ) { + if ( !$req->action ) { + $req->action = AuthManager::ACTION_CHANGE; + } + $req->username = $user->getName(); + return $this->manager->allowsAuthenticationDataChange( $req )->isGood(); + } ); if ( !$maybeLink ) { return AuthenticationResponse::newAbstain(); } diff --git a/includes/auth/CreateFromLoginAuthenticationRequest.php b/includes/auth/CreateFromLoginAuthenticationRequest.php index 949302d8bc..ddeb13d9d6 100644 --- a/includes/auth/CreateFromLoginAuthenticationRequest.php +++ b/includes/auth/CreateFromLoginAuthenticationRequest.php @@ -25,7 +25,8 @@ namespace MediaWiki\Auth; * This transfers state between the login and account creation flows. * * AuthManager::getAuthenticationRequests() won't return this type, but it - * may be passed to AuthManager::beginAccountCreation() anyway. + * may be passed to AuthManager::beginAuthentication() or + * AuthManager::beginAccountCreation() anyway. * * @ingroup Auth * @since 1.27 @@ -50,6 +51,7 @@ class CreateFromLoginAuthenticationRequest extends AuthenticationRequest { ) { $this->createRequest = $createRequest; $this->maybeLink = $maybeLink; + $this->username = $createRequest ? $createRequest->username : null; } public function getFieldInfo() { @@ -59,4 +61,36 @@ class CreateFromLoginAuthenticationRequest extends AuthenticationRequest { public function loadFromSubmission( array $data ) { return true; } + + /** + * Indicate whether this request contains any state for the specified + * action. + * @param string $action One of the AuthManager::ACTION_* constants + * @return boolean + */ + public function hasStateForAction( $action ) { + switch ( $action ) { + case AuthManager::ACTION_LOGIN: + return (bool)$this->maybeLink; + case AuthManager::ACTION_CREATE: + return $this->maybeLink || $this->createRequest; + default: + return false; + } + } + + /** + * Indicate whether this request contains state for the specified + * action sufficient to replace other primary-required requests. + * @param string $action One of the AuthManager::ACTION_* constants + * @return boolean + */ + public function hasPrimaryStateForAction( $action ) { + switch ( $action ) { + case AuthManager::ACTION_CREATE: + return (bool)$this->createRequest; + default: + return false; + } + } } diff --git a/includes/auth/CreationReasonAuthenticationRequest.php b/includes/auth/CreationReasonAuthenticationRequest.php index 1711aec974..146470ed8f 100644 --- a/includes/auth/CreationReasonAuthenticationRequest.php +++ b/includes/auth/CreationReasonAuthenticationRequest.php @@ -10,6 +10,8 @@ class CreationReasonAuthenticationRequest extends AuthenticationRequest { /** @var string Account creation reason (only used when creating for someone else) */ public $reason; + public $required = self::OPTIONAL; + public function getFieldInfo() { return [ 'reason' => [ diff --git a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php index 2e51cf22c1..dd97830dae 100644 --- a/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php +++ b/includes/auth/ResetPasswordSecondaryAuthenticationProvider.php @@ -95,12 +95,12 @@ class ResetPasswordSecondaryAuthenticationProvider extends AbstractSecondaryAuth } } - if ( isset( $data->req ) ) { - $needReq = $data->req; - } else { - $needReq = new PasswordAuthenticationRequest(); + $needReq = isset( $data->req ) ? $data->req : new PasswordAuthenticationRequest(); + if ( !$needReq->action ) { $needReq->action = AuthManager::ACTION_CHANGE; } + $needReq->required = $data->hard ? AuthenticationRequest::REQUIRED + : AuthenticationRequest::OPTIONAL; $needReqs = [ $needReq ]; if ( !$data->hard ) { $needReqs[] = new ButtonAuthenticationRequest( diff --git a/includes/cache/LinkBatch.php b/includes/cache/LinkBatch.php index 04d2524d91..ec37dd61c3 100644 --- a/includes/cache/LinkBatch.php +++ b/includes/cache/LinkBatch.php @@ -73,8 +73,8 @@ class LinkBatch { * @param string $dbkey */ public function add( $ns, $dbkey ) { - if ( $ns < 0 ) { - return; + if ( $ns < 0 || $dbkey === '' ) { + return; // T137083 } if ( !array_key_exists( $ns, $this->data ) ) { $this->data[$ns] = []; @@ -168,7 +168,7 @@ class LinkBatch { // The remaining links in $data are bad links, register them as such foreach ( $remaining as $ns => $dbkeys ) { foreach ( $dbkeys as $dbkey => $unused ) { - $title = new TitleValue( (int)$ns, $dbkey ); + $title = new TitleValue( (int)$ns, (string)$dbkey ); $cache->addBadLinkObj( $title ); $pdbk = $titleFormatter->getPrefixedDBkey( $title ); $ids[$pdbk] = 0; diff --git a/includes/collation/IcuCollation.php b/includes/collation/IcuCollation.php index f6a9dc76c5..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' => [ "Ç", "Ğ", "İ", "Ö", "Ş", "Ü" ], diff --git a/includes/db/DBConnRef.php b/includes/db/DBConnRef.php index d73ba85fc0..af5f8f9fee 100644 --- a/includes/db/DBConnRef.php +++ b/includes/db/DBConnRef.php @@ -433,7 +433,7 @@ class DBConnRef implements IDatabase { return $this->__call( __FUNCTION__, func_get_args() ); } - public function doAtomicSection( $fname, $callback ) { + public function doAtomicSection( $fname, callable $callback ) { return $this->__call( __FUNCTION__, func_get_args() ); } diff --git a/includes/db/Database.php b/includes/db/Database.php index 92e89b0de1..6bdcb24cdb 100644 --- a/includes/db/Database.php +++ b/includes/db/Database.php @@ -2561,11 +2561,7 @@ abstract class DatabaseBase implements IDatabase { } } - final public function doAtomicSection( $fname, $callback ) { - if ( !is_callable( $callback ) ) { - throw new UnexpectedValueException( "Invalid callback." ); - }; - + final public function doAtomicSection( $fname, callable $callback ) { $this->startAtomic( $fname ); try { call_user_func_array( $callback, [ $this, $fname ] ); diff --git a/includes/db/IDatabase.php b/includes/db/IDatabase.php index 710efb2ca6..0a71df2312 100644 --- a/includes/db/IDatabase.php +++ b/includes/db/IDatabase.php @@ -1313,7 +1313,7 @@ interface IDatabase { * @throws UnexpectedValueException * @since 1.27 */ - public function doAtomicSection( $fname, $callback ); + public function doAtomicSection( $fname, callable $callback ); /** * Begin a transaction. If a transaction is already in progress, diff --git a/includes/deferred/LinksDeletionUpdate.php b/includes/deferred/LinksDeletionUpdate.php index 65a8c0e0b1..b8bd74722c 100644 --- a/includes/deferred/LinksDeletionUpdate.php +++ b/includes/deferred/LinksDeletionUpdate.php @@ -42,48 +42,95 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate } elseif ( $pageId ) { $this->pageId = $pageId; } else { - throw new MWException( "Page ID not known, perhaps the page doesn't exist?" ); + throw new InvalidArgumentException( "Page ID not known. Page doesn't exist?" ); } } public function doUpdate() { - # Page may already be deleted, so don't just getId() + $config = RequestContext::getMain()->getConfig(); + $batchSize = $config->get( 'UpdateRowsPerQuery' ); + + // Page may already be deleted, so don't just getId() $id = $this->pageId; // Make sure all links update threads see the changes of each other. // This handles the case when updates have to batched into several COMMITs. $scopedLock = LinksUpdate::acquirePageLock( $this->mDb, $id ); - # Delete restrictions for it + // Delete restrictions for it $this->mDb->delete( 'page_restrictions', [ 'pr_page' => $id ], __METHOD__ ); - # Fix category table counts + // Fix category table counts $cats = $this->mDb->selectFieldValues( 'categorylinks', 'cl_to', [ 'cl_from' => $id ], __METHOD__ ); - $this->page->updateCategoryCounts( [], $cats ); + $catBatches = array_chunk( $cats, $batchSize ); + foreach ( $catBatches as $catBatch ) { + $this->page->updateCategoryCounts( [], $catBatch ); + if ( count( $catBatches ) > 1 ) { + $this->mDb->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); + } + } - # If using cascading deletes, we can skip some explicit deletes + // If using cascading deletes, we can skip some explicit deletes if ( !$this->mDb->cascadingDeletes() ) { - # Delete outgoing links - $this->mDb->delete( 'pagelinks', [ 'pl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'imagelinks', [ 'il_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'categorylinks', [ 'cl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'templatelinks', [ 'tl_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'externallinks', [ 'el_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'langlinks', [ 'll_from' => $id ], __METHOD__ ); - $this->mDb->delete( 'iwlinks', [ 'iwl_from' => $id ], __METHOD__ ); + // Delete outgoing links + $this->batchDeleteByPK( + 'pagelinks', + [ 'pl_from' => $id ], + [ 'pl_from', 'pl_namespace', 'pl_title' ], + $batchSize + ); + $this->batchDeleteByPK( + 'imagelinks', + [ 'il_from' => $id ], + [ 'il_from', 'il_to' ], + $batchSize + ); + $this->batchDeleteByPK( + 'categorylinks', + [ 'cl_from' => $id ], + [ 'cl_from', 'cl_to' ], + $batchSize + ); + $this->batchDeleteByPK( + 'templatelinks', + [ 'tl_from' => $id ], + [ 'tl_from', 'tl_namespace', 'tl_title' ], + $batchSize + ); + $this->batchDeleteByPK( + 'externallinks', + [ 'el_from' => $id ], + [ 'el_id' ], + $batchSize + ); + $this->batchDeleteByPK( + 'langlinks', + [ 'll_from' => $id ], + [ 'll_from', 'll_lang' ], + $batchSize + ); + $this->batchDeleteByPK( + 'iwlinks', + [ 'iwl_from' => $id ], + [ 'iwl_from', 'iwl_prefix', 'iwl_title' ], + $batchSize + ); + // Delete any redirect entry or page props entries $this->mDb->delete( 'redirect', [ 'rd_from' => $id ], __METHOD__ ); $this->mDb->delete( 'page_props', [ 'pp_page' => $id ], __METHOD__ ); } - # If using cleanup triggers, we can skip some manual deletes + // If using cleanup triggers, we can skip some manual deletes if ( !$this->mDb->cleanupTriggers() ) { $title = $this->page->getTitle(); - # Find recentchanges entries to clean up... - $rcIdsForTitle = $this->mDb->selectFieldValues( 'recentchanges', + // Find recentchanges entries to clean up... + $rcIdsForTitle = $this->mDb->selectFieldValues( + 'recentchanges', 'rc_id', [ 'rc_type != ' . RC_LOG, @@ -92,16 +139,21 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate ], __METHOD__ ); - $rcIdsForPage = $this->mDb->selectFieldValues( 'recentchanges', + $rcIdsForPage = $this->mDb->selectFieldValues( + 'recentchanges', 'rc_id', [ 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ], __METHOD__ ); - # T98706: delete PK to avoid lock contention with RC delete log insertions - $rcIds = array_merge( $rcIdsForTitle, $rcIdsForPage ); - if ( $rcIds ) { - $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIds ], __METHOD__ ); + // T98706: delete by PK to avoid lock contention with RC delete log insertions + $rcIdBatches = array_chunk( array_merge( $rcIdsForTitle, $rcIdsForPage ), $batchSize ); + foreach ( $rcIdBatches as $rcIdBatch ) { + $this->mDb->delete( 'recentchanges', [ 'rc_id' => $rcIdBatch ], __METHOD__ ); + if ( count( $rcIdBatches ) > 1 ) { + $this->mDb->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $this->mDb->getWikiID() ] ); + } } } @@ -111,6 +163,26 @@ class LinksDeletionUpdate extends SqlDataUpdate implements EnqueueableDataUpdate } ); } + private function batchDeleteByPK( $table, array $conds, array $pk, $bSize ) { + $dbw = $this->mDb; // convenience + $res = $dbw->select( $table, $pk, $conds, __METHOD__ ); + + $pkDeleteConds = []; + foreach ( $res as $row ) { + $pkDeleteConds[] = $this->mDb->makeList( (array)$row, LIST_AND ); + if ( count( $pkDeleteConds ) >= $bSize ) { + $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ ); + $dbw->commit( __METHOD__, 'flush' ); + wfGetLBFactory()->waitForReplication( [ 'wiki' => $dbw->getWikiID() ] ); + $pkDeleteConds = []; + } + } + + if ( $pkDeleteConds ) { + $dbw->delete( $table, $dbw->makeList( $pkDeleteConds, LIST_OR ), __METHOD__ ); + } + } + public function getAsJobSpecification() { return [ 'wiki' => $this->mDb->getWikiID(), diff --git a/includes/deferred/LinksUpdate.php b/includes/deferred/LinksUpdate.php index 07b5614424..d4a61faf86 100644 --- a/includes/deferred/LinksUpdate.php +++ b/includes/deferred/LinksUpdate.php @@ -155,10 +155,11 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { Hooks::run( 'LinksUpdate', [ &$this ] ); $this->doIncrementalUpdate(); - $this->mDb->onTransactionIdle( function() use ( &$scopedLock ) { + // Commit and release the lock + ScopedCallback::consume( $scopedLock ); + // Run post-commit hooks without DBO_TRX + $this->mDb->onTransactionIdle( function() { Hooks::run( 'LinksUpdateComplete', [ &$this ] ); - // Release the lock *after* the final COMMIT for correctness - ScopedCallback::consume( $scopedLock ); } ); } @@ -243,15 +244,14 @@ class LinksUpdate extends SqlDataUpdate implements EnqueueableDataUpdate { $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing ); $this->invalidateProperties( $changed ); - # Update the links table freshness for this title - $this->updateLinksTimestamp(); - # Refresh links of all pages including this page # This will be in a separate transaction if ( $this->mRecursive ) { $this->queueRecursiveJobs(); } + # Update the links table freshness for this title + $this->updateLinksTimestamp(); } /** 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/htmlform/HTMLForm.php b/includes/htmlform/HTMLForm.php index de3e0ae32f..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; @@ -1108,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