]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/installer/DatabaseUpdater.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / installer / DatabaseUpdater.php
index 57d82ea3a0604b160f1071c9f5d8eb5b6d7f398e..752bc5445bf5ffe3d2ebc0daa065117beef4fafb 100644 (file)
@@ -2,13 +2,31 @@
 /**
  * DBMS-specific updater helper.
  *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
  * @file
  * @ingroup Deployment
  */
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
+use MediaWiki\MediaWikiServices;
 
-require_once( dirname(__FILE__) . '/../../maintenance/Maintenance.php' );
+require_once __DIR__ . '/../../maintenance/Maintenance.php';
 
-/*
+/**
  * Class for handling database updates. Roughly based off of updaters.inc, with
  * a few improvements :)
  *
@@ -16,58 +34,97 @@ require_once( dirname(__FILE__) . '/../../maintenance/Maintenance.php' );
  * @since 1.17
  */
 abstract class DatabaseUpdater {
-
        /**
         * Array of updates to perform on the database
         *
         * @var array
         */
-       protected $updates = array();
+       protected $updates = [];
 
        /**
-        * List of extension-provided database updates
+        * Array of updates that were skipped
+        *
         * @var array
         */
-       protected $extensionUpdates = array();
+       protected $updatesSkipped = [];
 
        /**
-        * Used to hold schema files during installation process
+        * List of extension-provided database updates
         * @var array
         */
-       protected $newExtensions = array();
+       protected $extensionUpdates = [];
 
        /**
         * Handle to the database subclass
         *
-        * @var DatabaseBase
+        * @var Database
         */
        protected $db;
 
+       /**
+        * @var Maintenance
+        */
+       protected $maintenance;
+
        protected $shared = false;
 
-       protected $postDatabaseUpdateMaintenance = array(
-               'DeleteDefaultMessages'
-       );
+       /**
+        * @var string[] Scripts to run after database update
+        * Should be a subclass of LoggedUpdateMaintenance
+        */
+       protected $postDatabaseUpdateMaintenance = [
+               DeleteDefaultMessages::class,
+               PopulateRevisionLength::class,
+               PopulateRevisionSha1::class,
+               PopulateImageSha1::class,
+               FixExtLinksProtocolRelative::class,
+               PopulateFilearchiveSha1::class,
+               PopulateBacklinkNamespace::class,
+               FixDefaultJsonContentPages::class,
+               CleanupEmptyCategories::class,
+               AddRFCAndPMIDInterwiki::class,
+               PopulatePPSortKey::class,
+               PopulateIpChanges::class,
+       ];
+
+       /**
+        * File handle for SQL output.
+        *
+        * @var resource
+        */
+       protected $fileHandle = null;
 
        /**
-        * Constructor
+        * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
         *
-        * @param $db DatabaseBase object to perform updates on
-        * @param $shared bool Whether to perform updates on shared tables
-        * @param $maintenance Maintenance Maintenance object which created us
+        * @var bool
+        */
+       protected $skipSchema = false;
+
+       /**
+        * Hold the value of $wgContentHandlerUseDB during the upgrade.
         */
-       protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) {
+       protected $holdContentHandlerUseDB = true;
+
+       /**
+        * @param Database &$db To perform updates on
+        * @param bool $shared Whether to perform updates on shared tables
+        * @param Maintenance $maintenance Maintenance object which created us
+        */
+       protected function __construct( Database &$db, $shared, Maintenance $maintenance = null ) {
                $this->db = $db;
                $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
                $this->shared = $shared;
                if ( $maintenance ) {
                        $this->maintenance = $maintenance;
+                       $this->fileHandle = $maintenance->fileHandle;
                } else {
                        $this->maintenance = new FakeMaintenance;
                }
+               $this->maintenance->setDB( $db );
                $this->initOldGlobals();
                $this->loadExtensions();
-               wfRunHooks( 'LoadExtensionSchemaUpdates', array( $this ) );
+               Hooks::run( 'LoadExtensionSchemaUpdates', [ $this ] );
        }
 
        /**
@@ -80,44 +137,58 @@ abstract class DatabaseUpdater {
 
                # For extensions only, should be populated via hooks
                # $wgDBtype should be checked to specifiy the proper file
-               $wgExtNewTables = array(); // table, dir
-               $wgExtNewFields = array(); // table, column, dir
-               $wgExtPGNewFields = array(); // table, column, column attributes; for PostgreSQL
-               $wgExtPGAlteredFields = array(); // table, column, new type, conversion method; for PostgreSQL
-               $wgExtNewIndexes = array(); // table, index, dir
-               $wgExtModifiedFields = array(); // table, index, dir
+               $wgExtNewTables = []; // table, dir
+               $wgExtNewFields = []; // table, column, dir
+               $wgExtPGNewFields = []; // table, column, column attributes; for PostgreSQL
+               $wgExtPGAlteredFields = []; // table, column, new type, conversion method; for PostgreSQL
+               $wgExtNewIndexes = []; // table, index, dir
+               $wgExtModifiedFields = []; // table, index, dir
        }
 
        /**
-        * Loads LocalSettings.php, if needed, and initialises everything needed for LoadExtensionSchemaUpdates hook
+        * Loads LocalSettings.php, if needed, and initialises everything needed for
+        * LoadExtensionSchemaUpdates hook.
         */
        private function loadExtensions() {
                if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
                        return; // already loaded
                }
                $vars = Installer::getExistingLocalSettings();
-               if ( !$vars ) {
-                       return; // no LocalSettings found
+
+               $registry = ExtensionRegistry::getInstance();
+               $queue = $registry->getQueue();
+               // Don't accidentally load extensions in the future
+               $registry->clearQueue();
+
+               // This will automatically add "AutoloadClasses" to $wgAutoloadClasses
+               $data = $registry->readFromQueue( $queue );
+               $hooks = [ 'wgHooks' => [ 'LoadExtensionSchemaUpdates' => [] ] ];
+               if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
+                       $hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'];
                }
-               if ( !isset( $vars['wgHooks'] ) || !isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
-                       return;
+               if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
+                       $hooks = array_merge_recursive( $hooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
                }
                global $wgHooks, $wgAutoloadClasses;
-               $wgHooks['LoadExtensionSchemaUpdates'] = $vars['wgHooks']['LoadExtensionSchemaUpdates'];
-               $wgAutoloadClasses = $wgAutoloadClasses + $vars['wgAutoloadClasses'];
+               $wgHooks['LoadExtensionSchemaUpdates'] = $hooks;
+               if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
+                       $wgAutoloadClasses += $vars['wgAutoloadClasses'];
+               }
        }
 
        /**
-        * @throws MWException
-        * @param DatabaseBase $db
+        * @param Database $db
         * @param bool $shared
-        * @param null $maintenance
+        * @param Maintenance $maintenance
+        *
+        * @throws MWException
         * @return DatabaseUpdater
         */
-       public static function newForDB( &$db, $shared = false, $maintenance = null ) {
+       public static function newForDB( Database $db, $shared = false, $maintenance = null ) {
                $type = $db->getType();
-               if( in_array( $type, Installer::getDBTypes() ) ) {
+               if ( in_array( $type, Installer::getDBTypes() ) ) {
                        $class = ucfirst( $type ) . 'Updater';
+
                        return new $class( $db, $shared, $maintenance );
                } else {
                        throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
@@ -127,7 +198,7 @@ abstract class DatabaseUpdater {
        /**
         * Get a database connection to run updates
         *
-        * @return DatabaseBase
+        * @return Database
         */
        public function getDB() {
                return $this->db;
@@ -136,14 +207,14 @@ abstract class DatabaseUpdater {
        /**
         * Output some text. If we're running from web, escape the text first.
         *
-        * @param $str String: Text to output
+        * @param string $str Text to output
         */
        public function output( $str ) {
                if ( $this->maintenance->isQuiet() ) {
                        return;
                }
                global $wgCommandLineMode;
-               if( !$wgCommandLineMode ) {
+               if ( !$wgCommandLineMode ) {
                        $str = htmlspecialchars( $str );
                }
                echo $str;
@@ -154,66 +225,213 @@ abstract class DatabaseUpdater {
         * Add a new update coming from an extension. This should be called by
         * extensions while executing the LoadExtensionSchemaUpdates hook.
         *
-        * @param $update Array: the update to run. Format is the following:
-        *                first item is the callback function, it also can be a
-        *                simple string with the name of a function in this class,
-        *                following elements are parameters to the function.
-        *                Note that callback functions will receive this object as
-        *                first parameter.
+        * @since 1.17
+        *
+        * @param array $update The update to run. Format is [ $callback, $params... ]
+        *   $callback is the method to call; either a DatabaseUpdater method name or a callable.
+        *   Must be serializable (ie. no anonymous functions allowed). The rest of the parameters
+        *   (if any) will be passed to the callback. The first parameter passed to the callback
+        *   is always this object.
         */
-       public function addExtensionUpdate( Array $update ) {
+       public function addExtensionUpdate( array $update ) {
                $this->extensionUpdates[] = $update;
        }
 
        /**
         * Convenience wrapper for addExtensionUpdate() when adding a new table (which
         * is the most common usage of updaters in an extension)
-        * @param $tableName String Name of table to create
-        * @param $sqlPath String Full path to the schema file
+        *
+        * @since 1.18
+        *
+        * @param string $tableName Name of table to create
+        * @param string $sqlPath Full path to the schema file
         */
        public function addExtensionTable( $tableName, $sqlPath ) {
-               $this->extensionUpdates[] = array( 'addTable', $tableName, $sqlPath, true );
+               $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
        }
 
        /**
-        * Add a brand new extension to MediaWiki. Used during the initial install
-        * @param $ext String Name of extension
-        * @param $sqlPath String Full path to the schema file
+        * @since 1.19
+        *
+        * @param string $tableName
+        * @param string $indexName
+        * @param string $sqlPath
         */
-       public function addNewExtension( $ext, $sqlPath ) {
-               $this->newExtensions[ strtolower( $ext ) ] = $sqlPath;
+       public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
        }
 
        /**
-        * Get the list of extensions that registered a schema with our DB type
-        * @return array
+        *
+        * @since 1.19
+        *
+        * @param string $tableName
+        * @param string $columnName
+        * @param string $sqlPath
         */
-       public function getNewExtensions() {
-               return $this->newExtensions;
+       public function addExtensionField( $tableName, $columnName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
+       }
+
+       /**
+        *
+        * @since 1.20
+        *
+        * @param string $tableName
+        * @param string $columnName
+        * @param string $sqlPath
+        */
+       public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
+       }
+
+       /**
+        * Drop an index from an extension table
+        *
+        * @since 1.21
+        *
+        * @param string $tableName The table name
+        * @param string $indexName The index name
+        * @param string $sqlPath The path to the SQL change path
+        */
+       public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
+       }
+
+       /**
+        *
+        * @since 1.20
+        *
+        * @param string $tableName
+        * @param string $sqlPath
+        */
+       public function dropExtensionTable( $tableName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
+       }
+
+       /**
+        * Rename an index on an extension table
+        *
+        * @since 1.21
+        *
+        * @param string $tableName The table name
+        * @param string $oldIndexName The old index name
+        * @param string $newIndexName The new index name
+        * @param string $sqlPath The path to the SQL change path
+        * @param bool $skipBothIndexExistWarning Whether to warn if both the old
+        * and the new indexes exist. [facultative; by default, false]
+        */
+       public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
+               $sqlPath, $skipBothIndexExistWarning = false
+       ) {
+               $this->extensionUpdates[] = [
+                       'renameIndex',
+                       $tableName,
+                       $oldIndexName,
+                       $newIndexName,
+                       $skipBothIndexExistWarning,
+                       $sqlPath,
+                       true
+               ];
+       }
+
+       /**
+        * @since 1.21
+        *
+        * @param string $tableName The table name
+        * @param string $fieldName The field to be modified
+        * @param string $sqlPath The path to the SQL change path
+        */
+       public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
+               $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
+       }
+
+       /**
+        *
+        * @since 1.20
+        *
+        * @param string $tableName
+        * @return bool
+        */
+       public function tableExists( $tableName ) {
+               return ( $this->db->tableExists( $tableName, __METHOD__ ) );
+       }
+
+       /**
+        * Add a maintenance script to be run after the database updates are complete.
+        *
+        * Script should subclass LoggedUpdateMaintenance
+        *
+        * @since 1.19
+        *
+        * @param string $class Name of a Maintenance subclass
+        */
+       public function addPostDatabaseUpdateMaintenance( $class ) {
+               $this->postDatabaseUpdateMaintenance[] = $class;
        }
 
        /**
         * Get the list of extension-defined updates
         *
-        * @return Array
+        * @return array
         */
        protected function getExtensionUpdates() {
                return $this->extensionUpdates;
        }
 
+       /**
+        * @since 1.17
+        *
+        * @return string[]
+        */
        public function getPostDatabaseUpdateMaintenance() {
                return $this->postDatabaseUpdateMaintenance;
        }
 
+       /**
+        * @since 1.21
+        *
+        * Writes the schema updates desired to a file for the DB Admin to run.
+        * @param array $schemaUpdate
+        */
+       private function writeSchemaUpdateFile( array $schemaUpdate = [] ) {
+               $updates = $this->updatesSkipped;
+               $this->updatesSkipped = [];
+
+               foreach ( $updates as $funcList ) {
+                       $func = $funcList[0];
+                       $arg = $funcList[1];
+                       $origParams = $funcList[2];
+                       call_user_func_array( $func, $arg );
+                       flush();
+                       $this->updatesSkipped[] = $origParams;
+               }
+       }
+
+       /**
+        * Get appropriate schema variables in the current database connection.
+        *
+        * This should be called after any request data has been imported, but before
+        * any write operations to the database. The result should be passed to the DB
+        * setSchemaVars() method.
+        *
+        * @return array
+        * @since 1.28
+        */
+       public function getSchemaVars() {
+               return []; // DB-type specific
+       }
+
        /**
         * Do all the updates
         *
-        * @param $what Array: what updates to perform
+        * @param array $what What updates to perform
         */
-       public function doUpdates( $what = array( 'core', 'extensions', 'purge' ) ) {
-               global $wgVersion;
+       public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
+               $this->db->setSchemaVars( $this->getSchemaVars() );
 
                $what = array_flip( $what );
+               $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
                if ( isset( $what['core'] ) ) {
                        $this->runUpdates( $this->getCoreUpdateList(), false );
                }
@@ -222,73 +440,120 @@ abstract class DatabaseUpdater {
                        $this->runUpdates( $this->getExtensionUpdates(), true );
                }
 
-               $this->setAppliedUpdates( $wgVersion, $this->updates );
-
-               if( isset( $what['purge'] ) ) {
-                       $this->purgeCache();
-               }
-               if ( isset( $what['core'] ) ) {
+               if ( isset( $what['stats'] ) ) {
                        $this->checkStats();
                }
+
+               if ( $this->fileHandle ) {
+                       $this->skipSchema = false;
+                       $this->writeSchemaUpdateFile();
+               }
        }
 
        /**
         * Helper function for doUpdates()
         *
-        * @param $updates Array of updates to run
-        * @param $passSelf Boolean: whether to pass this object we calling external
-        *                  functions
+        * @param array $updates Array of updates to run
+        * @param bool $passSelf Whether to pass this object we calling external functions
         */
        private function runUpdates( array $updates, $passSelf ) {
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+
+               $updatesDone = [];
+               $updatesSkipped = [];
                foreach ( $updates as $params ) {
+                       $origParams = $params;
                        $func = array_shift( $params );
-                       if( !is_array( $func ) && method_exists( $this, $func ) ) {
-                               $func = array( $this, $func );
+                       if ( !is_array( $func ) && method_exists( $this, $func ) ) {
+                               $func = [ $this, $func ];
                        } elseif ( $passSelf ) {
                                array_unshift( $params, $this );
                        }
-                       call_user_func_array( $func, $params );
+                       $ret = call_user_func_array( $func, $params );
                        flush();
+                       if ( $ret !== false ) {
+                               $updatesDone[] = $origParams;
+                               $lbFactory->waitForReplication();
+                       } else {
+                               $updatesSkipped[] = [ $func, $params, $origParams ];
+                       }
                }
-               $this->updates = array_merge( $this->updates, $updates );
-       }
-
-       protected function setAppliedUpdates( $version, $updates = array() ) {
-               if( !$this->canUseNewUpdatelog() ) {
-                       return;
-               }
-               $key = "updatelist-$version-" . time();
-               $this->db->insert( 'updatelog',
-                       array( 'ul_key' => $key, 'ul_value' => serialize( $updates ) ),
-                        __METHOD__ );
+               $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
+               $this->updates = array_merge( $this->updates, $updatesDone );
        }
 
        /**
         * Helper function: check if the given key is present in the updatelog table.
         * Obviously, only use this for updates that occur after the updatelog table was
         * created!
+        * @param string $key Name of the key to check for
+        * @return bool
         */
        public function updateRowExists( $key ) {
                $row = $this->db->selectRow(
                        'updatelog',
-                       '1',
-                       array( 'ul_key' => $key ),
+                       # T67813
+                       '1 AS X',
+                       [ 'ul_key' => $key ],
                        __METHOD__
                );
+
                return (bool)$row;
        }
 
+       /**
+        * Helper function: Add a key to the updatelog table
+        * Obviously, only use this for updates that occur after the updatelog table was
+        * created!
+        * @param string $key Name of key to insert
+        * @param string $val [optional] Value to insert along with the key
+        */
+       public function insertUpdateRow( $key, $val = null ) {
+               $this->db->clearFlag( DBO_DDLMODE );
+               $values = [ 'ul_key' => $key ];
+               if ( $val && $this->canUseNewUpdatelog() ) {
+                       $values['ul_value'] = $val;
+               }
+               $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
+               $this->db->setFlag( DBO_DDLMODE );
+       }
+
        /**
         * Updatelog was changed in 1.17 to have a ul_value column so we can record
         * more information about what kind of updates we've done (that's what this
         * class does). Pre-1.17 wikis won't have this column, and really old wikis
         * might not even have updatelog at all
         *
-        * @return boolean
+        * @return bool
         */
        protected function canUseNewUpdatelog() {
-               return $this->db->tableExists( 'updatelog' ) &&
-                       $this->db->fieldExists( 'updatelog', 'ul_value' );
+               return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
+                       $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
+       }
+
+       /**
+        * Returns whether updates should be executed on the database table $name.
+        * Updates will be prevented if the table is a shared table and it is not
+        * specified to run updates on shared tables.
+        *
+        * @param string $name Table name
+        * @return bool
+        */
+       protected function doTable( $name ) {
+               global $wgSharedDB, $wgSharedTables;
+
+               // Don't bother to check $wgSharedTables if there isn't a shared database
+               // or the user actually also wants to do updates on the shared database.
+               if ( $wgSharedDB === null || $this->shared ) {
+                       return true;
+               }
+
+               if ( in_array( $name, $wgSharedTables ) ) {
+                       $this->output( "...skipping update to shared table $name.\n" );
+                       return false;
+               } else {
+                       return true;
+               }
        }
 
        /**
@@ -296,44 +561,40 @@ abstract class DatabaseUpdater {
         * $wgExtNewTables/Fields/Indexes. This is nasty :) We refactored a lot
         * of this in 1.17 but we want to remain back-compatible for a while. So
         * load up these old global-based things into our update list.
+        *
+        * @return array
         */
        protected function getOldGlobalUpdates() {
                global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
-                       $wgExtNewIndexes, $wgSharedDB, $wgSharedTables;
+                       $wgExtNewIndexes;
 
-               $doUser = $this->shared ?
-                       $wgSharedDB && in_array( 'user', $wgSharedTables ) :
-                       !$wgSharedDB || !in_array( 'user', $wgSharedTables );
-
-               $updates = array();
+               $updates = [];
 
                foreach ( $wgExtNewTables as $tableRecord ) {
-                       $updates[] = array(
+                       $updates[] = [
                                'addTable', $tableRecord[0], $tableRecord[1], true
-                       );
+                       ];
                }
 
                foreach ( $wgExtNewFields as $fieldRecord ) {
-                       if ( $fieldRecord[0] != 'user' || $doUser ) {
-                               $updates[] = array(
-                                       'addField', $fieldRecord[0], $fieldRecord[1],
-                                               $fieldRecord[2], true
-                               );
-                       }
+                       $updates[] = [
+                               'addField', $fieldRecord[0], $fieldRecord[1],
+                               $fieldRecord[2], true
+                       ];
                }
 
                foreach ( $wgExtNewIndexes as $fieldRecord ) {
-                       $updates[] = array(
+                       $updates[] = [
                                'addIndex', $fieldRecord[0], $fieldRecord[1],
-                                       $fieldRecord[2], true
-                       );
+                               $fieldRecord[2], true
+                       ];
                }
 
                foreach ( $wgExtModifiedFields as $fieldRecord ) {
-                       $updates[] = array(
+                       $updates[] = [
                                'modifyField', $fieldRecord[0], $fieldRecord[1],
-                                       $fieldRecord[2], true
-                       );
+                               $fieldRecord[2], true
+                       ];
                }
 
                return $updates;
@@ -345,139 +606,402 @@ abstract class DatabaseUpdater {
         * 1.13...) with the values being arrays of updates, identical to how
         * updaters.inc did it (for now)
         *
-        * @return Array
+        * @return array
+        */
+       abstract protected function getCoreUpdateList();
+
+       /**
+        * Append an SQL fragment to the open file handle.
+        *
+        * @param string $filename File name to open
         */
-       protected abstract function getCoreUpdateList();
+       public function copyFile( $filename ) {
+               $this->db->sourceFile(
+                       $filename,
+                       null,
+                       null,
+                       __METHOD__,
+                       [ $this, 'appendLine' ]
+               );
+       }
+
+       /**
+        * Append a line to the open filehandle.  The line is assumed to
+        * be a complete SQL statement.
+        *
+        * This is used as a callback for sourceLine().
+        *
+        * @param string $line Text to append to the file
+        * @return bool False to skip actually executing the file
+        * @throws MWException
+        */
+       public function appendLine( $line ) {
+               $line = rtrim( $line ) . ";\n";
+               if ( fwrite( $this->fileHandle, $line ) === false ) {
+                       throw new MWException( "trouble writing file" );
+               }
+
+               return false;
+       }
 
        /**
         * Applies a SQL patch
-        * @param $path String Path to the patch file
-        * @param $isFullPath Boolean Whether to treat $path as a relative or not
+        *
+        * @param string $path Path to the patch file
+        * @param bool $isFullPath Whether to treat $path as a relative or not
+        * @param string $msg Description of the patch
+        * @return bool False if patch is skipped.
         */
-       protected function applyPatch( $path, $isFullPath = false ) {
-               if ( $isFullPath ) {
+       protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
+               if ( $msg === null ) {
+                       $msg = "Applying $path patch";
+               }
+               if ( $this->skipSchema ) {
+                       $this->output( "...skipping schema change ($msg).\n" );
+
+                       return false;
+               }
+
+               $this->output( "$msg ..." );
+
+               if ( !$isFullPath ) {
+                       $path = $this->patchPath( $this->db, $path );
+               }
+               if ( $this->fileHandle !== null ) {
+                       $this->copyFile( $path );
+               } else {
                        $this->db->sourceFile( $path );
+               }
+               $this->output( "done.\n" );
+
+               return true;
+       }
+
+       /**
+        * Get the full path of a patch file. Originally based on archive()
+        * from updaters.inc. Keep in mind this always returns a patch, as
+        * it fails back to MySQL if no DB-specific patch can be found
+        *
+        * @param IDatabase $db
+        * @param string $patch The name of the patch, like patch-something.sql
+        * @return string Full path to patch file
+        */
+       public function patchPath( IDatabase $db, $patch ) {
+               global $IP;
+
+               $dbType = $db->getType();
+               if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
+                       return "$IP/maintenance/$dbType/archives/$patch";
                } else {
-                       $this->db->sourceFile( $this->db->patchPath( $path ) );
+                       return "$IP/maintenance/archives/$patch";
                }
        }
 
        /**
         * Add a new table to the database
-        * @param $name String Name of the new table
-        * @param $patch String Path to the patch file
-        * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        *
+        * @param string $name Name of the new table
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        protected function addTable( $name, $patch, $fullpath = false ) {
-               if ( $this->db->tableExists( $name ) ) {
+               if ( !$this->doTable( $name ) ) {
+                       return true;
+               }
+
+               if ( $this->db->tableExists( $name, __METHOD__ ) ) {
                        $this->output( "...$name table already exists.\n" );
                } else {
-                       $this->output( "Creating $name table..." );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+                       return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
                }
+
+               return true;
        }
 
        /**
         * Add a new field to an existing table
-        * @param $table String Name of the table to modify
-        * @param $field String Name of the new field
-        * @param $patch String Path to the patch file
-        * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        *
+        * @param string $table Name of the table to modify
+        * @param string $field Name of the new field
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        protected function addField( $table, $field, $patch, $fullpath = false ) {
-               if ( !$this->db->tableExists( $table ) ) {
-                       $this->output( "...$table table does not exist, skipping new field patch\n" );
-               } elseif ( $this->db->fieldExists( $table, $field ) ) {
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...$table table does not exist, skipping new field patch.\n" );
+               } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
                        $this->output( "...have $field field in $table table.\n" );
                } else {
-                       $this->output( "Adding $field field to table $table..." );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+                       return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
                }
+
+               return true;
        }
 
        /**
         * Add a new index to an existing table
-        * @param $table String Name of the table to modify
-        * @param $index String Name of the new index
-        * @param $patch String Path to the patch file
-        * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        *
+        * @param string $table Name of the table to modify
+        * @param string $index Name of the new index
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        protected function addIndex( $table, $index, $patch, $fullpath = false ) {
-               if ( $this->db->indexExists( $table, $index ) ) {
-                       $this->output( "...$index key already set on $table table.\n" );
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+               } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
+                       $this->output( "...index $index already set on $table table.\n" );
                } else {
-                       $this->output( "Adding $index key to table $table... " );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+                       return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
                }
+
+               return true;
        }
 
        /**
         * Drop a field from an existing table
         *
-        * @param $table String Name of the table to modify
-        * @param $field String Name of the old field
-        * @param $patch String Path to the patch file
-        * @param $fullpath Boolean Whether to treat $patch path as a relative or not
+        * @param string $table Name of the table to modify
+        * @param string $field Name of the old field
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        protected function dropField( $table, $field, $patch, $fullpath = false ) {
-               if ( $this->db->fieldExists( $table, $field ) ) {
-                       $this->output( "Table $table contains $field field. Dropping... " );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
+                       return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
                } else {
                        $this->output( "...$table table does not contain $field field.\n" );
                }
+
+               return true;
        }
 
        /**
         * Drop an index from an existing table
         *
-        * @param $table String: Name of the table to modify
-        * @param $index String: Name of the old index
-        * @param $patch String: Path to the patch file
-        * @param $fullpath Boolean: Whether to treat $patch path as a relative or not
+        * @param string $table Name of the table to modify
+        * @param string $index Name of the index
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
-               if ( $this->db->indexExists( $table, $index ) ) {
-                       $this->output( "Dropping $index from table $table... " );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
+                       return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
                } else {
                        $this->output( "...$index key doesn't exist.\n" );
                }
+
+               return true;
+       }
+
+       /**
+        * Rename an index from an existing table
+        *
+        * @param string $table Name of the table to modify
+        * @param string $oldIndex Old name of the index
+        * @param string $newIndex New name of the index
+        * @param bool $skipBothIndexExistWarning Whether to warn if both the
+        * old and the new indexes exist.
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
+        */
+       protected function renameIndex( $table, $oldIndex, $newIndex,
+               $skipBothIndexExistWarning, $patch, $fullpath = false
+       ) {
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               // First requirement: the table must exist
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
+
+                       return true;
+               }
+
+               // Second requirement: the new index must be missing
+               if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
+                       $this->output( "...index $newIndex already set on $table table.\n" );
+                       if ( !$skipBothIndexExistWarning &&
+                               $this->db->indexExists( $table, $oldIndex, __METHOD__ )
+                       ) {
+                               $this->output( "...WARNING: $oldIndex still exists, despite it has " .
+                                       "been renamed into $newIndex (which also exists).\n" .
+                                       "            $oldIndex should be manually removed if not needed anymore.\n" );
+                       }
+
+                       return true;
+               }
+
+               // Third requirement: the old index must exist
+               if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
+                       $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
+
+                       return true;
+               }
+
+               // Requirements have been satisfied, patch can be applied
+               return $this->applyPatch(
+                       $patch,
+                       $fullpath,
+                       "Renaming index $oldIndex into $newIndex to table $table"
+               );
+       }
+
+       /**
+        * If the specified table exists, drop it, or execute the
+        * patch if one is provided.
+        *
+        * Public @since 1.20
+        *
+        * @param string $table Table to drop.
+        * @param string|bool $patch String of patch file that will drop the table. Default: false.
+        * @param bool $fullpath Whether $patch is a full path. Default: false.
+        * @return bool False if this was skipped because schema changes are skipped
+        */
+       public function dropTable( $table, $patch = false, $fullpath = false ) {
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               if ( $this->db->tableExists( $table, __METHOD__ ) ) {
+                       $msg = "Dropping table $table";
+
+                       if ( $patch === false ) {
+                               $this->output( "$msg ..." );
+                               $this->db->dropTable( $table, __METHOD__ );
+                               $this->output( "done.\n" );
+                       } else {
+                               return $this->applyPatch( $patch, $fullpath, $msg );
+                       }
+               } else {
+                       $this->output( "...$table doesn't exist.\n" );
+               }
+
+               return true;
        }
 
        /**
         * Modify an existing field
         *
-        * @param $table String: name of the table to which the field belongs
-        * @param $field String: name of the field to modify
-        * @param $patch String: path to the patch file
-        * @param $fullpath Boolean: whether to treat $patch path as a relative or not
+        * @param string $table Name of the table to which the field belongs
+        * @param string $field Name of the field to modify
+        * @param string $patch Path to the patch file
+        * @param bool $fullpath Whether to treat $patch path as a relative or not
+        * @return bool False if this was skipped because schema changes are skipped
         */
        public function modifyField( $table, $field, $patch, $fullpath = false ) {
-               if ( !$this->db->tableExists( $table ) ) {
-                       $this->output( "...$table table does not exist, skipping modify field patch\n" );
-               } elseif ( !$this->db->fieldExists( $table, $field ) ) {
-                       $this->output( "...$field field does not exist in $table table, skipping modify field patch\n" );
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               $updateKey = "$table-$field-$patch";
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...$table table does not exist, skipping modify field patch.\n" );
+               } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
+                       $this->output( "...$field field does not exist in $table table, " .
+                               "skipping modify field patch.\n" );
+               } elseif ( $this->updateRowExists( $updateKey ) ) {
+                       $this->output( "...$field in table $table already modified by patch $patch.\n" );
+               } else {
+                       $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
+                       if ( $apply ) {
+                               $this->insertUpdateRow( $updateKey );
+                       }
+                       return $apply;
+               }
+               return true;
+       }
+
+       /**
+        * Modify an existing table, similar to modifyField. Intended for changes that
+        *  touch more than one column on a table.
+        *
+        * @param string $table Name of the table to modify
+        * @param string $patch Name of the patch file to apply
+        * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
+        * @return bool False if this was skipped because of schema changes being skipped
+        */
+       public function modifyTable( $table, $patch, $fullpath = false ) {
+               if ( !$this->doTable( $table ) ) {
+                       return true;
+               }
+
+               $updateKey = "$table-$patch";
+               if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
+                       $this->output( "...$table table does not exist, skipping modify table patch.\n" );
+               } elseif ( $this->updateRowExists( $updateKey ) ) {
+                       $this->output( "...table $table already modified by patch $patch.\n" );
                } else {
-                       $this->output( "Modifying $field field of table $table..." );
-                       $this->applyPatch( $patch, $fullpath );
-                       $this->output( "ok\n" );
+                       $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
+                       if ( $apply ) {
+                               $this->insertUpdateRow( $updateKey );
+                       }
+                       return $apply;
+               }
+               return true;
+       }
+
+       /**
+        * Set any .htaccess files or equivilent for storage repos
+        *
+        * Some zones (e.g. "temp") used to be public and may have been initialized as such
+        */
+       public function setFileAccess() {
+               $repo = RepoGroup::singleton()->getLocalRepo();
+               $zonePath = $repo->getZonePath( 'temp' );
+               if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
+                       // If the directory was never made, then it will have the right ACLs when it is made
+                       $status = $repo->getBackend()->secure( [
+                               'dir' => $zonePath,
+                               'noAccess' => true,
+                               'noListing' => true
+                       ] );
+                       if ( $status->isOK() ) {
+                               $this->output( "Set the local repo temp zone container to be private.\n" );
+                       } else {
+                               $this->output( "Failed to set the local repo temp zone container to be private.\n" );
+                       }
                }
        }
 
        /**
         * Purge the objectcache table
         */
-       protected function purgeCache() {
+       public function purgeCache() {
+               global $wgLocalisationCacheConf;
                # We can't guarantee that the user will be able to use TRUNCATE,
                # but we know that DELETE is available to us
                $this->output( "Purging caches..." );
                $this->db->delete( 'objectcache', '*', __METHOD__ );
+               if ( $wgLocalisationCacheConf['manualRecache'] ) {
+                       $this->rebuildLocalisationCache();
+               }
+               $blobStore = new MessageBlobStore();
+               $blobStore->clear();
+               $this->db->delete( 'module_deps', '*', __METHOD__ );
                $this->output( "done.\n" );
        }
 
@@ -485,90 +1009,207 @@ abstract class DatabaseUpdater {
         * Check the site_stats table is not properly populated.
         */
        protected function checkStats() {
-               $this->output( "Checking site_stats row..." );
-               $row = $this->db->selectRow( 'site_stats', '*', array( 'ss_row_id' => 1 ), __METHOD__ );
+               $this->output( "...site_stats is populated..." );
+               $row = $this->db->selectRow( 'site_stats', '*', [ 'ss_row_id' => 1 ], __METHOD__ );
                if ( $row === false ) {
                        $this->output( "data is missing! rebuilding...\n" );
                } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
                        $this->output( "missing ss_total_pages, rebuilding...\n" );
                } else {
                        $this->output( "done.\n" );
+
                        return;
                }
-               SiteStatsInit::doAllAndCommit( false );
+               SiteStatsInit::doAllAndCommit( $this->db );
        }
 
        # Common updater functions
 
+       /**
+        * Sets the number of active users in the site_stats table
+        */
        protected function doActiveUsersInit() {
                $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
                if ( $activeUsers == -1 ) {
                        $activeUsers = $this->db->selectField( 'recentchanges',
                                'COUNT( DISTINCT rc_user_text )',
-                               array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ), __METHOD__
+                               [ 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ], __METHOD__
                        );
                        $this->db->update( 'site_stats',
-                               array( 'ss_active_users' => intval( $activeUsers ) ),
-                               array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 )
+                               [ 'ss_active_users' => intval( $activeUsers ) ],
+                               [ 'ss_row_id' => 1 ], __METHOD__, [ 'LIMIT' => 1 ]
                        );
                }
                $this->output( "...ss_active_users user count set...\n" );
        }
 
+       /**
+        * Populates the log_user_text field in the logging table
+        */
        protected function doLogUsertextPopulation() {
-               if ( $this->updateRowExists( 'populate log_usertext' ) ) {
-                       $this->output( "...log_user_text field already populated.\n" );
-                       return;
-               }
+               if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
+                       $this->output(
+                               "Populating log_user_text field, printing progress markers. For large\n" .
+                               "databases, you may want to hit Ctrl-C and do this manually with\n" .
+                               "maintenance/populateLogUsertext.php.\n"
+                       );
 
-               $this->output(
-                       "Populating log_user_text field, printing progress markers. For large\n" .
-                       "databases, you may want to hit Ctrl-C and do this manually with\n" .
-                       "maintenance/populateLogUsertext.php.\n" );
-               $task = new PopulateLogUsertext();
-               $task->execute();
-               $this->output( "Done populating log_user_text field.\n" );
+                       $task = $this->maintenance->runChild( 'PopulateLogUsertext' );
+                       $task->execute();
+                       $this->output( "done.\n" );
+               }
        }
 
+       /**
+        * Migrate log params to new table and index for searching
+        */
        protected function doLogSearchPopulation() {
-               if ( $this->updateRowExists( 'populate log_search' ) ) {
-                       $this->output( "...log_search table already populated.\n" );
-                       return;
-               }
+               if ( !$this->updateRowExists( 'populate log_search' ) ) {
+                       $this->output(
+                               "Populating log_search table, printing progress markers. For large\n" .
+                               "databases, you may want to hit Ctrl-C and do this manually with\n" .
+                               "maintenance/populateLogSearch.php.\n" );
 
-               $this->output(
-                       "Populating log_search table, printing progress markers. For large\n" .
-                       "databases, you may want to hit Ctrl-C and do this manually with\n" .
-                       "maintenance/populateLogSearch.php.\n" );
-               $task = new PopulateLogSearch();
-               $task->execute();
-               $this->output( "Done populating log_search table.\n" );
+                       $task = $this->maintenance->runChild( 'PopulateLogSearch' );
+                       $task->execute();
+                       $this->output( "done.\n" );
+               }
        }
 
+       /**
+        * Updates the timestamps in the transcache table
+        * @return bool
+        */
        protected function doUpdateTranscacheField() {
                if ( $this->updateRowExists( 'convert transcache field' ) ) {
                        $this->output( "...transcache tc_time already converted.\n" );
-                       return;
+
+                       return true;
                }
 
-               $this->output( "Converting tc_time from UNIX epoch to MediaWiki timestamp... " );
-               $this->applyPatch( 'patch-tc-timestamp.sql' );
-               $this->output( "ok\n" );
+               return $this->applyPatch( 'patch-tc-timestamp.sql', false,
+                       "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
        }
 
+       /**
+        * Update CategoryLinks collation
+        */
        protected function doCollationUpdate() {
                global $wgCategoryCollation;
-               if ( $this->db->selectField(
-                       'categorylinks',
-                       'COUNT(*)',
-                       'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
-                       __METHOD__
-               ) == 0 ) {
-                       $this->output( "...collations up-to-date.\n" );
+               if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
+                       if ( $this->db->selectField(
+                               'categorylinks',
+                               'COUNT(*)',
+                               'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
+                               __METHOD__
+                               ) == 0
+                       ) {
+                               $this->output( "...collations up-to-date.\n" );
+
+                               return;
+                       }
+
+                       $this->output( "Updating category collations..." );
+                       $task = $this->maintenance->runChild( 'UpdateCollation' );
+                       $task->execute();
+                       $this->output( "...done.\n" );
+               }
+       }
+
+       /**
+        * Migrates user options from the user table blob to user_properties
+        */
+       protected function doMigrateUserOptions() {
+               if ( $this->db->tableExists( 'user_properties' ) ) {
+                       $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
+                       $cl->execute();
+                       $this->output( "done.\n" );
+               }
+       }
+
+       /**
+        * Enable profiling table when it's turned on
+        */
+       protected function doEnableProfiling() {
+               global $wgProfiler;
+
+               if ( !$this->doTable( 'profiling' ) ) {
                        return;
                }
 
-               $task = new UpdateCollation();
-               $task->execute();
+               $profileToDb = false;
+               if ( isset( $wgProfiler['output'] ) ) {
+                       $out = $wgProfiler['output'];
+                       if ( $out === 'db' ) {
+                               $profileToDb = true;
+                       } elseif ( is_array( $out ) && in_array( 'db', $out ) ) {
+                               $profileToDb = true;
+                       }
+               }
+
+               if ( $profileToDb && !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
+                       $this->applyPatch( 'patch-profiling.sql', false, 'Add profiling table' );
+               }
+       }
+
+       /**
+        * Rebuilds the localisation cache
+        */
+       protected function rebuildLocalisationCache() {
+               /**
+                * @var $cl RebuildLocalisationCache
+                */
+               $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
+               $this->output( "Rebuilding localisation cache...\n" );
+               $cl->setForce();
+               $cl->execute();
+               $this->output( "done.\n" );
+       }
+
+       /**
+        * Turns off content handler fields during parts of the upgrade
+        * where they aren't available.
+        */
+       protected function disableContentHandlerUseDB() {
+               global $wgContentHandlerUseDB;
+
+               if ( $wgContentHandlerUseDB ) {
+                       $this->output( "Turning off Content Handler DB fields for this part of upgrade.\n" );
+                       $this->holdContentHandlerUseDB = $wgContentHandlerUseDB;
+                       $wgContentHandlerUseDB = false;
+               }
+       }
+
+       /**
+        * Turns content handler fields back on.
+        */
+       protected function enableContentHandlerUseDB() {
+               global $wgContentHandlerUseDB;
+
+               if ( $this->holdContentHandlerUseDB ) {
+                       $this->output( "Content Handler DB fields should be usable now.\n" );
+                       $wgContentHandlerUseDB = $this->holdContentHandlerUseDB;
+               }
        }
+
+       /**
+        * Migrate comments to the new 'comment' table
+        * @since 1.30
+        */
+       protected function migrateComments() {
+               global $wgCommentTableSchemaMigrationStage;
+               if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
+                       !$this->updateRowExists( 'MigrateComments' )
+               ) {
+                       $this->output(
+                               "Migrating comments to the 'comments' table, printing progress markers. For large\n" .
+                               "databases, you may want to hit Ctrl-C and do this manually with\n" .
+                               "maintenance/migrateComments.php.\n"
+                       );
+                       $task = $this->maintenance->runChild( 'MigrateComments', 'migrateComments.php' );
+                       $task->execute();
+                       $this->output( "done.\n" );
+               }
+       }
+
 }