Merge "installer: Fix display of UPGRADE by disabling InterwikiLookup"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 7 Jun 2018 16:49:40 +0000 (16:49 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 7 Jun 2018 16:49:40 +0000 (16:49 +0000)
56 files changed:
RELEASE-NOTES-1.31
RELEASE-NOTES-1.32
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/actions/Action.php
includes/api/ApiBase.php
includes/auth/AuthManager.php
includes/auth/LegacyHookPreAuthenticationProvider.php
includes/cache/CacheDependency.php
includes/cache/CacheHelper.php
includes/changes/CategoryMembershipChange.php
includes/changes/ChangesListBooleanFilter.php
includes/changes/ChangesListStringOptionsFilterGroup.php
includes/changetags/ChangeTags.php
includes/context/ContextSource.php
includes/context/DerivativeContext.php
includes/context/RequestContext.php
includes/db/PatchFileLocation.php [new file with mode: 0644]
includes/debug/logger/MonologSpi.php
includes/deferred/AutoCommitUpdate.php
includes/diff/DifferenceEngine.php
includes/filerepo/FileRepo.php
includes/filerepo/ForeignDBRepo.php
includes/filerepo/LocalRepo.php
includes/installer/CliInstaller.php
includes/specialpage/FormSpecialPage.php
includes/specialpage/SpecialPage.php
includes/upload/UploadBase.php
maintenance/jsduck/eg-iframe.html
resources/src/mediawiki/mediawiki.base.js
resources/src/mediawiki/mediawiki.js
tests/common/TestsAutoLoader.php
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/RevisionContentHandlerDbTest.php [deleted file]
tests/phpunit/includes/RevisionDbTestBase.php
tests/phpunit/includes/RevisionNoContentHandlerDbTest.php [deleted file]
tests/phpunit/includes/RevisionNoContentModelDbTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionPreMcrDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/McrSchemaDetection.php [new file with mode: 0644]
tests/phpunit/includes/Storage/NoContentModelRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/PreMcrRevisionStoreDbTest.php [new file with mode: 0644]
tests/phpunit/includes/Storage/PreMcrSchemaOverride.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionStoreDbTest.php [deleted file]
tests/phpunit/includes/Storage/RevisionStoreDbTestBase.php [new file with mode: 0644]
tests/phpunit/includes/Storage/RevisionStoreTest.php
tests/phpunit/includes/Storage/create-pre-mcr-fields.sql [new file with mode: 0644]
tests/phpunit/includes/Storage/drop-mcr-tables.sql [new file with mode: 0644]
tests/phpunit/includes/changetags/ChangeTagsTest.php
tests/phpunit/includes/page/WikiPageContentHandlerDbTest.php [deleted file]
tests/phpunit/includes/page/WikiPageDbTestBase.php
tests/phpunit/includes/page/WikiPageNoContentHandlerDbTest.php [deleted file]
tests/phpunit/includes/page/WikiPageNoContentModelDbTest.php [new file with mode: 0644]
tests/phpunit/includes/page/WikiPagePreMcrDbTest.php [new file with mode: 0644]
tests/phpunit/includes/upload/UploadBaseTest.php

index 49d53bd..c98f663 100644 (file)
@@ -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
index 65d79f3..ad62af4 100644 (file)
@@ -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.
index b489428..1adc5e4 100644 (file)
@@ -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',
index f8a4e81..56fb534 100644 (file)
@@ -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
index 13d1623..22c29d6 100644 (file)
@@ -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 );
                }
index ae09602..6b4e4ee 100644 (file)
@@ -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() );
 }
 
 /**
index e8d9a3e..fb22445 100644 (file)
@@ -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 );
        }
 
        /**
index 8a99e6a..98aa554 100644 (file)
@@ -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]}" ) ] );
                }
        }
index 2adc00e..161dd56 100644 (file)
@@ -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 );
                }
        }
 
index 95fe3ab..e8a276c 100644 (file)
@@ -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
index 4ff1004..11df5bc 100644 (file)
@@ -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 );
index b2a91c2..9db8166 100644 (file)
@@ -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 );
        }
 
        /**
index f095b64..a49fb4c 100644 (file)
@@ -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();
        }
index f37ed2d..fc37882 100644 (file)
@@ -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
                );
        }
 
index 8cd7ba8..e06f081 100644 (file)
@@ -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
                );
        }
 
index 429a3b6..0c81144 100644 (file)
@@ -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
index 03fb9e2..6182538 100644 (file)
@@ -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 );
        }
 
        /**
index f7a1815..acf6fcb 100644 (file)
@@ -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 );
        }
 }
index db3a7a9..7563330 100644 (file)
@@ -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 (file)
index 0000000..013724c
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+/**
+ * Trait for finding SQL patch files.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\DB;
+
+use RuntimeException;
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Trait for finding SQL patch files.
+ *
+ * @since 1.32
+ */
+trait PatchFileLocation {
+
+       /**
+        * Utility function for finding the appropriate SQL patch file for the currently
+        * used database type.
+        *
+        * The file will be searched for in the following locations, in order of preference:
+        *   "$patchDir/$name.$dbType.sql",
+        *   "$patchDir/$dbType/$name.sql",
+        *   "$patchDir/$dbType/archives/$name.sql",
+        *   "$patchDir/$name.sql",
+        *   "$patchDir/archives/$name.sql"
+        *
+        * @param IDatabase $db
+        * @param string $name The script name (relative to $patchDir, without the '.sql' suffix)
+        * @param string $patchDir The directory to find the script in. Use __DIR__ to search in the
+        *        directory the calling code is located in. If omitted, the "maintenance"
+        *        directory will be used, where the scripts used by the updater are located.
+        *
+        * @return string
+        * @throws RuntimeException if no matching patch file could be found.
+        */
+       protected function getSqlPatchPath( IDatabase $db, $name, $patchDir = null ) {
+               $dbType = $db->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" );
+       }
+
+}
index 670ba39..ff653ab 100644 (file)
@@ -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 );
                        }
                }
 
index f9297af..8507157 100644 (file)
@@ -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 ) {
index b1ca435..25ba36a 100644 (file)
@@ -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
index 89bb81a..70068b9 100644 (file)
@@ -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 );
        }
 
        /**
index 7879448..4b33138 100644 (file)
@@ -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;
                }
index 03a9d44..9a6ef22 100644 (file)
@@ -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 );
                }
        }
 }
index 2264b80..845408a 100644 (file)
@@ -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 );
                        }
                }
 
index 66c7d47..81a0036 100644 (file)
@@ -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;
+       }
 }
index 6828b4a..317aa0d 100644 (file)
@@ -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();
        }
 
index 27c0ed7..6a471ba 100644 (file)
@@ -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 ) ) {
index 4c02998..f19a69b 100644 (file)
@@ -43,6 +43,7 @@
        </script>
        <script src="modules/lib/jquery/jquery.js"></script>
        <script src="modules/src/mediawiki/mediawiki.js"></script>
+       <script src="modules/src/mediawiki/mediawiki.base.js"></script>
        <script src="modules/src/mediawiki/mediawiki.errorLogger.js"></script>
        <script src="modules/lib/oojs/oojs.jquery.js"></script>
        <script src="modules/lib/oojs-ui/oojs-ui-core.js"></script>
index b68d779..43ee202 100644 (file)
  */
 /* 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 &quot;Wiki&quot; &lt;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 `<key>` 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
                        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();
+       };
 }() );
index c6a3bde..9d34bf1 100644 (file)
                }
        };
 
-       /**
-        * 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 &quot;Wiki&quot; &lt;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 `<key>` 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 */
                                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.
                 *
                // Expose Map constructor
                Map: Map,
 
-               // Expose Message constructor
-               Message: Message,
-
                /**
                 * Map of configuration values.
                 *
                 */
                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,
 
                                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 );
index abf718d..a798679 100644 (file)
@@ -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",
index cf9dde0..6bcbd93 100644 (file)
@@ -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 (file)
index fa0153d..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * @group Database
- * @group medium
- * @group ContentHandler
- */
-class RevisionContentHandlerDbTest extends RevisionDbTestBase {
-
-       protected function getContentHandlerUseDB() {
-               return true;
-       }
-
-}
index 5de34d1..ba1249a 100644 (file)
@@ -43,9 +43,21 @@ abstract class RevisionDbTestBase extends MediaWikiTestCase {
                );
        }
 
+       /**
+        * @return int
+        */
+       abstract protected function getMcrMigrationStage();
+
+       /**
+        * @return string[]
+        */
+       abstract protected function getMcrTablesToReset();
+
        protected function setUp() {
                global $wgContLang;
 
+               $this->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 (file)
index c980a48..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * @group Database
- * @group medium
- * @group ContentHandler
- */
-class RevisionNoContentHandlerDbTest extends RevisionDbTestBase {
-
-       protected function getContentHandlerUseDB() {
-               return false;
-       }
-
-}
diff --git a/tests/phpunit/includes/RevisionNoContentModelDbTest.php b/tests/phpunit/includes/RevisionNoContentModelDbTest.php
new file mode 100644 (file)
index 0000000..7923b22
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Tests Revision against the pre-MCR, pre ContentHandler DB schema.
+ *
+ * @covers Revision
+ *
+ * @group Revision
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class RevisionNoContentModelDbTest extends RevisionDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return false;
+       }
+
+}
diff --git a/tests/phpunit/includes/RevisionPreMcrDbTest.php b/tests/phpunit/includes/RevisionPreMcrDbTest.php
new file mode 100644 (file)
index 0000000..90f1140
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Tests Revision against the pre-MCR DB schema.
+ *
+ * @covers Revision
+ *
+ * @group Revision
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class RevisionPreMcrDbTest extends RevisionDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+}
diff --git a/tests/phpunit/includes/Storage/McrSchemaDetection.php b/tests/phpunit/includes/Storage/McrSchemaDetection.php
new file mode 100644 (file)
index 0000000..c90d428
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use Wikimedia\Rdbms\IDatabase;
+
+/**
+ * Trait providing methods for detecting which MCR schema migration phase the current schema
+ * is compatible with.
+ */
+trait McrSchemaDetection {
+
+       /**
+        * Returns true if MCR-related tables exist in the database.
+        * If yes, the database is compatible with with MIGRATION_NEW.
+        * If hasPreMcrFields() also returns true, the database supports MIGRATION_WRITE_BOTH mode.
+        *
+        * @param IDatabase $db
+        * @return bool
+        */
+       protected function hasMcrTables( IDatabase $db ) {
+               return $db->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 (file)
index 0000000..c77a94a
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+/**
+ * Tests RevisionStore against the pre-MCR, pre-ContentHandler DB schema.
+ *
+ * @covers \MediaWiki\Storage\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class NoContentModelRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return false;
+       }
+
+       public function provideGetArchiveQueryInfo() {
+               yield [
+                       [
+                               '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' => [],
+                       ]
+               ];
+       }
+
+       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 (file)
index 0000000..4336691
--- /dev/null
@@ -0,0 +1,84 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+/**
+ * Tests RevisionStore against the pre-MCR DB schema.
+ *
+ * @covers \MediaWiki\Storage\RevisionStore
+ *
+ * @group RevisionStore
+ * @group Storage
+ * @group Database
+ * @group medium
+ */
+class PreMcrRevisionStoreDbTest extends RevisionStoreDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       public function provideGetArchiveQueryInfo() {
+               yield [
+                       [
+                               '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' => [],
+                       ]
+               ];
+       }
+
+       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 (file)
index 0000000..5d516e8
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+namespace MediaWiki\Tests\Storage;
+
+use Wikimedia\Rdbms\IMaintainableDatabase;
+use MediaWiki\DB\PatchFileLocation;
+
+/**
+ * Trait providing schema overrides that allow tests to run against the pre-MCR database schema.
+ */
+trait PreMcrSchemaOverride {
+
+       use PatchFileLocation;
+       use McrSchemaDetection;
+
+       /**
+        * @return int
+        */
+       protected function getMcrMigrationStage() {
+               return MIGRATION_OLD;
+       }
+
+       /**
+        * @return string[]
+        */
+       protected function getMcrTablesToReset() {
+               return [];
+       }
+
+       /**
+        * @override MediaWikiTestCase::getSchemaOverrides
+        * @return array[]
+        */
+       protected function getSchemaOverrides( IMaintainableDatabase $db ) {
+               $overrides = [
+                       'scripts' => [],
+                       '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 (file)
index 2a92956..0000000
+++ /dev/null
@@ -1,1282 +0,0 @@
-<?php
-
-namespace MediaWiki\Tests\Storage;
-
-use CommentStoreComment;
-use Exception;
-use HashBagOStuff;
-use InvalidArgumentException;
-use Language;
-use MediaWiki\Linker\LinkTarget;
-use MediaWiki\MediaWikiServices;
-use MediaWiki\Storage\BlobStoreFactory;
-use MediaWiki\Storage\IncompleteRevisionException;
-use MediaWiki\Storage\MutableRevisionRecord;
-use MediaWiki\Storage\RevisionRecord;
-use MediaWiki\Storage\RevisionStore;
-use MediaWiki\Storage\SlotRecord;
-use MediaWiki\Storage\SqlBlobStore;
-use MediaWikiTestCase;
-use Revision;
-use TestUserRegistry;
-use Title;
-use WANObjectCache;
-use Wikimedia\Rdbms\Database;
-use Wikimedia\Rdbms\DatabaseSqlite;
-use Wikimedia\Rdbms\FakeResultWrapper;
-use Wikimedia\Rdbms\LoadBalancer;
-use Wikimedia\Rdbms\TransactionProfiler;
-use WikiPage;
-use WikitextContent;
-
-/**
- * @group Database
- */
-class RevisionStoreDbTest extends MediaWikiTestCase {
-
-       public function setUp() {
-               parent::setUp();
-               $this->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 (file)
index 0000000..bdff4cd
--- /dev/null
@@ -0,0 +1,1500 @@
+<?php
+
+namespace MediaWiki\Tests\Storage;
+
+use CommentStoreComment;
+use Exception;
+use HashBagOStuff;
+use InvalidArgumentException;
+use Language;
+use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\BlobStoreFactory;
+use MediaWiki\Storage\IncompleteRevisionException;
+use MediaWiki\Storage\MutableRevisionRecord;
+use MediaWiki\Storage\RevisionRecord;
+use MediaWiki\Storage\RevisionStore;
+use MediaWiki\Storage\SlotRecord;
+use MediaWiki\Storage\SqlBlobStore;
+use MediaWikiTestCase;
+use PHPUnit_Framework_MockObject_MockObject;
+use Revision;
+use TestUserRegistry;
+use Title;
+use WANObjectCache;
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\DatabaseSqlite;
+use Wikimedia\Rdbms\FakeResultWrapper;
+use Wikimedia\Rdbms\LoadBalancer;
+use Wikimedia\Rdbms\TransactionProfiler;
+use WikiPage;
+use WikitextContent;
+
+/**
+ * @group Database
+ * @group RevisionStore
+ */
+abstract class RevisionStoreDbTestBase extends MediaWikiTestCase {
+
+       /**
+        * @return int
+        */
+       abstract protected function getMcrMigrationStage();
+
+       /**
+        * @return bool
+        */
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+       /**
+        * @return string[]
+        */
+       abstract protected function getMcrTablesToReset();
+
+       public function needsDB() {
+               return true;
+       }
+
+       public function setUp() {
+               parent::setUp();
+               $this->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 );
+       }
+
+}
index fed9a0c..3749f29 100644 (file)
@@ -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 (file)
index 0000000..09deb4f
--- /dev/null
@@ -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 (file)
index 0000000..bc89edc
--- /dev/null
@@ -0,0 +1,4 @@
+DROP TABLE /*_*/slots;
+DROP TABLE /*_*/content;
+DROP TABLE /*_*/content_models;
+DROP TABLE /*_*/slot_roles;
index 63e0ec2..215cdfd 100644 (file)
@@ -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 (file)
index 2d7d6cc..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-
-/**
- * @group ContentHandler
- * @group Database
- * @group medium
- */
-class WikiPageContentHandlerDbTest extends WikiPageDbTestBase {
-
-       protected function getContentHandlerUseDB() {
-               return true;
-       }
-
-       /**
-        * @covers WikiPage::getContentModel
-        */
-       public function testGetContentModel() {
-               $page = $this->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() ) );
-       }
-
-}
index 68539b5..cc20b6b 100644 (file)
@@ -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 (file)
index a6ce185..0000000
+++ /dev/null
@@ -1,14 +0,0 @@
-<?php
-
-/**
- * @group ContentHandler
- * @group Database
- * @group medium
- */
-class WikiPageNoContentHandlerDbTest extends WikiPageDbTestBase {
-
-       protected function getContentHandlerUseDB() {
-               return false;
-       }
-
-}
diff --git a/tests/phpunit/includes/page/WikiPageNoContentModelDbTest.php b/tests/phpunit/includes/page/WikiPageNoContentModelDbTest.php
new file mode 100644 (file)
index 0000000..7c9c657
--- /dev/null
@@ -0,0 +1,23 @@
+<?php
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Tests WikiPage against the pre-MCR, pre ContentHandler DB schema.
+ *
+ * @covers WikiPage
+ *
+ * @group WikiPage
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class WikiPageNoContentModelDbTest extends WikiPageDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return false;
+       }
+
+}
diff --git a/tests/phpunit/includes/page/WikiPagePreMcrDbTest.php b/tests/phpunit/includes/page/WikiPagePreMcrDbTest.php
new file mode 100644 (file)
index 0000000..3e7c8fa
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+use MediaWiki\Tests\Storage\PreMcrSchemaOverride;
+
+/**
+ * Tests WikiPage against the pre-MCR DB schema.
+ *
+ * @covers WikiPage
+ *
+ * @group WikiPage
+ * @group Storage
+ * @group ContentHandler
+ * @group Database
+ * @group medium
+ */
+class WikiPagePreMcrDbTest extends WikiPageDbTestBase {
+
+       use PreMcrSchemaOverride;
+
+       protected function getContentHandlerUseDB() {
+               return true;
+       }
+
+       /**
+        * @covers WikiPage::getContentModel
+        */
+       public function testGetContentModel() {
+               $page = $this->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() ) );
+       }
+
+}
index 3541091..a80262e 100644 (file)
@@ -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() {