From: jenkins-bot Date: Thu, 7 Jun 2018 16:49:40 +0000 (+0000) Subject: Merge "installer: Fix display of UPGRADE by disabling InterwikiLookup" X-Git-Tag: 1.34.0-rc.0~5160 X-Git-Url: http://git.cyclocoop.org/%7B%24admin_url%7Dmembres/cotisations/gestion/rappel_supprimer.php?a=commitdiff_plain;h=693c3ad67badd955b71341d481e281c46f480b87;hp=a498abf272c7aff376c5225a6f593349e3cc3eaa;p=lhc%2Fweb%2Fwiklou.git Merge "installer: Fix display of UPGRADE by disabling InterwikiLookup" --- diff --git a/RELEASE-NOTES-1.31 b/RELEASE-NOTES-1.31 index 49d53bd398..c98f6633ef 100644 --- a/RELEASE-NOTES-1.31 +++ b/RELEASE-NOTES-1.31 @@ -10,6 +10,7 @@ production. * (T196092) Hide MySQL binary/utf-8 charset option in the installer. * (T196185) Don't allow setting $wgDBmysql5 in the installer. * (T196125) php-memcached 3.0 (provided with PHP 7.0) is now supported. +* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+ === Changes since MediaWiki 1.31.0-rc.0 === * (T33223) Drop archive.ar_text and ar_flags. @@ -166,6 +167,7 @@ production. * (T2087, T10897, T87753, T174639) Whitespace created by category and language links is now stripped rather than leaving blank lines in odd places. * (T3780) Uploads with UTF-8 names now work on PHP7.1+ on Windows servers. +* (T182366) UploadBase::checkXMLEncodingMissmatch() now works on PHP 7.1+ === Action API changes in 1.31 === * (T185058) The 'name' value to tgprop for action=query&list=tags has been diff --git a/RELEASE-NOTES-1.32 b/RELEASE-NOTES-1.32 index 65d79f33cc..ad62af43c9 100644 --- a/RELEASE-NOTES-1.32 +++ b/RELEASE-NOTES-1.32 @@ -37,6 +37,10 @@ production. * (T152462) A cookie can now be set when an IP user is blocked to track that user if they move to a new IP address. This is disabled by default. * (T194950) Added 'ApiMaxLagInfo' hook. +* SpecialPage::checkLoginSecurityLevel() will now preserve POST data when + reauthenticating. +* FormSpecialPage::execute() will now call checkLoginSecurityLevel() if + getLoginSecurityLevel() returns non-false. === External library changes in 1.32 === * … @@ -55,7 +59,8 @@ production. * … === Bug fixes in 1.32 === -* … +* SpecialPage::execute() will now only call checkLoginSecurityLevel() if + getLoginSecurityLevel() returns non-false. === Action API changes in 1.32 === * Added templated parameters. diff --git a/autoload.php b/autoload.php index b4894288d3..1adc5e4ea6 100644 --- a/autoload.php +++ b/autoload.php @@ -889,6 +889,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\\DB\\PatchFileLocation' => __DIR__ . '/includes/db/PatchFileLocation.php', 'MediaWiki\\Diff\\ComplexityException' => __DIR__ . '/includes/diff/ComplexityException.php', 'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php', 'MediaWiki\\HeaderCallback' => __DIR__ . '/includes/HeaderCallback.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index f8a4e81736..56fb534754 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -8909,6 +8909,20 @@ $wgActorTableSchemaMigrationStage = MIGRATION_OLD; */ $wgExpiryWidgetNoDatePicker = false; +/** + * change_tag table schema migration stage. + * + * - MIGRATION_OLD: Do not use change_tag_def table or ct_tag_id. + * - MIGRATION_WRITE_BOTH: Write to the change_tag_def table and ct_tag_id, but read from + * the old schema. This is different from the formal definition of the constants + * - MIGRATION_WRITE_NEW: Behaves the same as MIGRATION_WRITE_BOTH + * - MIGRATION_NEW: Use the change_tag_def table and ct_tag_id, do not read/write ct_tag + * + * @since 1.32 + * @var int One of the MIGRATION_* constants + */ +$wgChangeTagsSchemaMigrationStage = MIGRATION_OLD; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/EditPage.php b/includes/EditPage.php index 13d1623d08..22c29d6413 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -3541,7 +3541,7 @@ ERROR; // Allow for site and per-namespace customization of contribution/copyright notice. Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] ); - $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title ); + $msg = wfMessage( ...$copywarnMsg )->title( $title ); if ( $langcode ) { $msg->inLanguage( $langcode ); } diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index ae09602995..6b4e4ee4df 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -2196,9 +2196,7 @@ function wfStringToBool( $val ) { * @deprecated since 1.30 use MediaWiki\Shell::escape() */ function wfEscapeShellArg( /*...*/ ) { - $args = func_get_args(); - - return call_user_func_array( Shell::class . '::escape', $args ); + return Shell::escape( ...func_get_args() ); } /** @@ -2697,10 +2695,7 @@ function wfMakeStaticArrayFile( array $data, $header = 'Automatically generated' * @return string */ function wfMemcKey( /*...*/ ) { - return call_user_func_array( - [ ObjectCache::getLocalClusterInstance(), 'makeKey' ], - func_get_args() - ); + return ObjectCache::getLocalClusterInstance()->makeKey( ...func_get_args() ); } /** @@ -2716,10 +2711,7 @@ function wfMemcKey( /*...*/ ) { function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); $keyspace = $prefix ? "$db-$prefix" : $db; - return call_user_func_array( - [ ObjectCache::getLocalClusterInstance(), 'makeKeyInternal' ], - [ $keyspace, $args ] - ); + return ObjectCache::getLocalClusterInstance()->makeKeyInternal( $keyspace, $args ); } /** @@ -2735,10 +2727,7 @@ function wfForeignMemcKey( $db, $prefix /*...*/ ) { * @return string */ function wfGlobalCacheKey( /*...*/ ) { - return call_user_func_array( - [ ObjectCache::getLocalClusterInstance(), 'makeGlobalKey' ], - func_get_args() - ); + return ObjectCache::getLocalClusterInstance()->makeGlobalKey( ...func_get_args() ); } /** diff --git a/includes/actions/Action.php b/includes/actions/Action.php index e8d9a3e402..fb22445410 100644 --- a/includes/actions/Action.php +++ b/includes/actions/Action.php @@ -104,7 +104,7 @@ abstract class Action implements MessageLocalizer { } if ( is_callable( $classOrCallable ) ) { - return call_user_func_array( $classOrCallable, [ $page, $context ] ); + return $classOrCallable( $page, $context ); } return $classOrCallable; @@ -255,7 +255,7 @@ abstract class Action implements MessageLocalizer { */ final public function msg( $key ) { $params = func_get_args(); - return call_user_func_array( [ $this->getContext(), 'msg' ], $params ); + return $this->getContext()->msg( ...$params ); } /** diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 8a99e6a97f..98aa554b1d 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -1819,7 +1819,7 @@ abstract class ApiBase extends ContextSource { if ( is_string( $msg ) ) { $msg = wfMessage( $msg ); } elseif ( is_array( $msg ) ) { - $msg = call_user_func_array( 'wfMessage', $msg ); + $msg = wfMessage( ...$msg ); } if ( !$msg instanceof Message ) { return null; @@ -1866,7 +1866,7 @@ abstract class ApiBase extends ContextSource { [ 'blockinfo' => ApiQueryUserInfo::getBlockInfo( $user->getBlock() ) ] ) ); } else { - call_user_func_array( [ $status, 'fatal' ], (array)$error ); + $status->fatal( ...(array)$error ); } } return $status; @@ -2045,10 +2045,7 @@ abstract class ApiBase extends ContextSource { if ( !$status->getErrorsByType( 'error' ) ) { $newStatus = Status::newGood(); foreach ( $status->getErrorsByType( 'warning' ) as $err ) { - call_user_func_array( - [ $newStatus, 'fatal' ], - array_merge( [ $err['message'] ], $err['params'] ) - ); + $newStatus->fatal( $err['message'], ...$err['params'] ); } if ( !$newStatus->getErrorsByType( 'error' ) ) { $newStatus->fatal( 'unknownerror-nocode' ); @@ -2085,7 +2082,7 @@ abstract class ApiBase extends ContextSource { $user = $this->getUser(); } $rights = (array)$rights; - if ( !call_user_func_array( [ $user, 'isAllowedAny' ], $rights ) ) { + if ( !$user->isAllowedAny( ...$rights ) ) { $this->dieWithError( [ 'apierror-permissiondenied', $this->msg( "action-{$rights[0]}" ) ] ); } } diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index 2adc00efae..161dd56320 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -240,7 +240,7 @@ class AuthManager implements LoggerAwareInterface { global $wgAuth; if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) { - return call_user_func_array( [ $wgAuth, $method ], $params ); + return $wgAuth->$method( ...$params ); } else { return $return; } @@ -985,7 +985,7 @@ class AuthManager implements LoggerAwareInterface { if ( $permErrors ) { $status = Status::newGood(); foreach ( $permErrors as $args ) { - call_user_func_array( [ $status, 'fatal' ], $args ); + $status->fatal( ...$args ); } return $status; } @@ -2427,7 +2427,7 @@ class AuthManager implements LoggerAwareInterface { $providers += $this->getSecondaryAuthenticationProviders(); } foreach ( $providers as $provider ) { - call_user_func_array( [ $provider, $method ], $args ); + $provider->$method( ...$args ); } } diff --git a/includes/auth/LegacyHookPreAuthenticationProvider.php b/includes/auth/LegacyHookPreAuthenticationProvider.php index 95fe3ab852..e8a276cc2d 100644 --- a/includes/auth/LegacyHookPreAuthenticationProvider.php +++ b/includes/auth/LegacyHookPreAuthenticationProvider.php @@ -168,7 +168,7 @@ class LegacyHookPreAuthenticationProvider extends AbstractPreAuthenticationProvi case LoginForm::USER_MIGRATED: $error = $msg ?: 'login-migrated-generic'; - return call_user_func_array( 'StatusValue::newFatal', (array)$error ); + return StatusValue::newFatal( ...(array)$error ); // @codeCoverageIgnoreStart case LoginForm::CREATE_BLOCKED: // Can never happen diff --git a/includes/cache/CacheDependency.php b/includes/cache/CacheDependency.php index 4ff10047a4..11df5bcffc 100644 --- a/includes/cache/CacheDependency.php +++ b/includes/cache/CacheDependency.php @@ -118,7 +118,7 @@ class DependencyWrapper { if ( is_object( $obj ) && $obj instanceof DependencyWrapper && !$obj->isExpired() ) { $value = $obj->value; } elseif ( $callback ) { - $value = call_user_func_array( $callback, $callbackParams ); + $value = $callback( ...$callbackParams ); # Cache the newly-generated value $wrapper = new DependencyWrapper( $value, $deps ); $wrapper->storeToCache( $cache, $key, $expiry ); diff --git a/includes/cache/CacheHelper.php b/includes/cache/CacheHelper.php index b2a91c2974..9db8166d50 100644 --- a/includes/cache/CacheHelper.php +++ b/includes/cache/CacheHelper.php @@ -294,7 +294,7 @@ class CacheHelper implements ICacheHelper { $args = [ $args ]; } - $value = call_user_func_array( $computeFunction, $args ); + $value = $computeFunction( ...$args ); if ( $this->cacheEnabled ) { if ( is_null( $key ) ) { @@ -350,7 +350,7 @@ class CacheHelper implements ICacheHelper { throw new MWException( 'No cache key set, so cannot obtain or save the CacheHelper values.' ); } - return call_user_func_array( 'wfMemcKey', $this->cacheKey ); + return wfMemcKey( ...$this->cacheKey ); } /** diff --git a/includes/changes/CategoryMembershipChange.php b/includes/changes/CategoryMembershipChange.php index f095b64f82..a49fb4c727 100644 --- a/includes/changes/CategoryMembershipChange.php +++ b/includes/changes/CategoryMembershipChange.php @@ -187,22 +187,19 @@ class CategoryMembershipChange { } /** @var RecentChange $rc */ - $rc = call_user_func_array( - $this->newForCategorizationCallback, - [ - $timestamp, - $categoryTitle, - $user, - $comment, - $pageTitle, - $lastRevId, - $newRevId, - $lastTimestamp, - $bot, - $ip, - $deleted, - $added - ] + $rc = ( $this->newForCategorizationCallback )( + $timestamp, + $categoryTitle, + $user, + $comment, + $pageTitle, + $lastRevId, + $newRevId, + $lastTimestamp, + $bot, + $ip, + $deleted, + $added ); $rc->save(); } diff --git a/includes/changes/ChangesListBooleanFilter.php b/includes/changes/ChangesListBooleanFilter.php index f37ed2dd6d..fc37882c02 100644 --- a/includes/changes/ChangesListBooleanFilter.php +++ b/includes/changes/ChangesListBooleanFilter.php @@ -206,18 +206,15 @@ class ChangesListBooleanFilter extends ChangesListFilter { return; } - call_user_func_array( - $this->queryCallable, - [ - get_class( $specialPage ), - $specialPage->getContext(), - $dbr, - &$tables, - &$fields, - &$conds, - &$query_options, - &$join_conds - ] + ( $this->queryCallable )( + get_class( $specialPage ), + $specialPage->getContext(), + $dbr, + $tables, + $fields, + $conds, + $query_options, + $join_conds ); } diff --git a/includes/changes/ChangesListStringOptionsFilterGroup.php b/includes/changes/ChangesListStringOptionsFilterGroup.php index 8cd7ba8dea..e06f0817ba 100644 --- a/includes/changes/ChangesListStringOptionsFilterGroup.php +++ b/includes/changes/ChangesListStringOptionsFilterGroup.php @@ -200,19 +200,16 @@ class ChangesListStringOptionsFilterGroup extends ChangesListFilterGroup { sort( $selectedValues ); - call_user_func_array( - $this->queryCallable, - [ - get_class( $specialPage ), - $specialPage->getContext(), - $dbr, - &$tables, - &$fields, - &$conds, - &$query_options, - &$join_conds, - $selectedValues - ] + ( $this->queryCallable )( + get_class( $specialPage ), + $specialPage->getContext(), + $dbr, + $tables, + $fields, + $conds, + $query_options, + $join_conds, + $selectedValues ); } diff --git a/includes/changetags/ChangeTags.php b/includes/changetags/ChangeTags.php index 429a3b69c5..0c81144531 100644 --- a/includes/changetags/ChangeTags.php +++ b/includes/changetags/ChangeTags.php @@ -261,6 +261,8 @@ class ChangeTags { &$rev_id = null, &$log_id = null, $params = null, RecentChange $rc = null, User $user = null ) { + global $wgChangeTagsSchemaMigrationStage; + $tagsToAdd = array_filter( (array)$tagsToAdd ); // Make sure we're submitting all tags... $tagsToRemove = array_filter( (array)$tagsToRemove ); @@ -342,6 +344,35 @@ class ChangeTags { // insert a row into change_tag for each new tag if ( count( $tagsToAdd ) ) { + $changeTagMapping = []; + if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + $tagDefRows = []; + foreach ( $tagsToAdd as $tag ) { + $tagDefRows[] = [ + 'ctd_name' => $tag, + 'ctd_user_defined' => 0, + 'ctd_count' => 1 + ]; + } + + $dbw->upsert( + 'change_tag_def', + $tagDefRows, + [ 'ctd_name' ], + [ 'ctd_count = ctd_count + 1' ], + __METHOD__ + ); + + $res = $dbw->select( + 'change_tag_def', + [ 'ctd_name', 'ctd_id' ], + [ 'ctd_name' => $tagsToAdd ] + ); + foreach ( $res as $row ) { + $changeTagMapping[$row->ctd_name] = $row->ctd_id; + } + } + $tagsRows = []; foreach ( $tagsToAdd as $tag ) { // Filter so we don't insert NULLs as zero accidentally. @@ -354,9 +385,11 @@ class ChangeTags { 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, - 'ct_params' => $params + 'ct_params' => $params, + 'ct_tag_id' => isset( $changeTagMapping[$tag] ) ? $changeTagMapping[$tag] : null, ] ); + } $dbw->insert( 'change_tag', $tagsRows, __METHOD__, [ 'IGNORE' ] ); @@ -374,6 +407,20 @@ class ChangeTags { ] ); $dbw->delete( 'change_tag', $conds, __METHOD__ ); + if ( $dbw->affectedRows() && $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + $dbw->update( + 'change_tag_def', + [ 'ctd_count = ctd_count - 1' ], + [ 'ctd_name' => $tag ], + __METHOD__ + ); + + $dbw->delete( + 'change_tag_def', + [ 'ctd_name' => $tag, 'ctd_count' => 0, 'ctd_user_defined' => 0 ], + __METHOD__ + ); + } } } @@ -828,8 +875,8 @@ class ChangeTags { } /** - * Defines a tag in the valid_tag table, without checking that the tag name - * is valid. + * Defines a tag in the valid_tag table and/or update ctd_user_defined field in change_tag_def, + * without checking that the tag name is valid. * Extensions should NOT use this function; they can use the ListDefinedTags * hook instead. * @@ -837,26 +884,63 @@ class ChangeTags { * @since 1.25 */ public static function defineTag( $tag ) { + global $wgChangeTagsSchemaMigrationStage; + $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'valid_tag', + if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + $tagDef = [ + 'ctd_name' => $tag, + 'ctd_user_defined' => 1, + 'ctd_count' => 0 + ]; + $dbw->upsert( + 'change_tag_def', + $tagDef, + [ 'ctd_name' ], + [ 'ctd_user_defined' => 1 ], + __METHOD__ + ); + } + + $dbw->replace( + 'valid_tag', [ 'vt_tag' ], [ 'vt_tag' => $tag ], - __METHOD__ ); + __METHOD__ + ); // clear the memcache of defined tags self::purgeTagCacheAll(); } /** - * Removes a tag from the valid_tag table. The tag may remain in use by - * extensions, and may still show up as 'defined' if an extension is setting - * it from the ListDefinedTags hook. + * Removes a tag from the valid_tag table and/or update ctd_user_defined field in change_tag_def. + * The tag may remain in use by extensions, and may still show up as 'defined' + * if an extension is setting it from the ListDefinedTags hook. * * @param string $tag Tag to remove * @since 1.25 */ public static function undefineTag( $tag ) { + global $wgChangeTagsSchemaMigrationStage; + $dbw = wfGetDB( DB_MASTER ); + + if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + $dbw->update( + 'change_tag_def', + [ 'ctd_name' => $tag ], + [ 'ctd_user_defined' => 0 ], + __METHOD__ + ); + + $dbw->delete( + 'change_tag_def', + [ 'ctd_name' => $tag, 'ctd_count' => 0 ], + __METHOD__ + ); + } + $dbw->delete( 'valid_tag', [ 'vt_tag' => $tag ], __METHOD__ ); // clear the memcache of defined tags @@ -1108,6 +1192,7 @@ class ChangeTags { /** * Creates a tag by adding a row to the `valid_tag` table. + * and/or add it to `change_tag_def` table. * * Extensions should NOT use this function; they can use the ListDefinedTags * hook instead. @@ -1158,10 +1243,11 @@ class ChangeTags { * @since 1.25 */ public static function deleteTagEverywhere( $tag ) { + global $wgChangeTagsSchemaMigrationStage; $dbw = wfGetDB( DB_MASTER ); $dbw->startAtomic( __METHOD__ ); - // delete from valid_tag + // delete from valid_tag and/or set ctd_user_defined = 0 self::undefineTag( $tag ); // find out which revisions use this tag, so we can delete from tag_summary @@ -1180,6 +1266,10 @@ class ChangeTags { // delete from change_tag $dbw->delete( 'change_tag', [ 'ct_tag' => $tag ], __METHOD__ ); + if ( $wgChangeTagsSchemaMigrationStage > MIGRATION_OLD ) { + $dbw->delete( 'change_tag_def', [ 'ctd_name' => $tag ], __METHOD__ ); + } + $dbw->endAtomic( __METHOD__ ); // give extensions a chance diff --git a/includes/context/ContextSource.php b/includes/context/ContextSource.php index 03fb9e2bbf..618253859d 100644 --- a/includes/context/ContextSource.php +++ b/includes/context/ContextSource.php @@ -168,7 +168,7 @@ abstract class ContextSource implements IContextSource { public function msg( $key /* $args */ ) { $args = func_get_args(); - return call_user_func_array( [ $this->getContext(), 'msg' ], $args ); + return $this->getContext()->msg( ...$args ); } /** diff --git a/includes/context/DerivativeContext.php b/includes/context/DerivativeContext.php index f7a1815d02..acf6fcb9fc 100644 --- a/includes/context/DerivativeContext.php +++ b/includes/context/DerivativeContext.php @@ -296,6 +296,6 @@ class DerivativeContext extends ContextSource implements MutableContext { public function msg( $key ) { $args = func_get_args(); - return call_user_func_array( 'wfMessage', $args )->setContext( $this ); + return wfMessage( ...$args )->setContext( $this ); } } diff --git a/includes/context/RequestContext.php b/includes/context/RequestContext.php index db3a7a9695..7563330697 100644 --- a/includes/context/RequestContext.php +++ b/includes/context/RequestContext.php @@ -423,7 +423,7 @@ class RequestContext implements IContextSource, MutableContext { public function msg( $key ) { $args = func_get_args(); - return call_user_func_array( 'wfMessage', $args )->setContext( $this ); + return wfMessage( ...$args )->setContext( $this ); } /** diff --git a/includes/db/PatchFileLocation.php b/includes/db/PatchFileLocation.php new file mode 100644 index 0000000000..013724c343 --- /dev/null +++ b/includes/db/PatchFileLocation.php @@ -0,0 +1,90 @@ +getType(); + + if ( $patchDir === null ) { + $patchDir = $GLOBALS['IP'] . '/maintenance'; + } + + $paths = [ + + // For a small number of patch files, closely associated with code, + // e.g. for unit tests: + "$patchDir/$name.$dbType.sql", + + // For a large number of patch files, e.g. for schema updates of extensions: + "$patchDir/$dbType/$name.sql", + + // For MediaWiki core schema update patches: + "$patchDir/$dbType/archives/$name.sql", + + // Database-agnostic fallback: + "$patchDir/$name.sql", + + // Database-agnostic fallback for MediaWiki core schema update patches: + "$patchDir/archives/$name.sql" + ]; + + foreach ( $paths as $p ) { + if ( file_exists( $p ) ) { + return $p; + } + } + + throw new RuntimeException( "No SQL script matching $name could be found in $patchDir" ); + } + +} diff --git a/includes/debug/logger/MonologSpi.php b/includes/debug/logger/MonologSpi.php index 670ba391e7..ff653ab241 100644 --- a/includes/debug/logger/MonologSpi.php +++ b/includes/debug/logger/MonologSpi.php @@ -199,7 +199,7 @@ class MonologSpi implements Spi { if ( isset( $spec['calls'] ) ) { foreach ( $spec['calls'] as $method => $margs ) { - call_user_func_array( [ $obj, $method ], $margs ); + $obj->$method( ...$margs ); } } diff --git a/includes/deferred/AutoCommitUpdate.php b/includes/deferred/AutoCommitUpdate.php index f9297af584..85071576b0 100644 --- a/includes/deferred/AutoCommitUpdate.php +++ b/includes/deferred/AutoCommitUpdate.php @@ -39,7 +39,7 @@ class AutoCommitUpdate implements DeferrableUpdate, DeferrableCallback { try { /** @var Exception $e */ $e = null; - call_user_func_array( $this->callback, [ $this->dbw, $this->fname ] ); + ( $this->callback )( $this->dbw, $this->fname ); } catch ( Exception $e ) { } if ( $autoTrx ) { diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index b1ca435269..25ba36ac10 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -756,10 +756,7 @@ class DifferenceEngine extends ContextSource { // for backwards-compatibility $key = $this->getDiffBodyCacheKey(); if ( $key === null ) { - $key = call_user_func_array( - [ $cache, 'makeKey' ], - $this->getDiffBodyCacheKeyParams() - ); + $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() ); } // Try cache diff --git a/includes/filerepo/FileRepo.php b/includes/filerepo/FileRepo.php index 89bb81a7c0..70068b998b 100644 --- a/includes/filerepo/FileRepo.php +++ b/includes/filerepo/FileRepo.php @@ -1735,7 +1735,7 @@ class FileRepo { * @return Status */ public function newFatal( $message /*, parameters...*/ ) { - $status = call_user_func_array( [ Status::class, 'newFatal' ], func_get_args() ); + $status = Status::newFatal( ...func_get_args() ); $status->cleanCallback = $this->getErrorCleanupFunction(); return $status; @@ -1840,7 +1840,7 @@ class FileRepo { $args = func_get_args(); array_unshift( $args, 'filerepo', $this->getName() ); - return call_user_func_array( 'wfMemcKey', $args ); + return wfMemcKey( ...$args ); } /** diff --git a/includes/filerepo/ForeignDBRepo.php b/includes/filerepo/ForeignDBRepo.php index 7879448bb6..4b331389b7 100644 --- a/includes/filerepo/ForeignDBRepo.php +++ b/includes/filerepo/ForeignDBRepo.php @@ -132,9 +132,8 @@ class ForeignDBRepo extends LocalRepo { function getSharedCacheKey( /*...*/ ) { if ( $this->hasSharedCache() ) { $args = func_get_args(); - array_unshift( $args, $this->dbName, $this->tablePrefix ); - return call_user_func_array( 'wfForeignMemcKey', $args ); + return wfForeignMemcKey( $this->dbName, $this->tablePrefix, ...$args ); } else { return false; } diff --git a/includes/filerepo/LocalRepo.php b/includes/filerepo/LocalRepo.php index 03a9d44168..9a6ef22f17 100644 --- a/includes/filerepo/LocalRepo.php +++ b/includes/filerepo/LocalRepo.php @@ -507,7 +507,7 @@ class LocalRepo extends FileRepo { function getSharedCacheKey( /*...*/ ) { $args = func_get_args(); - return call_user_func_array( 'wfMemcKey', $args ); + return wfMemcKey( ...$args ); } /** @@ -590,7 +590,7 @@ class LocalRepo extends FileRepo { wfDebug( __METHOD__ . ": skipped because storage uses sha1 paths\n" ); return Status::newGood(); } else { - return call_user_func_array( 'parent::' . $function, $args ); + return parent::$function( ...$args ); } } } diff --git a/includes/installer/CliInstaller.php b/includes/installer/CliInstaller.php index 2264b80ddb..845408abd7 100644 --- a/includes/installer/CliInstaller.php +++ b/includes/installer/CliInstaller.php @@ -197,7 +197,7 @@ class CliInstaller extends Installer { if ( count( $warnings ) !== 0 ) { foreach ( $warnings as $w ) { - call_user_func_array( [ $this, 'showMessage' ], $w ); + $this->showMessage( ...$w ); } } diff --git a/includes/specialpage/FormSpecialPage.php b/includes/specialpage/FormSpecialPage.php index 66c7d47ea9..81a0036e83 100644 --- a/includes/specialpage/FormSpecialPage.php +++ b/includes/specialpage/FormSpecialPage.php @@ -35,6 +35,12 @@ abstract class FormSpecialPage extends SpecialPage { */ protected $par = null; + /** + * @var array|null POST data preserved across re-authentication + * @since 1.32 + */ + protected $reauthPostData = null; + /** * Get an HTMLForm descriptor array * @return array @@ -89,13 +95,31 @@ abstract class FormSpecialPage extends SpecialPage { * @return HTMLForm|null */ protected function getForm() { + $context = $this->getContext(); + $onSubmit = [ $this, 'onSubmit' ]; + + if ( $this->reauthPostData ) { + // Restore POST data + $context = new DerivativeContext( $context ); + $oldRequest = $this->getRequest(); + $context->setRequest( new DerivativeRequest( + $oldRequest, $this->reauthPostData + $oldRequest->getQueryValues(), true + ) ); + + // But don't treat it as a "real" submission just in case of some + // crazy kind of CSRF. + $onSubmit = function () { + return false; + }; + } + $form = HTMLForm::factory( $this->getDisplayFormat(), $this->getFormFields(), - $this->getContext(), + $context, $this->getMessagePrefix() ); - $form->setSubmitCallback( [ $this, 'onSubmit' ] ); + $form->setSubmitCallback( $onSubmit ); if ( $this->getDisplayFormat() !== 'ooui' ) { // No legend and wrapper by default in OOUI forms, but can be set manually // from alterForm() @@ -151,6 +175,11 @@ abstract class FormSpecialPage extends SpecialPage { // This will throw exceptions if there's a problem $this->checkExecutePermissions( $this->getUser() ); + $securityLevel = $this->getLoginSecurityLevel(); + if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { + return; + } + $form = $this->getForm(); if ( $form->show() ) { $this->onSuccess(); @@ -199,4 +228,14 @@ abstract class FormSpecialPage extends SpecialPage { public function requiresUnblock() { return true; } + + /** + * Preserve POST data across reauthentication + * + * @since 1.32 + * @param array $data + */ + protected function setReauthPostData( array $data ) { + $this->reauthPostData = $data; + } } diff --git a/includes/specialpage/SpecialPage.php b/includes/specialpage/SpecialPage.php index 6828b4a400..317aa0d7c8 100644 --- a/includes/specialpage/SpecialPage.php +++ b/includes/specialpage/SpecialPage.php @@ -352,6 +352,23 @@ class SpecialPage implements MessageLocalizer { return false; } + /** + * Record preserved POST data after a reauthentication. + * + * This is called from checkLoginSecurityLevel() when returning from the + * redirect for reauthentication, if the redirect had been served in + * response to a POST request. + * + * The base SpecialPage implementation does nothing. If your subclass uses + * getLoginSecurityLevel() or checkLoginSecurityLevel(), it should probably + * implement this to do something with the data. + * + * @since 1.32 + * @param array $data + */ + protected function setReauthPostData( array $data ) { + } + /** * Verifies that the user meets the security level, possibly reauthenticating them in the process. * @@ -378,16 +395,42 @@ class SpecialPage implements MessageLocalizer { */ protected function checkLoginSecurityLevel( $level = null ) { $level = $level ?: $this->getName(); + $key = 'SpecialPage:reauth:' . $this->getName(); + $request = $this->getRequest(); + $securityStatus = AuthManager::singleton()->securitySensitiveOperationStatus( $level ); if ( $securityStatus === AuthManager::SEC_OK ) { + $uniqueId = $request->getVal( 'postUniqueId' ); + if ( $uniqueId ) { + $key = $key . ':' . $uniqueId; + $session = $request->getSession(); + $data = $session->getSecret( $key ); + if ( $data ) { + $session->remove( $key ); + $this->setReauthPostData( $data ); + } + } return true; } elseif ( $securityStatus === AuthManager::SEC_REAUTH ) { - $request = $this->getRequest(); $title = self::getTitleFor( 'Userlogin' ); + $queryParams = $request->getQueryValues(); + + if ( $request->wasPosted() ) { + $data = array_diff_assoc( $request->getValues(), $request->getQueryValues() ); + if ( $data ) { + // unique ID in case the same special page is open in multiple browser tabs + $uniqueId = MWCryptRand::generateHex( 6 ); + $key = $key . ':' . $uniqueId; + $queryParams['postUniqueId'] = $uniqueId; + $session = $request->getSession(); + $session->persist(); // Just in case + $session->setSecret( $key, $data ); + } + } + $query = [ 'returnto' => $this->getFullTitle()->getPrefixedDBkey(), - 'returntoquery' => wfArrayToCgi( array_diff_key( $request->getQueryValues(), - [ 'title' => true ] ) ), + 'returntoquery' => wfArrayToCgi( array_diff_key( $queryParams, [ 'title' => true ] ) ), 'force' => $level, ]; $url = $title->getFullURL( $query, false, PROTO_HTTPS ); @@ -568,7 +611,10 @@ class SpecialPage implements MessageLocalizer { public function execute( $subPage ) { $this->setHeaders(); $this->checkPermissions(); - $this->checkLoginSecurityLevel( $this->getLoginSecurityLevel() ); + $securityLevel = $this->getLoginSecurityLevel(); + if ( $securityLevel !== false && !$this->checkLoginSecurityLevel( $securityLevel ) ) { + return; + } $this->outputHeader(); } diff --git a/includes/upload/UploadBase.php b/includes/upload/UploadBase.php index 27c0ed7730..6a471ba573 100644 --- a/includes/upload/UploadBase.php +++ b/includes/upload/UploadBase.php @@ -1397,7 +1397,7 @@ abstract class UploadBase { */ public static function checkXMLEncodingMissmatch( $file ) { global $wgSVGMetadataCutoff; - $contents = file_get_contents( $file, false, null, -1, $wgSVGMetadataCutoff ); + $contents = file_get_contents( $file, false, null, 0, $wgSVGMetadataCutoff ); $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si'; if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) { diff --git a/maintenance/jsduck/eg-iframe.html b/maintenance/jsduck/eg-iframe.html index 4c02998d19..f19a69b34b 100644 --- a/maintenance/jsduck/eg-iframe.html +++ b/maintenance/jsduck/eg-iframe.html @@ -43,6 +43,7 @@ + diff --git a/resources/src/mediawiki/mediawiki.base.js b/resources/src/mediawiki/mediawiki.base.js index b68d779358..43ee202f93 100644 --- a/resources/src/mediawiki/mediawiki.base.js +++ b/resources/src/mediawiki/mediawiki.base.js @@ -16,6 +16,208 @@ */ /* globals mw */ ( function () { + 'use strict'; + + var slice = Array.prototype.slice; + + /** + * Object constructor for messages. + * + * Similar to the Message class in MediaWiki PHP. + * + * Format defaults to 'text'. + * + * @example + * + * var obj, str; + * mw.messages.set( { + * 'hello': 'Hello world', + * 'hello-user': 'Hello, $1!', + * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3' + * } ); + * + * obj = new mw.Message( mw.messages, 'hello' ); + * mw.log( obj.text() ); + * // Hello world + * + * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] ); + * mw.log( obj.text() ); + * // Hello, John Doe! + * + * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] ); + * mw.log( obj.text() ); + * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago + * + * // Using mw.message shortcut + * obj = mw.message( 'hello-user', 'John Doe' ); + * mw.log( obj.text() ); + * // Hello, John Doe! + * + * // Using mw.msg shortcut + * str = mw.msg( 'hello-user', 'John Doe' ); + * mw.log( str ); + * // Hello, John Doe! + * + * // Different formats + * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] ); + * + * obj.format = 'text'; + * str = obj.toString(); + * // Same as: + * str = obj.text(); + * + * mw.log( str ); + * // Hello, John "Wiki" <3 Doe! + * + * mw.log( obj.escaped() ); + * // Hello, John "Wiki" <3 Doe! + * + * @class mw.Message + * + * @constructor + * @param {mw.Map} map Message store + * @param {string} key + * @param {Array} [parameters] + */ + function Message( map, key, parameters ) { + this.format = 'text'; + this.map = map; + this.key = key; + this.parameters = parameters === undefined ? [] : slice.call( parameters ); + return this; + } + + Message.prototype = { + /** + * Get parsed contents of the message. + * + * The default parser does simple $N replacements and nothing else. + * This may be overridden to provide a more complex message parser. + * The primary override is in the mediawiki.jqueryMsg module. + * + * This function will not be called for nonexistent messages. + * + * @return {string} Parsed message + */ + parser: function () { + return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) ); + }, + + /** + * Add (does not replace) parameters for `$N` placeholder values. + * + * @param {Array} parameters + * @return {mw.Message} + * @chainable + */ + params: function ( parameters ) { + var i; + for ( i = 0; i < parameters.length; i++ ) { + this.parameters.push( parameters[ i ] ); + } + return this; + }, + + /** + * Convert message object to its string form based on current format. + * + * @return {string} Message as a string in the current form, or `` if key + * does not exist. + */ + toString: function () { + var text; + + if ( !this.exists() ) { + // Use ⧼key⧽ as text if key does not exist + // Err on the side of safety, ensure that the output + // is always html safe in the event the message key is + // missing, since in that case its highly likely the + // message key is user-controlled. + // '⧼' is used instead of '<' to side-step any + // double-escaping issues. + // (Keep synchronised with Message::toString() in PHP.) + return '⧼' + mw.html.escape( this.key ) + '⧽'; + } + + if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { + text = this.parser(); + } + + if ( this.format === 'escaped' ) { + text = this.parser(); + text = mw.html.escape( text ); + } + + return text; + }, + + /** + * Change format to 'parse' and convert message to string + * + * If jqueryMsg is loaded, this parses the message text from wikitext + * (where supported) to HTML + * + * Otherwise, it is equivalent to plain. + * + * @return {string} String form of parsed message + */ + parse: function () { + this.format = 'parse'; + return this.toString(); + }, + + /** + * Change format to 'plain' and convert message to string + * + * This substitutes parameters, but otherwise does not change the + * message text. + * + * @return {string} String form of plain message + */ + plain: function () { + this.format = 'plain'; + return this.toString(); + }, + + /** + * Change format to 'text' and convert message to string + * + * If jqueryMsg is loaded, {{-transformation is done where supported + * (such as {{plural:}}, {{gender:}}, {{int:}}). + * + * Otherwise, it is equivalent to plain + * + * @return {string} String form of text message + */ + text: function () { + this.format = 'text'; + return this.toString(); + }, + + /** + * Change the format to 'escaped' and convert message to string + * + * This is equivalent to using the 'text' format (see #text), then + * HTML-escaping the output. + * + * @return {string} String form of html escaped message + */ + escaped: function () { + this.format = 'escaped'; + return this.toString(); + }, + + /** + * Check if a message exists + * + * @see mw.Map#exists + * @return {boolean} + */ + exists: function () { + return this.map.exists( this.key ); + } + }; + /** * @class mw * @singleton @@ -31,4 +233,54 @@ mw.inspect.runReports.apply( mw.inspect, args ); } ); }; + + /** + * Format a string. Replace $1, $2 ... $N with positional arguments. + * + * Used by Message#parser(). + * + * @since 1.25 + * @param {string} formatString Format string + * @param {...Mixed} parameters Values for $N replacements + * @return {string} Formatted string + */ + mw.format = function ( formatString ) { + var parameters = slice.call( arguments, 1 ); + return formatString.replace( /\$(\d+)/g, function ( str, match ) { + var index = parseInt( match, 10 ) - 1; + return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; + } ); + }; + + // Expose Message constructor + mw.Message = Message; + + /** + * Get a message object. + * + * Shortcut for `new mw.Message( mw.messages, key, parameters )`. + * + * @see mw.Message + * @param {string} key Key of message to get + * @param {...Mixed} parameters Values for $N replacements + * @return {mw.Message} + */ + mw.message = function ( key ) { + var parameters = slice.call( arguments, 1 ); + return new Message( mw.messages, key, parameters ); + }; + + /** + * Get a message string using the (default) 'text' format. + * + * Shortcut for `mw.message( key, parameters... ).text()`. + * + * @see mw.Message + * @param {string} key Key of message to get + * @param {...Mixed} parameters Values for $N replacements + * @return {string} + */ + mw.msg = function () { + return mw.message.apply( mw.message, arguments ).toString(); + }; }() ); diff --git a/resources/src/mediawiki/mediawiki.js b/resources/src/mediawiki/mediawiki.js index c6a3bded12..9d34bf1058 100644 --- a/resources/src/mediawiki/mediawiki.js +++ b/resources/src/mediawiki/mediawiki.js @@ -220,204 +220,6 @@ } }; - /** - * Object constructor for messages. - * - * Similar to the Message class in MediaWiki PHP. - * - * Format defaults to 'text'. - * - * @example - * - * var obj, str; - * mw.messages.set( { - * 'hello': 'Hello world', - * 'hello-user': 'Hello, $1!', - * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3' - * } ); - * - * obj = new mw.Message( mw.messages, 'hello' ); - * mw.log( obj.text() ); - * // Hello world - * - * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] ); - * mw.log( obj.text() ); - * // Hello, John Doe! - * - * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] ); - * mw.log( obj.text() ); - * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago - * - * // Using mw.message shortcut - * obj = mw.message( 'hello-user', 'John Doe' ); - * mw.log( obj.text() ); - * // Hello, John Doe! - * - * // Using mw.msg shortcut - * str = mw.msg( 'hello-user', 'John Doe' ); - * mw.log( str ); - * // Hello, John Doe! - * - * // Different formats - * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] ); - * - * obj.format = 'text'; - * str = obj.toString(); - * // Same as: - * str = obj.text(); - * - * mw.log( str ); - * // Hello, John "Wiki" <3 Doe! - * - * mw.log( obj.escaped() ); - * // Hello, John "Wiki" <3 Doe! - * - * @class mw.Message - * - * @constructor - * @param {mw.Map} map Message store - * @param {string} key - * @param {Array} [parameters] - */ - function Message( map, key, parameters ) { - this.format = 'text'; - this.map = map; - this.key = key; - this.parameters = parameters === undefined ? [] : slice.call( parameters ); - return this; - } - - Message.prototype = { - /** - * Get parsed contents of the message. - * - * The default parser does simple $N replacements and nothing else. - * This may be overridden to provide a more complex message parser. - * The primary override is in the mediawiki.jqueryMsg module. - * - * This function will not be called for nonexistent messages. - * - * @return {string} Parsed message - */ - parser: function () { - return mw.format.apply( null, [ this.map.get( this.key ) ].concat( this.parameters ) ); - }, - - /** - * Add (does not replace) parameters for `$N` placeholder values. - * - * @param {Array} parameters - * @return {mw.Message} - * @chainable - */ - params: function ( parameters ) { - var i; - for ( i = 0; i < parameters.length; i++ ) { - this.parameters.push( parameters[ i ] ); - } - return this; - }, - - /** - * Convert message object to its string form based on current format. - * - * @return {string} Message as a string in the current form, or `` if key - * does not exist. - */ - toString: function () { - var text; - - if ( !this.exists() ) { - // Use ⧼key⧽ as text if key does not exist - // Err on the side of safety, ensure that the output - // is always html safe in the event the message key is - // missing, since in that case its highly likely the - // message key is user-controlled. - // '⧼' is used instead of '<' to side-step any - // double-escaping issues. - // (Keep synchronised with Message::toString() in PHP.) - return '⧼' + mw.html.escape( this.key ) + '⧽'; - } - - if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) { - text = this.parser(); - } - - if ( this.format === 'escaped' ) { - text = this.parser(); - text = mw.html.escape( text ); - } - - return text; - }, - - /** - * Change format to 'parse' and convert message to string - * - * If jqueryMsg is loaded, this parses the message text from wikitext - * (where supported) to HTML - * - * Otherwise, it is equivalent to plain. - * - * @return {string} String form of parsed message - */ - parse: function () { - this.format = 'parse'; - return this.toString(); - }, - - /** - * Change format to 'plain' and convert message to string - * - * This substitutes parameters, but otherwise does not change the - * message text. - * - * @return {string} String form of plain message - */ - plain: function () { - this.format = 'plain'; - return this.toString(); - }, - - /** - * Change format to 'text' and convert message to string - * - * If jqueryMsg is loaded, {{-transformation is done where supported - * (such as {{plural:}}, {{gender:}}, {{int:}}). - * - * Otherwise, it is equivalent to plain - * - * @return {string} String form of text message - */ - text: function () { - this.format = 'text'; - return this.toString(); - }, - - /** - * Change the format to 'escaped' and convert message to string - * - * This is equivalent to using the 'text' format (see #text), then - * HTML-escaping the output. - * - * @return {string} String form of html escaped message - */ - escaped: function () { - this.format = 'escaped'; - return this.toString(); - }, - - /** - * Check if a message exists - * - * @see mw.Map#exists - * @return {boolean} - */ - exists: function () { - return this.map.exists( this.key ); - } - }; - defineFallbacks(); /* eslint-disable no-console */ @@ -557,24 +359,6 @@ function () { return Date.now(); }; }() ), - /** - * Format a string. Replace $1, $2 ... $N with positional arguments. - * - * Used by Message#parser(). - * - * @since 1.25 - * @param {string} formatString Format string - * @param {...Mixed} parameters Values for $N replacements - * @return {string} Formatted string - */ - format: function ( formatString ) { - var parameters = slice.call( arguments, 1 ); - return formatString.replace( /\$(\d+)/g, function ( str, match ) { - var index = parseInt( match, 10 ) - 1; - return parameters[ index ] !== undefined ? parameters[ index ] : '$' + match; - } ); - }, - /** * Track an analytic event. * @@ -646,9 +430,6 @@ // Expose Map constructor Map: Map, - // Expose Message constructor - Message: Message, - /** * Map of configuration values. * @@ -699,35 +480,6 @@ */ templates: new Map(), - /** - * Get a message object. - * - * Shortcut for `new mw.Message( mw.messages, key, parameters )`. - * - * @see mw.Message - * @param {string} key Key of message to get - * @param {...Mixed} parameters Values for $N replacements - * @return {mw.Message} - */ - message: function ( key ) { - var parameters = slice.call( arguments, 1 ); - return new Message( mw.messages, key, parameters ); - }, - - /** - * Get a message string using the (default) 'text' format. - * - * Shortcut for `mw.message( key, parameters... ).text()`. - * - * @see mw.Message - * @param {string} key Key of message to get - * @param {...Mixed} parameters Values for $N replacements - * @return {string} - */ - msg: function () { - return mw.message.apply( mw.message, arguments ).toString(); - }, - // Expose mw.log log: log, @@ -1135,11 +887,9 @@ for ( i = 0; i < deps.length; i++ ) { if ( resolved.indexOf( deps[ i ] ) === -1 ) { if ( unresolved.has( deps[ i ] ) ) { - throw new Error( mw.format( - 'Circular reference detected: $1 -> $2', - module, - deps[ i ] - ) ); + throw new Error( + 'Circular reference detected: ' + module + ' -> ' + deps[ i ] + ); } unresolved.add( module ); diff --git a/tests/common/TestsAutoLoader.php b/tests/common/TestsAutoLoader.php index abf718d07e..a79867913e 100644 --- a/tests/common/TestsAutoLoader.php +++ b/tests/common/TestsAutoLoader.php @@ -149,8 +149,11 @@ $wgAutoloadClasses += [ 'SpecialPageExecutor' => "$testDir/phpunit/includes/specials/SpecialPageExecutor.php", # tests/phpunit/includes/Storage + 'MediaWiki\Tests\Storage\McrSchemaDetection' => "$testDir/phpunit/includes/Storage/McrSchemaDetection.php", 'MediaWiki\Tests\Storage\RevisionSlotsTest' => "$testDir/phpunit/includes/Storage/RevisionSlotsTest.php", 'MediaWiki\Tests\Storage\RevisionRecordTests' => "$testDir/phpunit/includes/Storage/RevisionRecordTests.php", + 'MediaWiki\Tests\Storage\RevisionStoreDbTestBase' => "$testDir/phpunit/includes/Storage/RevisionStoreDbTestBase.php", + 'MediaWiki\Tests\Storage\PreMcrSchemaOverride' => "$testDir/phpunit/includes/Storage/PreMcrSchemaOverride.php", # tests/phpunit/languages 'LanguageClassesTestCase' => "$testDir/phpunit/languages/LanguageClassesTestCase.php", diff --git a/tests/phpunit/MediaWikiTestCase.php b/tests/phpunit/MediaWikiTestCase.php index cf9dde0160..6bcbd938d8 100644 --- a/tests/phpunit/MediaWikiTestCase.php +++ b/tests/phpunit/MediaWikiTestCase.php @@ -402,10 +402,6 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { if ( !self::$dbSetup ) { $this->setupAllTestDBs(); $this->addCoreDBData(); - - if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) { - $this->resetDB( $this->db, $this->tablesUsed ); - } } // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB @@ -413,6 +409,7 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { // This would also remove the need for the HACK that is oncePerClass(). if ( $this->oncePerClass() ) { $this->setUpSchema( $this->db ); + $this->resetDB( $this->db, $this->tablesUsed ); $this->addDBDataOnce(); } @@ -1566,6 +1563,19 @@ abstract class MediaWikiTestCase extends PHPUnit\Framework\TestCase { $tablesUsed = array_unique( array_merge( $tablesUsed, $pageTables ) ); } + // Postgres, Oracle, and MSSQL all use mwuser/pagecontent + // instead of user/text. But Postgres does not remap the + // table name in tableExists(), so we mark the real table + // names as being used. + if ( $db->getType() === 'postgres' ) { + if ( in_array( 'user', $tablesUsed ) ) { + $tablesUsed[] = 'mwuser'; + } + if ( in_array( 'text', $tablesUsed ) ) { + $tablesUsed[] = 'pagecontent'; + } + } + $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] ); foreach ( $tablesUsed as $tbl ) { // TODO: reset interwiki table to its original content. diff --git a/tests/phpunit/includes/RevisionContentHandlerDbTest.php b/tests/phpunit/includes/RevisionContentHandlerDbTest.php deleted file mode 100644 index fa0153d3e0..0000000000 --- a/tests/phpunit/includes/RevisionContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -tablesUsed += $this->getMcrTablesToReset(); + parent::setUp(); $this->mergeMwGlobalArrayValue( @@ -72,11 +84,17 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { ); $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); MWNamespace::clearCaches(); // Reset namespace cache $wgContLang->resetNamespaces(); + $this->overrideMwServices(); + if ( !$this->testPage ) { /** * We have to create a new page for each subclass as the page creation may result @@ -1346,6 +1364,7 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase { */ public function testNewKnownCurrent() { // Setup the services + $this->resetGlobalServices(); $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); $this->setService( 'MainWANObjectCache', $cache ); $db = wfGetDB( DB_MASTER ); diff --git a/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php b/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php deleted file mode 100644 index c980a487f7..0000000000 --- a/tests/phpunit/includes/RevisionNoContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -tableExists( 'slots', __METHOD__ ); + } + + /** + * Returns true if pre-MCR fields still exist in the database. + * If yes, the database is compatible with with MIGRATION_OLD mode. + * If hasMcrTables() also returns true, the database supports MIGRATION_WRITE_BOTH mode. + * + * Note that if the database has been updated in MIGRATION_NEW mode, + * the rev_text_id field will be 0 for new revisions. This means that + * in MIGRATION_OLD mode, reading such revisions will fail, even though + * all the necessary fields exist. + * This is not relevant for unit tests, since unit tests reset the database content anyway. + * + * @param IDatabase $db + * @return bool + */ + protected function hasPreMcrFields( IDatabase $db ) { + return $db->fieldExists( 'revision', 'rev_content_model', __METHOD__ ); + } + +} diff --git a/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php new file mode 100644 index 0000000000..c77a94a2c9 --- /dev/null +++ b/tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php @@ -0,0 +1,114 @@ + [ 'archive' ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + ] + ), + 'joins' => [], + ] + ]; + } + + public function provideGetQueryInfo() { + yield [ + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + [ 'page' ], + [ + 'tables' => [ 'revision', 'page' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + ], + ] + ]; + yield [ + [ 'user' ], + [ + 'tables' => [ 'revision', 'user' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'user_name', + ] + ), + 'joins' => [ + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + ], + ] + ]; + yield [ + [ 'text' ], + [ + 'tables' => [ 'revision', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + [ + 'old_text', + 'old_flags', + ] + ), + 'joins' => [ + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + } + +} diff --git a/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php new file mode 100644 index 0000000000..4336691185 --- /dev/null +++ b/tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php @@ -0,0 +1,84 @@ + [ 'archive' ], + 'fields' => array_merge( + $this->getDefaultArchiveFields(), + [ + 'ar_comment_text' => 'ar_comment', + 'ar_comment_data' => 'NULL', + 'ar_comment_cid' => 'NULL', + 'ar_user_text' => 'ar_user_text', + 'ar_user' => 'ar_user', + 'ar_actor' => 'NULL', + 'ar_content_format', + 'ar_content_model', + ] + ), + 'joins' => [], + ] + ]; + } + + public function provideGetQueryInfo() { + yield [ + [], + [ + 'tables' => [ 'revision' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields() + ), + 'joins' => [], + ] + ]; + yield [ + [ 'page', 'user', 'text' ], + [ + 'tables' => [ 'revision', 'page', 'user', 'text' ], + 'fields' => array_merge( + $this->getDefaultQueryFields(), + $this->getCommentQueryFields(), + $this->getActorQueryFields(), + $this->getContentHandlerQueryFields(), + [ + 'page_namespace', + 'page_title', + 'page_id', + 'page_latest', + 'page_is_redirect', + 'page_len', + 'user_name', + 'old_text', + 'old_flags' + ] + ), + 'joins' => [ + 'page' => [ 'INNER JOIN', [ 'page_id = rev_page' ] ], + 'user' => [ 'LEFT JOIN', [ 'rev_user != 0', 'user_id = rev_user' ] ], + 'text' => [ 'INNER JOIN', [ 'rev_text_id=old_id' ] ], + ], + ] + ]; + } + +} diff --git a/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php b/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php new file mode 100644 index 0000000000..5d516e8258 --- /dev/null +++ b/tests/phpunit/includes/Storage/PreMcrSchemaOverride.php @@ -0,0 +1,54 @@ + [], + 'drop' => [], + 'create' => [], + 'alter' => [], + ]; + + if ( $this->hasMcrTables( $db ) ) { + $overrides['drop'] = [ 'slots', 'content', 'slot_roles', 'content_models', ]; + $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/drop-mcr-tables', __DIR__ ); + } + + if ( !$this->hasPreMcrFields( $db ) ) { + $overrides['alter'][] = 'revision'; + $overrides['scripts'][] = $this->getSqlPatchPath( $db, '/create-pre-mcr-fields', __DIR__ ); + } + + return $overrides; + } + +} diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTest.php b/tests/phpunit/includes/Storage/RevisionStoreDbTest.php deleted file mode 100644 index 2a9295628e..0000000000 --- a/tests/phpunit/includes/Storage/RevisionStoreDbTest.php +++ /dev/null @@ -1,1282 +0,0 @@ -tablesUsed[] = 'archive'; - $this->tablesUsed[] = 'page'; - $this->tablesUsed[] = 'revision'; - $this->tablesUsed[] = 'comment'; - } - - /** - * @return LoadBalancer - */ - private function getLoadBalancerMock( array $server ) { - $lb = $this->getMockBuilder( LoadBalancer::class ) - ->setMethods( [ 'reallyOpenConnection' ] ) - ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] ) - ->getMock(); - - $lb->method( 'reallyOpenConnection' )->willReturnCallback( - function ( array $server, $dbNameOverride ) { - return $this->getDatabaseMock( $server ); - } - ); - - return $lb; - } - - /** - * @return Database - */ - private function getDatabaseMock( array $params ) { - $db = $this->getMockBuilder( DatabaseSqlite::class ) - ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] ) - ->setConstructorArgs( [ $params ] ) - ->getMock(); - - $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); - $db->method( 'isOpen' )->willReturn( true ); - - return $db; - } - - public function provideDomainCheck() { - yield [ false, 'test', '' ]; - yield [ 'test', 'test', '' ]; - - yield [ false, 'test', 'foo_' ]; - yield [ 'test-foo_', 'test', 'foo_' ]; - - yield [ false, 'dash-test', '' ]; - yield [ 'dash-test', 'dash-test', '' ]; - - yield [ false, 'underscore_test', 'foo_' ]; - yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ]; - } - - /** - * @dataProvider provideDomainCheck - * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId - */ - public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) { - $this->setMwGlobals( - [ - 'wgDBname' => $dbName, - 'wgDBprefix' => $dbPrefix, - ] - ); - - $loadBalancer = $this->getLoadBalancerMock( - [ - 'host' => '*dummy*', - 'dbDirectory' => '*dummy*', - 'user' => 'test', - 'password' => 'test', - 'flags' => 0, - 'variables' => [], - 'schema' => '', - 'cliMode' => true, - 'agent' => '', - 'load' => 100, - 'profiler' => null, - 'trxProfiler' => new TransactionProfiler(), - 'connLogger' => new \Psr\Log\NullLogger(), - 'queryLogger' => new \Psr\Log\NullLogger(), - 'errorLogger' => function () { - }, - 'deprecationLogger' => function () { - }, - 'type' => 'test', - 'dbname' => $dbName, - 'tablePrefix' => $dbPrefix, - ] - ); - $db = $loadBalancer->getConnection( DB_REPLICA ); - - $blobStore = $this->getMockBuilder( SqlBlobStore::class ) - ->disableOriginalConstructor() - ->getMock(); - - $store = new RevisionStore( - $loadBalancer, - $blobStore, - new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ), - MediaWikiServices::getInstance()->getCommentStore(), - MediaWikiServices::getInstance()->getActorMigration(), - $wikiId - ); - - $count = $store->countRevisionsByPageId( $db, 0 ); - - // Dummy check to make PhpUnit happy. We are really only interested in - // countRevisionsByPageId not failing due to the DB domain check. - $this->assertSame( 0, $count ); - } - - private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) { - $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() ); - $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() ); - $this->assertEquals( $l1->getFragment(), $l2->getFragment() ); - $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() ); - } - - private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) { - $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() ); - $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() ); - $this->assertEquals( $r1->getComment(), $r2->getComment() ); - $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() ); - $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() ); - $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() ); - $this->assertEquals( $r1->getSha1(), $r2->getSha1() ); - $this->assertEquals( $r1->getParentId(), $r2->getParentId() ); - $this->assertEquals( $r1->getSize(), $r2->getSize() ); - $this->assertEquals( $r1->getPageId(), $r2->getPageId() ); - $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() ); - $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() ); - $this->assertEquals( $r1->isMinor(), $r2->isMinor() ); - foreach ( $r1->getSlotRoles() as $role ) { - $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) ); - $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) ); - } - foreach ( [ - RevisionRecord::DELETED_TEXT, - RevisionRecord::DELETED_COMMENT, - RevisionRecord::DELETED_USER, - RevisionRecord::DELETED_RESTRICTED, - ] as $field ) { - $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) ); - } - } - - private function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) { - $this->assertSame( $s1->getRole(), $s2->getRole() ); - $this->assertSame( $s1->getModel(), $s2->getModel() ); - $this->assertSame( $s1->getFormat(), $s2->getFormat() ); - $this->assertSame( $s1->getSha1(), $s2->getSha1() ); - $this->assertSame( $s1->getSize(), $s2->getSize() ); - $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) ); - - $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null; - $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null; - } - - private function assertRevisionCompleteness( RevisionRecord $r ) { - foreach ( $r->getSlotRoles() as $role ) { - $this->assertSlotCompleteness( $r, $r->getSlot( $role ) ); - } - } - - private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) { - $this->assertTrue( $slot->hasAddress() ); - $this->assertSame( $r->getId(), $slot->getRevision() ); - } - - /** - * @param mixed[] $details - * - * @return RevisionRecord - */ - private function getRevisionRecordFromDetailsArray( $title, $details = [] ) { - // Convert some values that can't be provided by dataProviders - $page = WikiPage::factory( $title ); - if ( isset( $details['user'] ) && $details['user'] === true ) { - $details['user'] = $this->getTestUser()->getUser(); - } - if ( isset( $details['page'] ) && $details['page'] === true ) { - $details['page'] = $page->getId(); - } - if ( isset( $details['parent'] ) && $details['parent'] === true ) { - $details['parent'] = $page->getLatest(); - } - - // Create the RevisionRecord with any available data - $rev = new MutableRevisionRecord( $title ); - isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null; - isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null; - isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null; - isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null; - isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null; - isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null; - isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null; - isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null; - isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null; - isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null; - isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null; - - return $rev; - } - - private function getRandomCommentStoreComment() { - return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) ); - } - - public function provideInsertRevisionOn_successes() { - yield 'Bare minimum revision insertion' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - ]; - yield 'Detailed revision insertion' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'page' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - 'minor' => true, - 'visibility' => RevisionRecord::DELETED_RESTRICTED, - ], - ]; - } - - /** - * @dataProvider provideInsertRevisionOn_successes - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_successes( Title $title, array $revDetails = [] ) { - $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); - - $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $rev, $return ); - $this->assertRevisionCompleteness( $return ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_blobAddressExists() { - $title = Title::newFromText( 'UTPage' ); - $revDetails = [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'parent' => true, - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ]; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - // Insert the first revision - $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) ); - $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $revOne, $firstReturn ); - - // Insert a second revision inheriting the same blob address - $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) ); - $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) ); - $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() ); - $this->assertRevisionRecordsEqual( $revTwo, $secondReturn ); - - // Assert that the same blob address has been used. - $this->assertEquals( - $firstReturn->getSlot( 'main' )->getAddress(), - $secondReturn->getSlot( 'main' )->getAddress() - ); - // And that different revisions have been created. - $this->assertNotSame( - $firstReturn->getId(), - $secondReturn->getId() - ); - } - - public function provideInsertRevisionOn_failures() { - yield 'no slot' => [ - Title::newFromText( 'UTPage' ), - [ - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new InvalidArgumentException( 'At least one slot needs to be defined!' ) - ]; - yield 'slot that is not main slot' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new InvalidArgumentException( 'Only the main slot is supported for now!' ) - ]; - yield 'no timestamp' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'user' => true, - ], - new IncompleteRevisionException( 'timestamp field must not be NULL!' ) - ]; - yield 'no comment' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'timestamp' => '20171117010101', - 'user' => true, - ], - new IncompleteRevisionException( 'comment must not be NULL!' ) - ]; - yield 'no user' => [ - Title::newFromText( 'UTPage' ), - [ - 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), - 'comment' => $this->getRandomCommentStoreComment(), - 'timestamp' => '20171117010101', - ], - new IncompleteRevisionException( 'user must not be NULL!' ) - ]; - } - - /** - * @dataProvider provideInsertRevisionOn_failures - * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn - */ - public function testInsertRevisionOn_failures( - Title $title, - array $revDetails = [], - Exception $exception ) { - $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - $this->setExpectedException( - get_class( $exception ), - $exception->getMessage(), - $exception->getCode() - ); - $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); - } - - public function provideNewNullRevision() { - yield [ - Title::newFromText( 'UTPage' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ), - true, - ]; - yield [ - Title::newFromText( 'UTPage' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ), - false, - ]; - } - - /** - * @dataProvider provideNewNullRevision - * @covers \MediaWiki\Storage\RevisionStore::newNullRevision - */ - public function testNewNullRevision( Title $title, $comment, $minor ) { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser(); - - $parent = $store->getRevisionByTitle( $title ); - $record = $store->newNullRevision( - wfGetDB( DB_MASTER ), - $title, - $comment, - $minor, - $user - ); - - $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() ); - $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() ); - $this->assertEquals( $comment, $record->getComment() ); - $this->assertEquals( $minor, $record->isMinor() ); - $this->assertEquals( $user->getName(), $record->getUser()->getName() ); - $this->assertEquals( $parent->getId(), $record->getParentId() ); - - $parentSlot = $parent->getSlot( 'main' ); - $slot = $record->getSlot( 'main' ); - - $this->assertTrue( $slot->isInherited(), 'isInherited' ); - $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' ); - $this->assertSame( $parentSlot->getAddress(), $slot->getAddress(), 'getAddress' ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newNullRevision - */ - public function testNewNullRevision_nonExistingTitle() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newNullRevision( - wfGetDB( DB_MASTER ), - Title::newFromText( __METHOD__ . '.iDontExist!' ), - CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ), - false, - TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser() - ); - $this->assertNull( $record ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled - */ - public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revisionRecord = $store->getRevisionById( $rev->getId() ); - $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); - - $this->assertGreaterThan( 0, $result ); - $this->assertSame( - $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ), - $result - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled - */ - public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() { - // This assumes that sysops are auto patrolled - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $status = $page->doEditContent( - new WikitextContent( __METHOD__ ), - __METHOD__, - 0, - false, - $sysop - ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revisionRecord = $store->getRevisionById( $rev->getId() ); - $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); - - $this->assertSame( 0, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRecentChange - */ - public function testGetRecentChange() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionById( $rev->getId() ); - $recentChange = $store->getRecentChange( $revRecord ); - - $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); - $this->assertEquals( $rev->getRecentChange(), $recentChange ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionById - */ - public function testGetRevisionById() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionById( $rev->getId() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle - */ - public function testGetRevisionByTitle() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByTitle( $page->getTitle() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId - */ - public function testGetRevisionByPageId() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByPageId( $page->getId() ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp - */ - public function testGetRevisionByTimestamp() { - // Make sure there is 1 second between the last revision and the rev we create... - // Otherwise we might not get the correct revision and the test may fail... - // :( - sleep( 1 ); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $content = new WikitextContent( __METHOD__ ); - $status = $page->doEditContent( $content, __METHOD__ ); - /** @var Revision $rev */ - $rev = $status->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $revRecord = $store->getRevisionByTimestamp( - $page->getTitle(), - $rev->getTimestamp() - ); - - $this->assertSame( $rev->getId(), $revRecord->getId() ); - $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); - $this->assertSame( __METHOD__, $revRecord->getComment()->text ); - } - - private function revisionToRow( Revision $rev ) { - $page = WikiPage::factory( $rev->getTitle() ); - - return (object)[ - 'rev_id' => (string)$rev->getId(), - 'rev_page' => (string)$rev->getPage(), - 'rev_text_id' => (string)$rev->getTextId(), - 'rev_timestamp' => (string)$rev->getTimestamp(), - 'rev_user_text' => (string)$rev->getUserText(), - 'rev_user' => (string)$rev->getUser(), - 'rev_minor_edit' => $rev->isMinor() ? '1' : '0', - 'rev_deleted' => (string)$rev->getVisibility(), - 'rev_len' => (string)$rev->getSize(), - 'rev_parent_id' => (string)$rev->getParentId(), - 'rev_sha1' => (string)$rev->getSha1(), - 'rev_comment_text' => $rev->getComment(), - 'rev_comment_data' => null, - 'rev_comment_cid' => null, - 'rev_content_format' => $rev->getContentFormat(), - 'rev_content_model' => $rev->getContentModel(), - 'page_namespace' => (string)$page->getTitle()->getNamespace(), - 'page_title' => $page->getTitle()->getDBkey(), - 'page_id' => (string)$page->getId(), - 'page_latest' => (string)$page->getLatest(), - 'page_is_redirect' => $page->isRedirect() ? '1' : '0', - 'page_len' => (string)$page->getContent()->getSize(), - 'user_name' => (string)$rev->getUserText(), - ]; - } - - private function assertRevisionRecordMatchesRevision( - Revision $rev, - RevisionRecord $record - ) { - $this->assertSame( $rev->getId(), $record->getId() ); - $this->assertSame( $rev->getPage(), $record->getPageId() ); - $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() ); - $this->assertSame( $rev->getUserText(), $record->getUser()->getName() ); - $this->assertSame( $rev->getUser(), $record->getUser()->getId() ); - $this->assertSame( $rev->isMinor(), $record->isMinor() ); - $this->assertSame( $rev->getVisibility(), $record->getVisibility() ); - $this->assertSame( $rev->getSize(), $record->getSize() ); - /** - * @note As of MW 1.31, the database schema allows the parent ID to be - * NULL to indicate that it is unknown. - */ - $expectedParent = $rev->getParentId(); - if ( $expectedParent === null ) { - $expectedParent = 0; - } - $this->assertSame( $expectedParent, $record->getParentId() ); - $this->assertSame( $rev->getSha1(), $record->getSha1() ); - $this->assertSame( $rev->getComment(), $record->getComment()->text ); - $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() ); - $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() ); - $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_anonEdit() { - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); - $this->overrideMwServices(); - - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'a-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__ . 'a' - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_anonEdit_legacyEncoding() { - $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); - $this->overrideMwServices(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'a-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__. 'a' - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 - */ - public function testNewRevisionFromRow_userEdit() { - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_WRITE_BOTH ); - $this->overrideMwServices(); - - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $text = __METHOD__ . 'b-ä'; - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( $text ), - __METHOD__ . 'b', - 0, - false, - $this->getTestUser()->getUser() - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->newRevisionFromRow( - $this->revisionToRow( $rev ), - [], - $page->getTitle() - ); - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - $this->assertSame( $text, $rev->getContent()->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow - */ - public function testNewRevisionFromArchiveRow() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $title = Title::newFromText( __METHOD__ ); - $text = __METHOD__ . '-bä'; - $page = WikiPage::factory( $title ); - /** @var Revision $orig */ - $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) - ->value['revision']; - $page->doDeleteArticle( __METHOD__ ); - - $db = wfGetDB( DB_MASTER ); - $arQuery = $store->getArchiveQueryInfo(); - $res = $db->select( - $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], - __METHOD__, [], $arQuery['joins'] - ); - $this->assertTrue( is_object( $res ), 'query failed' ); - - $row = $res->fetchObject(); - $res->free(); - $record = $store->newRevisionFromArchiveRow( $row ); - - $this->assertRevisionRecordMatchesRevision( $orig, $record ); - $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow - */ - public function testNewRevisionFromArchiveRow_legacyEncoding() { - $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); - $this->overrideMwServices(); - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $title = Title::newFromText( __METHOD__ ); - $text = __METHOD__ . '-bä'; - $page = WikiPage::factory( $title ); - /** @var Revision $orig */ - $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) - ->value['revision']; - $page->doDeleteArticle( __METHOD__ ); - - $db = wfGetDB( DB_MASTER ); - $arQuery = $store->getArchiveQueryInfo(); - $res = $db->select( - $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], - __METHOD__, [], $arQuery['joins'] - ); - $this->assertTrue( is_object( $res ), 'query failed' ); - - $row = $res->fetchObject(); - $res->free(); - $record = $store->newRevisionFromArchiveRow( $row ); - - $this->assertRevisionRecordMatchesRevision( $orig, $record ); - $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId - */ - public function testLoadRevisionFromId() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId - */ - public function testLoadRevisionFromPageId() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle - */ - public function testLoadRevisionFromTitle() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title ); - $this->assertRevisionRecordMatchesRevision( $rev, $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp - */ - public function testLoadRevisionFromTimestamp() { - $title = Title::newFromText( __METHOD__ ); - $page = WikiPage::factory( $title ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - // Sleep to ensure different timestamps... )(evil) - sleep( 1 ); - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertNull( - $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' ) - ); - $this->assertSame( - $revOne->getId(), - $store->loadRevisionFromTimestamp( - wfGetDB( DB_MASTER ), - $title, - $revOne->getTimestamp() - )->getId() - ); - $this->assertSame( - $revTwo->getId(), - $store->loadRevisionFromTimestamp( - wfGetDB( DB_MASTER ), - $title, - $revTwo->getTimestamp() - )->getId() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes - */ - public function testGetParentLengths() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertSame( - [ - $revOne->getId() => strlen( __METHOD__ ), - ], - $store->listRevisionSizes( - wfGetDB( DB_MASTER ), - [ $revOne->getId() ] - ) - ); - $this->assertSame( - [ - $revOne->getId() => strlen( __METHOD__ ), - $revTwo->getId() => strlen( __METHOD__ ) + 1, - ], - $store->listRevisionSizes( - wfGetDB( DB_MASTER ), - [ $revOne->getId(), $revTwo->getId() ] - ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision - */ - public function testGetPreviousRevision() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertNull( - $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) ) - ); - $this->assertSame( - $revOne->getId(), - $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getNextRevision - */ - public function testGetNextRevision() { - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - /** @var Revision $revOne */ - $revOne = $page->doEditContent( - new WikitextContent( __METHOD__ ), __METHOD__ - )->value['revision']; - /** @var Revision $revTwo */ - $revTwo = $page->doEditContent( - new WikitextContent( __METHOD__ . '2' ), __METHOD__ - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $this->assertSame( - $revTwo->getId(), - $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId() - ); - $this->assertNull( - $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId - */ - public function testGetTimestampFromId_found() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() - ); - - $this->assertSame( $rev->getTimestamp(), $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId - */ - public function testGetTimestampFromId_notFound() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) - ->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->getTimestampFromId( - $page->getTitle(), - $rev->getId() + 1 - ); - - $this->assertFalse( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId - */ - public function testCountRevisionsByPageId() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - - $this->assertSame( - 0, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - $page->doEditContent( new WikitextContent( 'a' ), 'a' ); - $this->assertSame( - 1, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - $page->doEditContent( new WikitextContent( 'b' ), 'b' ); - $this->assertSame( - 2, - $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle - */ - public function testCountRevisionsByTitle() { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); - - $this->assertSame( - 0, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - $page->doEditContent( new WikitextContent( 'a' ), 'a' ); - $this->assertSame( - 1, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - $page->doEditContent( new WikitextContent( 'b' ), 'b' ); - $this->assertSame( - 2, - $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit - */ - public function testUserWasLastToEdit_false() { - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->userWasLastToEdit( - wfGetDB( DB_MASTER ), - $page->getId(), - $sysop->getId(), - '20160101010101' - ); - $this->assertFalse( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit - */ - public function testUserWasLastToEdit_true() { - $startTime = wfTimestampNow(); - $sysop = $this->getTestSysop()->getUser(); - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - $page->doEditContent( - new WikitextContent( __METHOD__ ), - __METHOD__, - 0, - false, - $sysop - ); - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $result = $store->userWasLastToEdit( - wfGetDB( DB_MASTER ), - $page->getId(), - $sysop->getId(), - $startTime - ); - $this->assertTrue( $result ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision - */ - public function testGetKnownCurrentRevision() { - $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); - /** @var Revision $rev */ - $rev = $page->doEditContent( - new WikitextContent( __METHOD__ . 'b' ), - __METHOD__ . 'b', - 0, - false, - $this->getTestUser()->getUser() - )->value['revision']; - - $store = MediaWikiServices::getInstance()->getRevisionStore(); - $record = $store->getKnownCurrentRevision( - $page->getTitle(), - $rev->getId() - ); - - $this->assertRevisionRecordMatchesRevision( $rev, $record ); - } - - public function provideNewMutableRevisionFromArray() { - yield 'Basic array, with page & id' => [ - [ - 'id' => 2, - 'page' => 1, - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - yield 'Basic array, content object' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content' => new WikitextContent( 'Some Content' ), - ] - ]; - yield 'Basic array, serialized text' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), - ] - ]; - yield 'Basic array, serialized text, utf-8 flags' => [ - [ - 'id' => 2, - 'page' => 1, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), - 'flags' => 'utf-8', - ] - ]; - yield 'Basic array, with title' => [ - [ - 'title' => Title::newFromText( 'SomeText' ), - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.2', - 'user' => 0, - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - yield 'Basic array, no user field' => [ - [ - 'id' => 2, - 'page' => 1, - 'text_id' => 2, - 'timestamp' => '20171017114835', - 'user_text' => '111.0.1.3', - 'minor_edit' => false, - 'deleted' => 0, - 'len' => 46, - 'parent_id' => 1, - 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', - 'comment' => 'Goat Comment!', - 'content_format' => 'text/x-wiki', - 'content_model' => 'wikitext', - ] - ]; - } - - /** - * @dataProvider provideNewMutableRevisionFromArray - * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray - */ - public function testNewMutableRevisionFromArray( array $array ) { - $store = MediaWikiServices::getInstance()->getRevisionStore(); - - $result = $store->newMutableRevisionFromArray( $array ); - - if ( isset( $array['id'] ) ) { - $this->assertSame( $array['id'], $result->getId() ); - } - if ( isset( $array['page'] ) ) { - $this->assertSame( $array['page'], $result->getPageId() ); - } - $this->assertSame( $array['timestamp'], $result->getTimestamp() ); - $this->assertSame( $array['user_text'], $result->getUser()->getName() ); - if ( isset( $array['user'] ) ) { - $this->assertSame( $array['user'], $result->getUser()->getId() ); - } - $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() ); - $this->assertSame( $array['deleted'], $result->getVisibility() ); - $this->assertSame( $array['len'], $result->getSize() ); - $this->assertSame( $array['parent_id'], $result->getParentId() ); - $this->assertSame( $array['sha1'], $result->getSha1() ); - $this->assertSame( $array['comment'], $result->getComment()->text ); - if ( isset( $array['content'] ) ) { - $this->assertTrue( - $result->getSlot( 'main' )->getContent()->equals( $array['content'] ) - ); - } elseif ( isset( $array['text'] ) ) { - $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() ); - } else { - $this->assertSame( - $array['content_format'], - $result->getSlot( 'main' )->getContent()->getDefaultFormat() - ); - $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() ); - } - } - - /** - * @dataProvider provideNewMutableRevisionFromArray - * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray - */ - public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) { - $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); - $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); - $blobStore = new SqlBlobStore( $lb, $cache ); - $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); - - $factory = $this->getMockBuilder( BlobStoreFactory::class ) - ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] ) - ->disableOriginalConstructor() - ->getMock(); - $factory->expects( $this->any() ) - ->method( 'newBlobStore' ) - ->willReturn( $blobStore ); - $factory->expects( $this->any() ) - ->method( 'newSqlBlobStore' ) - ->willReturn( $blobStore ); - - $this->setService( 'BlobStoreFactory', $factory ); - - $this->testNewMutableRevisionFromArray( $array ); - } - -} diff --git a/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php new file mode 100644 index 0000000000..bdff4cd7d4 --- /dev/null +++ b/tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php @@ -0,0 +1,1500 @@ +tablesUsed[] = 'archive'; + $this->tablesUsed[] = 'page'; + $this->tablesUsed[] = 'revision'; + $this->tablesUsed[] = 'comment'; + + $this->tablesUsed += $this->getMcrTablesToReset(); + + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); + + $this->setMwGlobals( + 'wgContentHandlerUseDB', + $this->getContentHandlerUseDB() + ); + + $this->overrideMwServices(); + } + + /** + * @return LoadBalancer|PHPUnit_Framework_MockObject_MockObject + */ + private function getLoadBalancerMock( array $server ) { + $lb = $this->getMockBuilder( LoadBalancer::class ) + ->setMethods( [ 'reallyOpenConnection' ] ) + ->setConstructorArgs( [ [ 'servers' => [ $server ] ] ] ) + ->getMock(); + + $lb->method( 'reallyOpenConnection' )->willReturnCallback( + function ( array $server, $dbNameOverride ) { + return $this->getDatabaseMock( $server ); + } + ); + + return $lb; + } + + /** + * @return Database|PHPUnit_Framework_MockObject_MockObject + */ + private function getDatabaseMock( array $params ) { + $db = $this->getMockBuilder( DatabaseSqlite::class ) + ->setMethods( [ 'select', 'doQuery', 'open', 'closeConnection', 'isOpen' ] ) + ->setConstructorArgs( [ $params ] ) + ->getMock(); + + $db->method( 'select' )->willReturn( new FakeResultWrapper( [] ) ); + $db->method( 'isOpen' )->willReturn( true ); + + return $db; + } + + public function provideDomainCheck() { + yield [ false, 'test', '' ]; + yield [ 'test', 'test', '' ]; + + yield [ false, 'test', 'foo_' ]; + yield [ 'test-foo_', 'test', 'foo_' ]; + + yield [ false, 'dash-test', '' ]; + yield [ 'dash-test', 'dash-test', '' ]; + + yield [ false, 'underscore_test', 'foo_' ]; + yield [ 'underscore_test-foo_', 'underscore_test', 'foo_' ]; + } + + /** + * @dataProvider provideDomainCheck + * @covers \MediaWiki\Storage\RevisionStore::checkDatabaseWikiId + */ + public function testDomainCheck( $wikiId, $dbName, $dbPrefix ) { + $this->setMwGlobals( + [ + 'wgDBname' => $dbName, + 'wgDBprefix' => $dbPrefix, + ] + ); + + $loadBalancer = $this->getLoadBalancerMock( + [ + 'host' => '*dummy*', + 'dbDirectory' => '*dummy*', + 'user' => 'test', + 'password' => 'test', + 'flags' => 0, + 'variables' => [], + 'schema' => '', + 'cliMode' => true, + 'agent' => '', + 'load' => 100, + 'profiler' => null, + 'trxProfiler' => new TransactionProfiler(), + 'connLogger' => new \Psr\Log\NullLogger(), + 'queryLogger' => new \Psr\Log\NullLogger(), + 'errorLogger' => function () { + }, + 'deprecationLogger' => function () { + }, + 'type' => 'test', + 'dbname' => $dbName, + 'tablePrefix' => $dbPrefix, + ] + ); + $db = $loadBalancer->getConnection( DB_REPLICA ); + + /** @var SqlBlobStore $blobStore */ + $blobStore = $this->getMockBuilder( SqlBlobStore::class ) + ->disableOriginalConstructor() + ->getMock(); + + $store = new RevisionStore( + $loadBalancer, + $blobStore, + new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ), + MediaWikiServices::getInstance()->getCommentStore(), + MediaWikiServices::getInstance()->getActorMigration(), + $wikiId + ); + + $count = $store->countRevisionsByPageId( $db, 0 ); + + // Dummy check to make PhpUnit happy. We are really only interested in + // countRevisionsByPageId not failing due to the DB domain check. + $this->assertSame( 0, $count ); + } + + private function assertLinkTargetsEqual( LinkTarget $l1, LinkTarget $l2 ) { + $this->assertEquals( $l1->getDBkey(), $l2->getDBkey() ); + $this->assertEquals( $l1->getNamespace(), $l2->getNamespace() ); + $this->assertEquals( $l1->getFragment(), $l2->getFragment() ); + $this->assertEquals( $l1->getInterwiki(), $l2->getInterwiki() ); + } + + private function assertRevisionRecordsEqual( RevisionRecord $r1, RevisionRecord $r2 ) { + $this->assertEquals( $r1->getUser()->getName(), $r2->getUser()->getName() ); + $this->assertEquals( $r1->getUser()->getId(), $r2->getUser()->getId() ); + $this->assertEquals( $r1->getComment(), $r2->getComment() ); + $this->assertEquals( $r1->getPageAsLinkTarget(), $r2->getPageAsLinkTarget() ); + $this->assertEquals( $r1->getTimestamp(), $r2->getTimestamp() ); + $this->assertEquals( $r1->getVisibility(), $r2->getVisibility() ); + $this->assertEquals( $r1->getSha1(), $r2->getSha1() ); + $this->assertEquals( $r1->getParentId(), $r2->getParentId() ); + $this->assertEquals( $r1->getSize(), $r2->getSize() ); + $this->assertEquals( $r1->getPageId(), $r2->getPageId() ); + $this->assertEquals( $r1->getSlotRoles(), $r2->getSlotRoles() ); + $this->assertEquals( $r1->getWikiId(), $r2->getWikiId() ); + $this->assertEquals( $r1->isMinor(), $r2->isMinor() ); + foreach ( $r1->getSlotRoles() as $role ) { + $this->assertSlotRecordsEqual( $r1->getSlot( $role ), $r2->getSlot( $role ) ); + $this->assertTrue( $r1->getContent( $role )->equals( $r2->getContent( $role ) ) ); + } + foreach ( [ + RevisionRecord::DELETED_TEXT, + RevisionRecord::DELETED_COMMENT, + RevisionRecord::DELETED_USER, + RevisionRecord::DELETED_RESTRICTED, + ] as $field ) { + $this->assertEquals( $r1->isDeleted( $field ), $r2->isDeleted( $field ) ); + } + } + + private function assertSlotRecordsEqual( SlotRecord $s1, SlotRecord $s2 ) { + $this->assertSame( $s1->getRole(), $s2->getRole() ); + $this->assertSame( $s1->getModel(), $s2->getModel() ); + $this->assertSame( $s1->getFormat(), $s2->getFormat() ); + $this->assertSame( $s1->getSha1(), $s2->getSha1() ); + $this->assertSame( $s1->getSize(), $s2->getSize() ); + $this->assertTrue( $s1->getContent()->equals( $s2->getContent() ) ); + + $s1->hasRevision() ? $this->assertSame( $s1->getRevision(), $s2->getRevision() ) : null; + $s1->hasAddress() ? $this->assertSame( $s1->hasAddress(), $s2->hasAddress() ) : null; + } + + private function assertRevisionCompleteness( RevisionRecord $r ) { + foreach ( $r->getSlotRoles() as $role ) { + $this->assertSlotCompleteness( $r, $r->getSlot( $role ) ); + } + } + + private function assertSlotCompleteness( RevisionRecord $r, SlotRecord $slot ) { + $this->assertTrue( $slot->hasAddress() ); + $this->assertSame( $r->getId(), $slot->getRevision() ); + } + + /** + * @param mixed[] $details + * + * @return RevisionRecord + */ + private function getRevisionRecordFromDetailsArray( $title, $details = [] ) { + // Convert some values that can't be provided by dataProviders + $page = WikiPage::factory( $title ); + if ( isset( $details['user'] ) && $details['user'] === true ) { + $details['user'] = $this->getTestUser()->getUser(); + } + if ( isset( $details['page'] ) && $details['page'] === true ) { + $details['page'] = $page->getId(); + } + if ( isset( $details['parent'] ) && $details['parent'] === true ) { + $details['parent'] = $page->getLatest(); + } + + // Create the RevisionRecord with any available data + $rev = new MutableRevisionRecord( $title ); + isset( $details['slot'] ) ? $rev->setSlot( $details['slot'] ) : null; + isset( $details['parent'] ) ? $rev->setParentId( $details['parent'] ) : null; + isset( $details['page'] ) ? $rev->setPageId( $details['page'] ) : null; + isset( $details['size'] ) ? $rev->setSize( $details['size'] ) : null; + isset( $details['sha1'] ) ? $rev->setSha1( $details['sha1'] ) : null; + isset( $details['comment'] ) ? $rev->setComment( $details['comment'] ) : null; + isset( $details['timestamp'] ) ? $rev->setTimestamp( $details['timestamp'] ) : null; + isset( $details['minor'] ) ? $rev->setMinorEdit( $details['minor'] ) : null; + isset( $details['user'] ) ? $rev->setUser( $details['user'] ) : null; + isset( $details['visibility'] ) ? $rev->setVisibility( $details['visibility'] ) : null; + isset( $details['id'] ) ? $rev->setId( $details['id'] ) : null; + + return $rev; + } + + public function provideInsertRevisionOn_successes() { + yield 'Bare minimum revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + ]; + yield 'Detailed revision insertion' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'page' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + 'minor' => true, + 'visibility' => RevisionRecord::DELETED_RESTRICTED, + ], + ]; + } + + private function getRandomCommentStoreComment() { + return CommentStoreComment::newUnsavedComment( __METHOD__ . '.' . rand( 0, 1000 ) ); + } + + /** + * @dataProvider provideInsertRevisionOn_successes + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_successes( + Title $title, + array $revDetails = [] + ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $return = $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + + $this->assertLinkTargetsEqual( $title, $return->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $rev, $return ); + $this->assertRevisionCompleteness( $return ); + $this->assertRevisionExistsInDatabase( $return ); + } + + protected function assertRevisionExistsInDatabase( RevisionRecord $rev ) { + $this->assertSelect( + 'revision', [ 'count(*)' ], [ 'rev_id' => $rev->getId() ], [ [ '1' ] ] + ); + } + + /** + * @param SlotRecord $a + * @param SlotRecord $b + */ + protected function assertSameSlotContent( SlotRecord $a, SlotRecord $b ) { + // Assert that the same blob address has been used. + $this->assertSame( $a->getAddress(), $b->getAddress() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_blobAddressExists() { + $title = Title::newFromText( 'UTPage' ); + $revDetails = [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'parent' => true, + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ]; + + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + // Insert the first revision + $revOne = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $firstReturn = $store->insertRevisionOn( $revOne, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $firstReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revOne, $firstReturn ); + + // Insert a second revision inheriting the same blob address + $revDetails['slot'] = SlotRecord::newInherited( $firstReturn->getSlot( 'main' ) ); + $revTwo = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + $secondReturn = $store->insertRevisionOn( $revTwo, wfGetDB( DB_MASTER ) ); + $this->assertLinkTargetsEqual( $title, $secondReturn->getPageAsLinkTarget() ); + $this->assertRevisionRecordsEqual( $revTwo, $secondReturn ); + + $firstMainSlot = $firstReturn->getSlot( 'main' ); + $secondMainSlot = $secondReturn->getSlot( 'main' ); + + $this->assertSameSlotContent( $firstMainSlot, $secondMainSlot ); + + // And that different revisions have been created. + $this->assertNotSame( $firstReturn->getId(), $secondReturn->getId() ); + + // Make sure the slot rows reference the correct revision + $this->assertSame( $firstReturn->getId(), $firstMainSlot->getRevision() ); + $this->assertSame( $secondReturn->getId(), $secondMainSlot->getRevision() ); + } + + public function provideInsertRevisionOn_failures() { + yield 'no slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'At least one slot needs to be defined!' ) + ]; + yield 'slot that is not main slot' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'lalala', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new InvalidArgumentException( 'Only the main slot is supported for now!' ) + ]; + yield 'no timestamp' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'user' => true, + ], + new IncompleteRevisionException( 'timestamp field must not be NULL!' ) + ]; + yield 'no comment' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'timestamp' => '20171117010101', + 'user' => true, + ], + new IncompleteRevisionException( 'comment must not be NULL!' ) + ]; + yield 'no user' => [ + Title::newFromText( 'UTPage' ), + [ + 'slot' => SlotRecord::newUnsaved( 'main', new WikitextContent( 'Chicken' ) ), + 'comment' => $this->getRandomCommentStoreComment(), + 'timestamp' => '20171117010101', + ], + new IncompleteRevisionException( 'user must not be NULL!' ) + ]; + } + + /** + * @dataProvider provideInsertRevisionOn_failures + * @covers \MediaWiki\Storage\RevisionStore::insertRevisionOn + */ + public function testInsertRevisionOn_failures( + Title $title, + array $revDetails = [], + Exception $exception + ) { + $rev = $this->getRevisionRecordFromDetailsArray( $title, $revDetails ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $this->setExpectedException( + get_class( $exception ), + $exception->getMessage(), + $exception->getCode() + ); + $store->insertRevisionOn( $rev, wfGetDB( DB_MASTER ) ); + } + + public function provideNewNullRevision() { + yield [ + Title::newFromText( 'UTPage_notAutoCreated' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment1' ), + true, + ]; + yield [ + Title::newFromText( 'UTPage_notAutoCreated' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment2', [ 'a' => 1 ] ), + false, + ]; + } + + /** + * @dataProvider provideNewNullRevision + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision( Title $title, $comment, $minor ) { + $this->overrideMwServices(); + + $page = WikiPage::factory( $title ); + $status = $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false + ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $user = TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser(); + + $parent = $store->getRevisionById( $rev->getId() ); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + $title, + $comment, + $minor, + $user + ); + + $this->assertEquals( $title->getNamespace(), $record->getPageAsLinkTarget()->getNamespace() ); + $this->assertEquals( $title->getDBkey(), $record->getPageAsLinkTarget()->getDBkey() ); + $this->assertEquals( $comment, $record->getComment() ); + $this->assertEquals( $minor, $record->isMinor() ); + $this->assertEquals( $user->getName(), $record->getUser()->getName() ); + $this->assertEquals( $parent->getId(), $record->getParentId() ); + + $parentSlot = $parent->getSlot( 'main' ); + $slot = $record->getSlot( 'main' ); + + $this->assertTrue( $slot->isInherited(), 'isInherited' ); + $this->assertSame( $parentSlot->getOrigin(), $slot->getOrigin(), 'getOrigin' ); + $this->assertSameSlotContent( $parentSlot, $slot ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newNullRevision + */ + public function testNewNullRevision_nonExistingTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newNullRevision( + wfGetDB( DB_MASTER ), + Title::newFromText( __METHOD__ . '.iDontExist!' ), + CommentStoreComment::newUnsavedComment( __METHOD__ . ' comment' ), + false, + TestUserRegistry::getMutableTestUser( __METHOD__ )->getUser() + ); + $this->assertNull( $record ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsRecentChangesId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertGreaterThan( 0, $result ); + $this->assertSame( + $page->getRevision()->getRecentChange()->getAttribute( 'rc_id' ), + $result + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRcIdIfUnpatrolled + */ + public function testGetRcIdIfUnpatrolled_returnsZeroIfPatrolled() { + // This assumes that sysops are auto patrolled + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $status = $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revisionRecord = $store->getRevisionById( $rev->getId() ); + $result = $store->getRcIdIfUnpatrolled( $revisionRecord ); + + $this->assertSame( 0, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRecentChange + */ + public function testGetRecentChange() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + $recentChange = $store->getRecentChange( $revRecord ); + + $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) ); + $this->assertEquals( $rev->getRecentChange(), $recentChange ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionById + */ + public function testGetRevisionById() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionById( $rev->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTitle + */ + public function testGetRevisionByTitle() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTitle( $page->getTitle() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByPageId + */ + public function testGetRevisionByPageId() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByPageId( $page->getId() ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getRevisionByTimestamp + */ + public function testGetRevisionByTimestamp() { + // Make sure there is 1 second between the last revision and the rev we create... + // Otherwise we might not get the correct revision and the test may fail... + // :( + sleep( 1 ); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $content = new WikitextContent( __METHOD__ ); + $status = $page->doEditContent( $content, __METHOD__ ); + /** @var Revision $rev */ + $rev = $status->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $revRecord = $store->getRevisionByTimestamp( + $page->getTitle(), + $rev->getTimestamp() + ); + + $this->assertSame( $rev->getId(), $revRecord->getId() ); + $this->assertTrue( $revRecord->getSlot( 'main' )->getContent()->equals( $content ) ); + $this->assertSame( __METHOD__, $revRecord->getComment()->text ); + } + + protected function revisionToRow( Revision $rev ) { + $page = WikiPage::factory( $rev->getTitle() ); + + return (object)[ + 'rev_id' => (string)$rev->getId(), + 'rev_page' => (string)$rev->getPage(), + 'rev_text_id' => (string)$rev->getTextId(), + 'rev_timestamp' => $this->db->timestamp( $rev->getTimestamp() ), + 'rev_user_text' => (string)$rev->getUserText(), + 'rev_user' => (string)$rev->getUser(), + 'rev_minor_edit' => $rev->isMinor() ? '1' : '0', + 'rev_deleted' => (string)$rev->getVisibility(), + 'rev_len' => (string)$rev->getSize(), + 'rev_parent_id' => (string)$rev->getParentId(), + 'rev_sha1' => (string)$rev->getSha1(), + 'rev_comment_text' => $rev->getComment(), + 'rev_comment_data' => null, + 'rev_comment_cid' => null, + 'rev_content_format' => $rev->getContentFormat(), + 'rev_content_model' => $rev->getContentModel(), + 'page_namespace' => (string)$page->getTitle()->getNamespace(), + 'page_title' => $page->getTitle()->getDBkey(), + 'page_id' => (string)$page->getId(), + 'page_latest' => (string)$page->getLatest(), + 'page_is_redirect' => $page->isRedirect() ? '1' : '0', + 'page_len' => (string)$page->getContent()->getSize(), + 'user_name' => (string)$rev->getUserText(), + ]; + } + + private function assertRevisionRecordMatchesRevision( + Revision $rev, + RevisionRecord $record + ) { + $this->assertSame( $rev->getId(), $record->getId() ); + $this->assertSame( $rev->getPage(), $record->getPageId() ); + $this->assertSame( $rev->getTimestamp(), $record->getTimestamp() ); + $this->assertSame( $rev->getUserText(), $record->getUser()->getName() ); + $this->assertSame( $rev->getUser(), $record->getUser()->getId() ); + $this->assertSame( $rev->isMinor(), $record->isMinor() ); + $this->assertSame( $rev->getVisibility(), $record->getVisibility() ); + $this->assertSame( $rev->getSize(), $record->getSize() ); + /** + * @note As of MW 1.31, the database schema allows the parent ID to be + * NULL to indicate that it is unknown. + */ + $expectedParent = $rev->getParentId(); + if ( $expectedParent === null ) { + $expectedParent = 0; + } + $this->assertSame( $expectedParent, $record->getParentId() ); + $this->assertSame( $rev->getSha1(), $record->getSha1() ); + $this->assertSame( $rev->getComment(), $record->getComment()->text ); + $this->assertSame( $rev->getContentFormat(), $record->getContent( 'main' )->getDefaultFormat() ); + $this->assertSame( $rev->getContentModel(), $record->getContent( 'main' )->getModel() ); + $this->assertLinkTargetsEqual( $rev->getTitle(), $record->getPageAsLinkTarget() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_anonEdit_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'a-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__. 'a' + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromRow_1_29 + */ + public function testNewRevisionFromRow_userEdit() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $text = __METHOD__ . 'b-ä'; + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( $text ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->newRevisionFromRow( + $this->revisionToRow( $rev ), + [], + $page->getTitle() + ); + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + $this->assertSame( $text, $rev->getContent()->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::newRevisionFromArchiveRow + */ + public function testNewRevisionFromArchiveRow_legacyEncoding() { + $this->setMwGlobals( 'wgLegacyEncoding', 'windows-1252' ); + $this->overrideMwServices(); + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $title = Title::newFromText( __METHOD__ ); + $text = __METHOD__ . '-bä'; + $page = WikiPage::factory( $title ); + /** @var Revision $orig */ + $orig = $page->doEditContent( new WikitextContent( $text ), __METHOD__ ) + ->value['revision']; + $page->doDeleteArticle( __METHOD__ ); + + $db = wfGetDB( DB_MASTER ); + $arQuery = $store->getArchiveQueryInfo(); + $res = $db->select( + $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ], + __METHOD__, [], $arQuery['joins'] + ); + $this->assertTrue( is_object( $res ), 'query failed' ); + + $row = $res->fetchObject(); + $res->free(); + $record = $store->newRevisionFromArchiveRow( $row ); + + $this->assertRevisionRecordMatchesRevision( $orig, $record ); + $this->assertSame( $text, $record->getContent( 'main' )->serialize() ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromId + */ + public function testLoadRevisionFromId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromId( wfGetDB( DB_MASTER ), $rev->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromPageId + */ + public function testLoadRevisionFromPageId() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromPageId( wfGetDB( DB_MASTER ), $page->getId() ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTitle + */ + public function testLoadRevisionFromTitle() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->loadRevisionFromTitle( wfGetDB( DB_MASTER ), $title ); + $this->assertRevisionRecordMatchesRevision( $rev, $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::loadRevisionFromTimestamp + */ + public function testLoadRevisionFromTimestamp() { + $title = Title::newFromText( __METHOD__ ); + $page = WikiPage::factory( $title ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + // Sleep to ensure different timestamps... )(evil) + sleep( 1 ); + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( new WikitextContent( __METHOD__ . 'a' ), '' ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->loadRevisionFromTimestamp( wfGetDB( DB_MASTER ), $title, '20150101010101' ) + ); + $this->assertSame( + $revOne->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revOne->getTimestamp() + )->getId() + ); + $this->assertSame( + $revTwo->getId(), + $store->loadRevisionFromTimestamp( + wfGetDB( DB_MASTER ), + $title, + $revTwo->getTimestamp() + )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::listRevisionSizes + */ + public function testGetParentLengths() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId() ] + ) + ); + $this->assertSame( + [ + $revOne->getId() => strlen( __METHOD__ ), + $revTwo->getId() => strlen( __METHOD__ ) + 1, + ], + $store->listRevisionSizes( + wfGetDB( DB_MASTER ), + [ $revOne->getId(), $revTwo->getId() ] + ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getPreviousRevision + */ + public function testGetPreviousRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertNull( + $store->getPreviousRevision( $store->getRevisionById( $revOne->getId() ) ) + ); + $this->assertSame( + $revOne->getId(), + $store->getPreviousRevision( $store->getRevisionById( $revTwo->getId() ) )->getId() + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getNextRevision + */ + public function testGetNextRevision() { + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + /** @var Revision $revOne */ + $revOne = $page->doEditContent( + new WikitextContent( __METHOD__ ), __METHOD__ + )->value['revision']; + /** @var Revision $revTwo */ + $revTwo = $page->doEditContent( + new WikitextContent( __METHOD__ . '2' ), __METHOD__ + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $this->assertSame( + $revTwo->getId(), + $store->getNextRevision( $store->getRevisionById( $revOne->getId() ) )->getId() + ); + $this->assertNull( + $store->getNextRevision( $store->getRevisionById( $revTwo->getId() ) ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_found() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + ); + + $this->assertSame( $rev->getTimestamp(), $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getTimestampFromId + */ + public function testGetTimestampFromId_notFound() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ) + ->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->getTimestampFromId( + $page->getTitle(), + $rev->getId() + 1 + ); + + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByPageId + */ + public function testCountRevisionsByPageId() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByPageId( wfGetDB( DB_MASTER ), $page->getId() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::countRevisionsByTitle + */ + public function testCountRevisionsByTitle() { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $page = WikiPage::factory( Title::newFromText( __METHOD__ ) ); + + $this->assertSame( + 0, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'a' ), 'a' ); + $this->assertSame( + 1, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + $page->doEditContent( new WikitextContent( 'b' ), 'b' ); + $this->assertSame( + 2, + $store->countRevisionsByTitle( wfGetDB( DB_MASTER ), $page->getTitle() ) + ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_false() { + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + '20160101010101' + ); + $this->assertFalse( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::userWasLastToEdit + */ + public function testUserWasLastToEdit_true() { + $startTime = wfTimestampNow(); + $sysop = $this->getTestSysop()->getUser(); + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + $page->doEditContent( + new WikitextContent( __METHOD__ ), + __METHOD__, + 0, + false, + $sysop + ); + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $result = $store->userWasLastToEdit( + wfGetDB( DB_MASTER ), + $page->getId(), + $sysop->getId(), + $startTime + ); + $this->assertTrue( $result ); + } + + /** + * @covers \MediaWiki\Storage\RevisionStore::getKnownCurrentRevision + */ + public function testGetKnownCurrentRevision() { + $page = WikiPage::factory( Title::newFromText( 'UTPage' ) ); + /** @var Revision $rev */ + $rev = $page->doEditContent( + new WikitextContent( __METHOD__ . 'b' ), + __METHOD__ . 'b', + 0, + false, + $this->getTestUser()->getUser() + )->value['revision']; + + $store = MediaWikiServices::getInstance()->getRevisionStore(); + $record = $store->getKnownCurrentRevision( + $page->getTitle(), + $rev->getId() + ); + + $this->assertRevisionRecordMatchesRevision( $rev, $record ); + } + + public function provideNewMutableRevisionFromArray() { + yield 'Basic array, with page & id' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, content object' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content' => new WikitextContent( 'Some Content' ), + ] + ]; + yield 'Basic array, serialized text' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + ] + ]; + yield 'Basic array, serialized text, utf-8 flags' => [ + [ + 'id' => 2, + 'page' => 1, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'text' => ( new WikitextContent( 'Söme Content' ) )->serialize(), + 'flags' => 'utf-8', + ] + ]; + yield 'Basic array, with title' => [ + [ + 'title' => Title::newFromText( 'SomeText' ), + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.2', + 'user' => 0, + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + yield 'Basic array, no user field' => [ + [ + 'id' => 2, + 'page' => 1, + 'text_id' => 2, + 'timestamp' => '20171017114835', + 'user_text' => '111.0.1.3', + 'minor_edit' => false, + 'deleted' => 0, + 'len' => 46, + 'parent_id' => 1, + 'sha1' => 'rdqbbzs3pkhihgbs8qf2q9jsvheag5z', + 'comment' => 'Goat Comment!', + 'content_format' => 'text/x-wiki', + 'content_model' => 'wikitext', + ] + ]; + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray( array $array ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $result = $store->newMutableRevisionFromArray( $array ); + + if ( isset( $array['id'] ) ) { + $this->assertSame( $array['id'], $result->getId() ); + } + if ( isset( $array['page'] ) ) { + $this->assertSame( $array['page'], $result->getPageId() ); + } + $this->assertSame( $array['timestamp'], $result->getTimestamp() ); + $this->assertSame( $array['user_text'], $result->getUser()->getName() ); + if ( isset( $array['user'] ) ) { + $this->assertSame( $array['user'], $result->getUser()->getId() ); + } + $this->assertSame( (bool)$array['minor_edit'], $result->isMinor() ); + $this->assertSame( $array['deleted'], $result->getVisibility() ); + $this->assertSame( $array['len'], $result->getSize() ); + $this->assertSame( $array['parent_id'], $result->getParentId() ); + $this->assertSame( $array['sha1'], $result->getSha1() ); + $this->assertSame( $array['comment'], $result->getComment()->text ); + if ( isset( $array['content'] ) ) { + $this->assertTrue( + $result->getSlot( 'main' )->getContent()->equals( $array['content'] ) + ); + } elseif ( isset( $array['text'] ) ) { + $this->assertSame( $array['text'], $result->getSlot( 'main' )->getContent()->serialize() ); + } else { + $this->assertSame( + $array['content_format'], + $result->getSlot( 'main' )->getContent()->getDefaultFormat() + ); + $this->assertSame( $array['content_model'], $result->getSlot( 'main' )->getModel() ); + } + } + + /** + * @dataProvider provideNewMutableRevisionFromArray + * @covers \MediaWiki\Storage\RevisionStore::newMutableRevisionFromArray + */ + public function testNewMutableRevisionFromArray_legacyEncoding( array $array ) { + $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] ); + $lb = MediaWikiServices::getInstance()->getDBLoadBalancer(); + $blobStore = new SqlBlobStore( $lb, $cache ); + $blobStore->setLegacyEncoding( 'windows-1252', Language::factory( 'en' ) ); + + $factory = $this->getMockBuilder( BlobStoreFactory::class ) + ->setMethods( [ 'newBlobStore', 'newSqlBlobStore' ] ) + ->disableOriginalConstructor() + ->getMock(); + $factory->expects( $this->any() ) + ->method( 'newBlobStore' ) + ->willReturn( $blobStore ); + $factory->expects( $this->any() ) + ->method( 'newSqlBlobStore' ) + ->willReturn( $blobStore ); + + $this->setService( 'BlobStoreFactory', $factory ); + + $this->testNewMutableRevisionFromArray( $array ); + } + + protected function getDefaultQueryFields( $returnTextIdField = true ) { + $fields = [ + 'rev_id', + 'rev_page', + 'rev_timestamp', + 'rev_minor_edit', + 'rev_deleted', + 'rev_len', + 'rev_parent_id', + 'rev_sha1', + ]; + if ( $returnTextIdField ) { + $fields[] = 'rev_text_id'; + } + return $fields; + } + + protected function getCommentQueryFields() { + return [ + 'rev_comment_text' => 'rev_comment', + 'rev_comment_data' => 'NULL', + 'rev_comment_cid' => 'NULL', + ]; + } + + protected function getActorQueryFields() { + return [ + 'rev_user' => 'rev_user', + 'rev_user_text' => 'rev_user_text', + 'rev_actor' => 'NULL', + ]; + } + + protected function getContentHandlerQueryFields() { + return [ + 'rev_content_format', + 'rev_content_model', + ]; + } + + abstract public function provideGetQueryInfo(); + + /** + * @dataProvider provideGetQueryInfo + * @covers \MediaWiki\Storage\RevisionStore::getQueryInfo + */ + public function testGetQueryInfo( $options, $expected ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $queryInfo = $store->getQueryInfo( $options ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['tables'], + $queryInfo['tables'] + ); + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['fields'], + $queryInfo['fields'] + ); + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['joins'], + $queryInfo['joins'] + ); + } + + protected function getDefaultArchiveFields( $returnTextFields = true ) { + $fields = [ + 'ar_id', + 'ar_page_id', + 'ar_namespace', + 'ar_title', + 'ar_rev_id', + 'ar_timestamp', + 'ar_minor_edit', + 'ar_deleted', + 'ar_len', + 'ar_parent_id', + 'ar_sha1', + ]; + if ( $returnTextFields ) { + $fields[] = 'ar_text_id'; + } + return $fields; + } + + abstract public function provideGetArchiveQueryInfo(); + + /** + * @dataProvider provideGetArchiveQueryInfo + * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo + */ + public function testGetArchiveQueryInfo( $expected ) { + $store = MediaWikiServices::getInstance()->getRevisionStore(); + + $archiveQueryInfo = $store->getArchiveQueryInfo(); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['tables'], + $archiveQueryInfo['tables'] + ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['fields'], + $archiveQueryInfo['fields'] + ); + + $this->assertArrayEqualsIgnoringIntKeyOrder( + $expected['joins'], + $archiveQueryInfo['joins'] + ); + } + + /** + * Assert that the two arrays passed are equal, ignoring the order of the values that integer + * keys. + * + * Note: Failures of this assertion can be slightly confusing as the arrays are actually + * split into a string key array and an int key array before assertions occur. + * + * @param array $expected + * @param array $actual + */ + private function assertArrayEqualsIgnoringIntKeyOrder( array $expected, array $actual ) { + $this->objectAssociativeSort( $expected ); + $this->objectAssociativeSort( $actual ); + + // Separate the int key values from the string key values so that assertion failures are + // easier to understand. + $expectedIntKeyValues = []; + $actualIntKeyValues = []; + + // Remove all int keys and re add them at the end after sorting by value + // This will result in all int keys being in the same order with same ints at the end of + // the array + foreach ( $expected as $key => $value ) { + if ( is_int( $key ) ) { + unset( $expected[$key] ); + $expectedIntKeyValues[] = $value; + } + } + foreach ( $actual as $key => $value ) { + if ( is_int( $key ) ) { + unset( $actual[$key] ); + $actualIntKeyValues[] = $value; + } + } + + $this->assertArrayEquals( $expected, $actual, false, true ); + $this->assertArrayEquals( $expectedIntKeyValues, $actualIntKeyValues, false, true ); + } + +} diff --git a/tests/phpunit/includes/Storage/RevisionStoreTest.php b/tests/phpunit/includes/Storage/RevisionStoreTest.php index fed9a0c3d5..3749f294bf 100644 --- a/tests/phpunit/includes/Storage/RevisionStoreTest.php +++ b/tests/phpunit/includes/Storage/RevisionStoreTest.php @@ -247,87 +247,6 @@ class RevisionStoreTest extends MediaWikiTestCase { $this->assertEquals( $expected, $store->getQueryInfo( $options ) ); } - private function getDefaultArchiveFields() { - return [ - 'ar_id', - 'ar_page_id', - 'ar_namespace', - 'ar_title', - 'ar_rev_id', - 'ar_text_id', - 'ar_timestamp', - 'ar_minor_edit', - 'ar_deleted', - 'ar_len', - 'ar_parent_id', - 'ar_sha1', - ]; - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo - */ - public function testGetArchiveQueryInfo_contentHandlerDb() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); - $this->overrideMwServices(); - $store = $this->getRevisionStore(); - $store->setContentHandlerUseDB( true ); - $this->assertEquals( - [ - 'tables' => [ - 'archive' - ], - 'fields' => array_merge( - $this->getDefaultArchiveFields(), - [ - 'ar_comment_text' => 'ar_comment', - 'ar_comment_data' => 'NULL', - 'ar_comment_cid' => 'NULL', - 'ar_user_text' => 'ar_user_text', - 'ar_user' => 'ar_user', - 'ar_actor' => 'NULL', - 'ar_content_format', - 'ar_content_model', - ] - ), - 'joins' => [], - ], - $store->getArchiveQueryInfo() - ); - } - - /** - * @covers \MediaWiki\Storage\RevisionStore::getArchiveQueryInfo - */ - public function testGetArchiveQueryInfo_noContentHandlerDb() { - $this->setMwGlobals( 'wgCommentTableSchemaMigrationStage', MIGRATION_OLD ); - $this->setMwGlobals( 'wgActorTableSchemaMigrationStage', MIGRATION_OLD ); - $this->overrideMwServices(); - $store = $this->getRevisionStore(); - $store->setContentHandlerUseDB( false ); - $this->assertEquals( - [ - 'tables' => [ - 'archive' - ], - 'fields' => array_merge( - $this->getDefaultArchiveFields(), - [ - 'ar_comment_text' => 'ar_comment', - 'ar_comment_data' => 'NULL', - 'ar_comment_cid' => 'NULL', - 'ar_user_text' => 'ar_user_text', - 'ar_user' => 'ar_user', - 'ar_actor' => 'NULL', - ] - ), - 'joins' => [], - ], - $store->getArchiveQueryInfo() - ); - } - public function testGetTitle_successFromPageId() { $mockLoadBalancer = $this->getMockLoadBalancer(); // Title calls wfGetDB() so we have to set the main service diff --git a/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql b/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql new file mode 100644 index 0000000000..09deb4f2cd --- /dev/null +++ b/tests/phpunit/includes/Storage/create-pre-mcr-fields.sql @@ -0,0 +1,3 @@ +ALTER TABLE /*_*/revision ADD rev_text_id INTEGER DEFAULT 0; +ALTER TABLE /*_*/revision ADD rev_content_model VARBINARY(32) DEFAULT NULL; +ALTER TABLE /*_*/revision ADD rev_content_format VARBINARY(64) DEFAULT NULL; diff --git a/tests/phpunit/includes/Storage/drop-mcr-tables.sql b/tests/phpunit/includes/Storage/drop-mcr-tables.sql new file mode 100644 index 0000000000..bc89edc95e --- /dev/null +++ b/tests/phpunit/includes/Storage/drop-mcr-tables.sql @@ -0,0 +1,4 @@ +DROP TABLE /*_*/slots; +DROP TABLE /*_*/content; +DROP TABLE /*_*/content_models; +DROP TABLE /*_*/slot_roles; diff --git a/tests/phpunit/includes/changetags/ChangeTagsTest.php b/tests/phpunit/includes/changetags/ChangeTagsTest.php index 63e0ec2268..215cdfdf90 100644 --- a/tests/phpunit/includes/changetags/ChangeTagsTest.php +++ b/tests/phpunit/includes/changetags/ChangeTagsTest.php @@ -2,9 +2,18 @@ /** * @covers ChangeTags + * @group Database */ class ChangeTagsTest extends MediaWikiTestCase { + public function setUp() { + parent::setUp(); + + $this->tablesUsed[] = 'change_tag'; + $this->tablesUsed[] = 'change_tag_def'; + $this->tablesUsed[] = 'tag_summary'; + } + // TODO only modifyDisplayQuery and getSoftwareTags are tested, nothing else is /** @dataProvider provideModifyDisplayQuery */ @@ -306,4 +315,221 @@ class ChangeTagsTest extends MediaWikiTestCase { sort( $actual ); $this->assertEquals( $expected, $actual ); } + + public function testUpdateTagsMigrationOld() { + $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'change_tag', '*' ); + $dbw->delete( 'change_tag_def', '*' ); + + $rcId = 123; + ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( [], iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => null, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag2', + 'ct_tag_id' => null, + 'ct_rc_id' => 123 + ], + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + + $rcId = 124; + ChangeTags::updateTags( [ 'tag1', 'tag3' ], [], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( [], iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => null, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag2', + 'ct_tag_id' => null, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => null, + 'ct_rc_id' => 124 + ], + (object)[ + 'ct_tag' => 'tag3', + 'ct_tag_id' => null, + 'ct_rc_id' => 124 + ], + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + } + + public function testUpdateTagsMigrationWriteBoth() { + $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'change_tag', '*' ); + $dbw->delete( 'change_tag_def', '*' ); + + $rcId = 123; + ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $expected = [ + (object)[ + 'ctd_name' => 'tag1', + 'ctd_id' => 1, + 'ctd_count' => 1 + ], + (object)[ + 'ctd_name' => 'tag2', + 'ctd_id' => 2, + 'ctd_count' => 1 + ], + ]; + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( $expected, iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => 1, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag2', + 'ct_tag_id' => 2, + 'ct_rc_id' => 123 + ], + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + + $rcId = 124; + ChangeTags::updateTags( [ 'tag1', 'tag3' ], [], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $expected = [ + (object)[ + 'ctd_name' => 'tag1', + 'ctd_id' => 1, + 'ctd_count' => 2 + ], + (object)[ + 'ctd_name' => 'tag2', + 'ctd_id' => 2, + 'ctd_count' => 1 + ], + (object)[ + 'ctd_name' => 'tag3', + 'ctd_id' => 3, + 'ctd_count' => 1 + ], + ]; + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( $expected, iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => 1, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag2', + 'ct_tag_id' => 2, + 'ct_rc_id' => 123 + ], + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => 1, + 'ct_rc_id' => 124 + ], + (object)[ + 'ct_tag' => 'tag3', + 'ct_tag_id' => 3, + 'ct_rc_id' => 124 + ], + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + } + + public function testDeleteTagsMigrationOld() { + $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_OLD ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'change_tag', '*' ); + $dbw->delete( 'change_tag_def', '*' ); + + $rcId = 123; + ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId ); + + ChangeTags::updateTags( [], [ 'tag2' ], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( [], iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => null, + 'ct_rc_id' => 123 + ] + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + } + + public function testDeleteTagsMigrationWriteBoth() { + $this->setMwGlobals( 'wgChangeTagsSchemaMigrationStage', MIGRATION_WRITE_BOTH ); + $dbw = wfGetDB( DB_MASTER ); + $dbw->delete( 'change_tag', '*' ); + $dbw->delete( 'change_tag_def', '*' ); + + $rcId = 123; + ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId ); + + ChangeTags::updateTags( [], [ 'tag2' ], $rcId ); + + $dbr = wfGetDB( DB_REPLICA ); + + $expected = [ + (object)[ + 'ctd_name' => 'tag1', + 'ctd_id' => 1, + 'ctd_count' => 1 + ], + ]; + $res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' ); + $this->assertEquals( $expected, iterator_to_array( $res, false ) ); + + $expected2 = [ + (object)[ + 'ct_tag' => 'tag1', + 'ct_tag_id' => 1, + 'ct_rc_id' => 123 + ] + ]; + $res2 = $dbr->select( 'change_tag', [ 'ct_tag', 'ct_tag_id', 'ct_rc_id' ], '' ); + $this->assertEquals( $expected2, iterator_to_array( $res2, false ) ); + } + } diff --git a/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php b/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php deleted file mode 100644 index 2d7d6cc3aa..0000000000 --- a/tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php +++ /dev/null @@ -1,42 +0,0 @@ -createPage( - __METHOD__, - "some text", - CONTENT_MODEL_JAVASCRIPT - ); - - $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); - } - - /** - * @covers WikiPage::getContentHandler - */ - public function testGetContentHandler() { - $page = $this->createPage( - __METHOD__, - "some text", - CONTENT_MODEL_JAVASCRIPT - ); - - $page = new WikiPage( $page->getTitle() ); - $this->assertEquals( JavaScriptContentHandler::class, get_class( $page->getContentHandler() ) ); - } - -} diff --git a/tests/phpunit/includes/page/WikiPageDbTestBase.php b/tests/phpunit/includes/page/WikiPageDbTestBase.php index 68539b5ec7..cc20b6bd30 100644 --- a/tests/phpunit/includes/page/WikiPageDbTestBase.php +++ b/tests/phpunit/includes/page/WikiPageDbTestBase.php @@ -30,10 +30,29 @@ abstract class WikiPageDbTestBase extends MediaWikiLangTestCase { 'iwlinks' ] ); } + /** + * @return int + */ + abstract protected function getMcrMigrationStage(); + + /** + * @return string[] + */ + abstract protected function getMcrTablesToReset(); + protected function setUp() { parent::setUp(); + + $this->tablesUsed += $this->getMcrTablesToReset(); + $this->setMwGlobals( 'wgContentHandlerUseDB', $this->getContentHandlerUseDB() ); + $this->setMwGlobals( + 'wgMultiContentRevisionSchemaMigrationStage', + $this->getMcrMigrationStage() + ); $this->pagesToDelete = []; + + $this->overrideMwServices(); } protected function tearDown() { diff --git a/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php b/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php deleted file mode 100644 index a6ce185a12..0000000000 --- a/tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php +++ /dev/null @@ -1,14 +0,0 @@ -createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() ); + } + + /** + * @covers WikiPage::getContentHandler + */ + public function testGetContentHandler() { + $page = $this->createPage( + __METHOD__, + "some text", + CONTENT_MODEL_JAVASCRIPT + ); + + $page = new WikiPage( $page->getTitle() ); + $this->assertEquals( JavaScriptContentHandler::class, get_class( $page->getContentHandler() ) ); + } + +} diff --git a/tests/phpunit/includes/upload/UploadBaseTest.php b/tests/phpunit/includes/upload/UploadBaseTest.php index 3541091a68..a80262e932 100644 --- a/tests/phpunit/includes/upload/UploadBaseTest.php +++ b/tests/phpunit/includes/upload/UploadBaseTest.php @@ -562,7 +562,7 @@ class UploadBaseTest extends MediaWikiTestCase { public function testCheckXMLEncodingMissmatch( $fileContents, $evil ) { $filename = $this->getNewTempFile(); file_put_contents( $filename, $fileContents ); - $this->assertSame( UploadBase::checkXMLEncodingMissmatch( $filename ), $evil ); + $this->assertSame( $evil, UploadBase::checkXMLEncodingMissmatch( $filename ) ); } public function provideCheckXMLEncodingMissmatch() {