]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/installer/DatabaseUpdater.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / installer / DatabaseUpdater.php
1 <?php
2 /**
3  * DBMS-specific updater helper.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Deployment
22  */
23 use Wikimedia\Rdbms\Database;
24 use Wikimedia\Rdbms\IDatabase;
25 use MediaWiki\MediaWikiServices;
26
27 require_once __DIR__ . '/../../maintenance/Maintenance.php';
28
29 /**
30  * Class for handling database updates. Roughly based off of updaters.inc, with
31  * a few improvements :)
32  *
33  * @ingroup Deployment
34  * @since 1.17
35  */
36 abstract class DatabaseUpdater {
37         /**
38          * Array of updates to perform on the database
39          *
40          * @var array
41          */
42         protected $updates = [];
43
44         /**
45          * Array of updates that were skipped
46          *
47          * @var array
48          */
49         protected $updatesSkipped = [];
50
51         /**
52          * List of extension-provided database updates
53          * @var array
54          */
55         protected $extensionUpdates = [];
56
57         /**
58          * Handle to the database subclass
59          *
60          * @var Database
61          */
62         protected $db;
63
64         /**
65          * @var Maintenance
66          */
67         protected $maintenance;
68
69         protected $shared = false;
70
71         /**
72          * @var string[] Scripts to run after database update
73          * Should be a subclass of LoggedUpdateMaintenance
74          */
75         protected $postDatabaseUpdateMaintenance = [
76                 DeleteDefaultMessages::class,
77                 PopulateRevisionLength::class,
78                 PopulateRevisionSha1::class,
79                 PopulateImageSha1::class,
80                 FixExtLinksProtocolRelative::class,
81                 PopulateFilearchiveSha1::class,
82                 PopulateBacklinkNamespace::class,
83                 FixDefaultJsonContentPages::class,
84                 CleanupEmptyCategories::class,
85                 AddRFCAndPMIDInterwiki::class,
86                 PopulatePPSortKey::class,
87                 PopulateIpChanges::class,
88         ];
89
90         /**
91          * File handle for SQL output.
92          *
93          * @var resource
94          */
95         protected $fileHandle = null;
96
97         /**
98          * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
99          *
100          * @var bool
101          */
102         protected $skipSchema = false;
103
104         /**
105          * Hold the value of $wgContentHandlerUseDB during the upgrade.
106          */
107         protected $holdContentHandlerUseDB = true;
108
109         /**
110          * @param Database &$db To perform updates on
111          * @param bool $shared Whether to perform updates on shared tables
112          * @param Maintenance $maintenance Maintenance object which created us
113          */
114         protected function __construct( Database &$db, $shared, Maintenance $maintenance = null ) {
115                 $this->db = $db;
116                 $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
117                 $this->shared = $shared;
118                 if ( $maintenance ) {
119                         $this->maintenance = $maintenance;
120                         $this->fileHandle = $maintenance->fileHandle;
121                 } else {
122                         $this->maintenance = new FakeMaintenance;
123                 }
124                 $this->maintenance->setDB( $db );
125                 $this->initOldGlobals();
126                 $this->loadExtensions();
127                 Hooks::run( 'LoadExtensionSchemaUpdates', [ $this ] );
128         }
129
130         /**
131          * Initialize all of the old globals. One day this should all become
132          * something much nicer
133          */
134         private function initOldGlobals() {
135                 global $wgExtNewTables, $wgExtNewFields, $wgExtPGNewFields,
136                         $wgExtPGAlteredFields, $wgExtNewIndexes, $wgExtModifiedFields;
137
138                 # For extensions only, should be populated via hooks
139                 # $wgDBtype should be checked to specifiy the proper file
140                 $wgExtNewTables = []; // table, dir
141                 $wgExtNewFields = []; // table, column, dir
142                 $wgExtPGNewFields = []; // table, column, column attributes; for PostgreSQL
143                 $wgExtPGAlteredFields = []; // table, column, new type, conversion method; for PostgreSQL
144                 $wgExtNewIndexes = []; // table, index, dir
145                 $wgExtModifiedFields = []; // table, index, dir
146         }
147
148         /**
149          * Loads LocalSettings.php, if needed, and initialises everything needed for
150          * LoadExtensionSchemaUpdates hook.
151          */
152         private function loadExtensions() {
153                 if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
154                         return; // already loaded
155                 }
156                 $vars = Installer::getExistingLocalSettings();
157
158                 $registry = ExtensionRegistry::getInstance();
159                 $queue = $registry->getQueue();
160                 // Don't accidentally load extensions in the future
161                 $registry->clearQueue();
162
163                 // This will automatically add "AutoloadClasses" to $wgAutoloadClasses
164                 $data = $registry->readFromQueue( $queue );
165                 $hooks = [ 'wgHooks' => [ 'LoadExtensionSchemaUpdates' => [] ] ];
166                 if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
167                         $hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'];
168                 }
169                 if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
170                         $hooks = array_merge_recursive( $hooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
171                 }
172                 global $wgHooks, $wgAutoloadClasses;
173                 $wgHooks['LoadExtensionSchemaUpdates'] = $hooks;
174                 if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
175                         $wgAutoloadClasses += $vars['wgAutoloadClasses'];
176                 }
177         }
178
179         /**
180          * @param Database $db
181          * @param bool $shared
182          * @param Maintenance $maintenance
183          *
184          * @throws MWException
185          * @return DatabaseUpdater
186          */
187         public static function newForDB( Database $db, $shared = false, $maintenance = null ) {
188                 $type = $db->getType();
189                 if ( in_array( $type, Installer::getDBTypes() ) ) {
190                         $class = ucfirst( $type ) . 'Updater';
191
192                         return new $class( $db, $shared, $maintenance );
193                 } else {
194                         throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
195                 }
196         }
197
198         /**
199          * Get a database connection to run updates
200          *
201          * @return Database
202          */
203         public function getDB() {
204                 return $this->db;
205         }
206
207         /**
208          * Output some text. If we're running from web, escape the text first.
209          *
210          * @param string $str Text to output
211          */
212         public function output( $str ) {
213                 if ( $this->maintenance->isQuiet() ) {
214                         return;
215                 }
216                 global $wgCommandLineMode;
217                 if ( !$wgCommandLineMode ) {
218                         $str = htmlspecialchars( $str );
219                 }
220                 echo $str;
221                 flush();
222         }
223
224         /**
225          * Add a new update coming from an extension. This should be called by
226          * extensions while executing the LoadExtensionSchemaUpdates hook.
227          *
228          * @since 1.17
229          *
230          * @param array $update The update to run. Format is [ $callback, $params... ]
231          *   $callback is the method to call; either a DatabaseUpdater method name or a callable.
232          *   Must be serializable (ie. no anonymous functions allowed). The rest of the parameters
233          *   (if any) will be passed to the callback. The first parameter passed to the callback
234          *   is always this object.
235          */
236         public function addExtensionUpdate( array $update ) {
237                 $this->extensionUpdates[] = $update;
238         }
239
240         /**
241          * Convenience wrapper for addExtensionUpdate() when adding a new table (which
242          * is the most common usage of updaters in an extension)
243          *
244          * @since 1.18
245          *
246          * @param string $tableName Name of table to create
247          * @param string $sqlPath Full path to the schema file
248          */
249         public function addExtensionTable( $tableName, $sqlPath ) {
250                 $this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
251         }
252
253         /**
254          * @since 1.19
255          *
256          * @param string $tableName
257          * @param string $indexName
258          * @param string $sqlPath
259          */
260         public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
261                 $this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
262         }
263
264         /**
265          *
266          * @since 1.19
267          *
268          * @param string $tableName
269          * @param string $columnName
270          * @param string $sqlPath
271          */
272         public function addExtensionField( $tableName, $columnName, $sqlPath ) {
273                 $this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
274         }
275
276         /**
277          *
278          * @since 1.20
279          *
280          * @param string $tableName
281          * @param string $columnName
282          * @param string $sqlPath
283          */
284         public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
285                 $this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
286         }
287
288         /**
289          * Drop an index from an extension table
290          *
291          * @since 1.21
292          *
293          * @param string $tableName The table name
294          * @param string $indexName The index name
295          * @param string $sqlPath The path to the SQL change path
296          */
297         public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
298                 $this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
299         }
300
301         /**
302          *
303          * @since 1.20
304          *
305          * @param string $tableName
306          * @param string $sqlPath
307          */
308         public function dropExtensionTable( $tableName, $sqlPath ) {
309                 $this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
310         }
311
312         /**
313          * Rename an index on an extension table
314          *
315          * @since 1.21
316          *
317          * @param string $tableName The table name
318          * @param string $oldIndexName The old index name
319          * @param string $newIndexName The new index name
320          * @param string $sqlPath The path to the SQL change path
321          * @param bool $skipBothIndexExistWarning Whether to warn if both the old
322          * and the new indexes exist. [facultative; by default, false]
323          */
324         public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
325                 $sqlPath, $skipBothIndexExistWarning = false
326         ) {
327                 $this->extensionUpdates[] = [
328                         'renameIndex',
329                         $tableName,
330                         $oldIndexName,
331                         $newIndexName,
332                         $skipBothIndexExistWarning,
333                         $sqlPath,
334                         true
335                 ];
336         }
337
338         /**
339          * @since 1.21
340          *
341          * @param string $tableName The table name
342          * @param string $fieldName The field to be modified
343          * @param string $sqlPath The path to the SQL change path
344          */
345         public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
346                 $this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
347         }
348
349         /**
350          *
351          * @since 1.20
352          *
353          * @param string $tableName
354          * @return bool
355          */
356         public function tableExists( $tableName ) {
357                 return ( $this->db->tableExists( $tableName, __METHOD__ ) );
358         }
359
360         /**
361          * Add a maintenance script to be run after the database updates are complete.
362          *
363          * Script should subclass LoggedUpdateMaintenance
364          *
365          * @since 1.19
366          *
367          * @param string $class Name of a Maintenance subclass
368          */
369         public function addPostDatabaseUpdateMaintenance( $class ) {
370                 $this->postDatabaseUpdateMaintenance[] = $class;
371         }
372
373         /**
374          * Get the list of extension-defined updates
375          *
376          * @return array
377          */
378         protected function getExtensionUpdates() {
379                 return $this->extensionUpdates;
380         }
381
382         /**
383          * @since 1.17
384          *
385          * @return string[]
386          */
387         public function getPostDatabaseUpdateMaintenance() {
388                 return $this->postDatabaseUpdateMaintenance;
389         }
390
391         /**
392          * @since 1.21
393          *
394          * Writes the schema updates desired to a file for the DB Admin to run.
395          * @param array $schemaUpdate
396          */
397         private function writeSchemaUpdateFile( array $schemaUpdate = [] ) {
398                 $updates = $this->updatesSkipped;
399                 $this->updatesSkipped = [];
400
401                 foreach ( $updates as $funcList ) {
402                         $func = $funcList[0];
403                         $arg = $funcList[1];
404                         $origParams = $funcList[2];
405                         call_user_func_array( $func, $arg );
406                         flush();
407                         $this->updatesSkipped[] = $origParams;
408                 }
409         }
410
411         /**
412          * Get appropriate schema variables in the current database connection.
413          *
414          * This should be called after any request data has been imported, but before
415          * any write operations to the database. The result should be passed to the DB
416          * setSchemaVars() method.
417          *
418          * @return array
419          * @since 1.28
420          */
421         public function getSchemaVars() {
422                 return []; // DB-type specific
423         }
424
425         /**
426          * Do all the updates
427          *
428          * @param array $what What updates to perform
429          */
430         public function doUpdates( array $what = [ 'core', 'extensions', 'stats' ] ) {
431                 $this->db->setSchemaVars( $this->getSchemaVars() );
432
433                 $what = array_flip( $what );
434                 $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
435                 if ( isset( $what['core'] ) ) {
436                         $this->runUpdates( $this->getCoreUpdateList(), false );
437                 }
438                 if ( isset( $what['extensions'] ) ) {
439                         $this->runUpdates( $this->getOldGlobalUpdates(), false );
440                         $this->runUpdates( $this->getExtensionUpdates(), true );
441                 }
442
443                 if ( isset( $what['stats'] ) ) {
444                         $this->checkStats();
445                 }
446
447                 if ( $this->fileHandle ) {
448                         $this->skipSchema = false;
449                         $this->writeSchemaUpdateFile();
450                 }
451         }
452
453         /**
454          * Helper function for doUpdates()
455          *
456          * @param array $updates Array of updates to run
457          * @param bool $passSelf Whether to pass this object we calling external functions
458          */
459         private function runUpdates( array $updates, $passSelf ) {
460                 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
461
462                 $updatesDone = [];
463                 $updatesSkipped = [];
464                 foreach ( $updates as $params ) {
465                         $origParams = $params;
466                         $func = array_shift( $params );
467                         if ( !is_array( $func ) && method_exists( $this, $func ) ) {
468                                 $func = [ $this, $func ];
469                         } elseif ( $passSelf ) {
470                                 array_unshift( $params, $this );
471                         }
472                         $ret = call_user_func_array( $func, $params );
473                         flush();
474                         if ( $ret !== false ) {
475                                 $updatesDone[] = $origParams;
476                                 $lbFactory->waitForReplication();
477                         } else {
478                                 $updatesSkipped[] = [ $func, $params, $origParams ];
479                         }
480                 }
481                 $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
482                 $this->updates = array_merge( $this->updates, $updatesDone );
483         }
484
485         /**
486          * Helper function: check if the given key is present in the updatelog table.
487          * Obviously, only use this for updates that occur after the updatelog table was
488          * created!
489          * @param string $key Name of the key to check for
490          * @return bool
491          */
492         public function updateRowExists( $key ) {
493                 $row = $this->db->selectRow(
494                         'updatelog',
495                         # T67813
496                         '1 AS X',
497                         [ 'ul_key' => $key ],
498                         __METHOD__
499                 );
500
501                 return (bool)$row;
502         }
503
504         /**
505          * Helper function: Add a key to the updatelog table
506          * Obviously, only use this for updates that occur after the updatelog table was
507          * created!
508          * @param string $key Name of key to insert
509          * @param string $val [optional] Value to insert along with the key
510          */
511         public function insertUpdateRow( $key, $val = null ) {
512                 $this->db->clearFlag( DBO_DDLMODE );
513                 $values = [ 'ul_key' => $key ];
514                 if ( $val && $this->canUseNewUpdatelog() ) {
515                         $values['ul_value'] = $val;
516                 }
517                 $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
518                 $this->db->setFlag( DBO_DDLMODE );
519         }
520
521         /**
522          * Updatelog was changed in 1.17 to have a ul_value column so we can record
523          * more information about what kind of updates we've done (that's what this
524          * class does). Pre-1.17 wikis won't have this column, and really old wikis
525          * might not even have updatelog at all
526          *
527          * @return bool
528          */
529         protected function canUseNewUpdatelog() {
530                 return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
531                         $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
532         }
533
534         /**
535          * Returns whether updates should be executed on the database table $name.
536          * Updates will be prevented if the table is a shared table and it is not
537          * specified to run updates on shared tables.
538          *
539          * @param string $name Table name
540          * @return bool
541          */
542         protected function doTable( $name ) {
543                 global $wgSharedDB, $wgSharedTables;
544
545                 // Don't bother to check $wgSharedTables if there isn't a shared database
546                 // or the user actually also wants to do updates on the shared database.
547                 if ( $wgSharedDB === null || $this->shared ) {
548                         return true;
549                 }
550
551                 if ( in_array( $name, $wgSharedTables ) ) {
552                         $this->output( "...skipping update to shared table $name.\n" );
553                         return false;
554                 } else {
555                         return true;
556                 }
557         }
558
559         /**
560          * Before 1.17, we used to handle updates via stuff like
561          * $wgExtNewTables/Fields/Indexes. This is nasty :) We refactored a lot
562          * of this in 1.17 but we want to remain back-compatible for a while. So
563          * load up these old global-based things into our update list.
564          *
565          * @return array
566          */
567         protected function getOldGlobalUpdates() {
568                 global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
569                         $wgExtNewIndexes;
570
571                 $updates = [];
572
573                 foreach ( $wgExtNewTables as $tableRecord ) {
574                         $updates[] = [
575                                 'addTable', $tableRecord[0], $tableRecord[1], true
576                         ];
577                 }
578
579                 foreach ( $wgExtNewFields as $fieldRecord ) {
580                         $updates[] = [
581                                 'addField', $fieldRecord[0], $fieldRecord[1],
582                                 $fieldRecord[2], true
583                         ];
584                 }
585
586                 foreach ( $wgExtNewIndexes as $fieldRecord ) {
587                         $updates[] = [
588                                 'addIndex', $fieldRecord[0], $fieldRecord[1],
589                                 $fieldRecord[2], true
590                         ];
591                 }
592
593                 foreach ( $wgExtModifiedFields as $fieldRecord ) {
594                         $updates[] = [
595                                 'modifyField', $fieldRecord[0], $fieldRecord[1],
596                                 $fieldRecord[2], true
597                         ];
598                 }
599
600                 return $updates;
601         }
602
603         /**
604          * Get an array of updates to perform on the database. Should return a
605          * multi-dimensional array. The main key is the MediaWiki version (1.12,
606          * 1.13...) with the values being arrays of updates, identical to how
607          * updaters.inc did it (for now)
608          *
609          * @return array
610          */
611         abstract protected function getCoreUpdateList();
612
613         /**
614          * Append an SQL fragment to the open file handle.
615          *
616          * @param string $filename File name to open
617          */
618         public function copyFile( $filename ) {
619                 $this->db->sourceFile(
620                         $filename,
621                         null,
622                         null,
623                         __METHOD__,
624                         [ $this, 'appendLine' ]
625                 );
626         }
627
628         /**
629          * Append a line to the open filehandle.  The line is assumed to
630          * be a complete SQL statement.
631          *
632          * This is used as a callback for sourceLine().
633          *
634          * @param string $line Text to append to the file
635          * @return bool False to skip actually executing the file
636          * @throws MWException
637          */
638         public function appendLine( $line ) {
639                 $line = rtrim( $line ) . ";\n";
640                 if ( fwrite( $this->fileHandle, $line ) === false ) {
641                         throw new MWException( "trouble writing file" );
642                 }
643
644                 return false;
645         }
646
647         /**
648          * Applies a SQL patch
649          *
650          * @param string $path Path to the patch file
651          * @param bool $isFullPath Whether to treat $path as a relative or not
652          * @param string $msg Description of the patch
653          * @return bool False if patch is skipped.
654          */
655         protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
656                 if ( $msg === null ) {
657                         $msg = "Applying $path patch";
658                 }
659                 if ( $this->skipSchema ) {
660                         $this->output( "...skipping schema change ($msg).\n" );
661
662                         return false;
663                 }
664
665                 $this->output( "$msg ..." );
666
667                 if ( !$isFullPath ) {
668                         $path = $this->patchPath( $this->db, $path );
669                 }
670                 if ( $this->fileHandle !== null ) {
671                         $this->copyFile( $path );
672                 } else {
673                         $this->db->sourceFile( $path );
674                 }
675                 $this->output( "done.\n" );
676
677                 return true;
678         }
679
680         /**
681          * Get the full path of a patch file. Originally based on archive()
682          * from updaters.inc. Keep in mind this always returns a patch, as
683          * it fails back to MySQL if no DB-specific patch can be found
684          *
685          * @param IDatabase $db
686          * @param string $patch The name of the patch, like patch-something.sql
687          * @return string Full path to patch file
688          */
689         public function patchPath( IDatabase $db, $patch ) {
690                 global $IP;
691
692                 $dbType = $db->getType();
693                 if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
694                         return "$IP/maintenance/$dbType/archives/$patch";
695                 } else {
696                         return "$IP/maintenance/archives/$patch";
697                 }
698         }
699
700         /**
701          * Add a new table to the database
702          *
703          * @param string $name Name of the new table
704          * @param string $patch Path to the patch file
705          * @param bool $fullpath Whether to treat $patch path as a relative or not
706          * @return bool False if this was skipped because schema changes are skipped
707          */
708         protected function addTable( $name, $patch, $fullpath = false ) {
709                 if ( !$this->doTable( $name ) ) {
710                         return true;
711                 }
712
713                 if ( $this->db->tableExists( $name, __METHOD__ ) ) {
714                         $this->output( "...$name table already exists.\n" );
715                 } else {
716                         return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
717                 }
718
719                 return true;
720         }
721
722         /**
723          * Add a new field to an existing table
724          *
725          * @param string $table Name of the table to modify
726          * @param string $field Name of the new field
727          * @param string $patch Path to the patch file
728          * @param bool $fullpath Whether to treat $patch path as a relative or not
729          * @return bool False if this was skipped because schema changes are skipped
730          */
731         protected function addField( $table, $field, $patch, $fullpath = false ) {
732                 if ( !$this->doTable( $table ) ) {
733                         return true;
734                 }
735
736                 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
737                         $this->output( "...$table table does not exist, skipping new field patch.\n" );
738                 } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
739                         $this->output( "...have $field field in $table table.\n" );
740                 } else {
741                         return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
742                 }
743
744                 return true;
745         }
746
747         /**
748          * Add a new index to an existing table
749          *
750          * @param string $table Name of the table to modify
751          * @param string $index Name of the new index
752          * @param string $patch Path to the patch file
753          * @param bool $fullpath Whether to treat $patch path as a relative or not
754          * @return bool False if this was skipped because schema changes are skipped
755          */
756         protected function addIndex( $table, $index, $patch, $fullpath = false ) {
757                 if ( !$this->doTable( $table ) ) {
758                         return true;
759                 }
760
761                 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
762                         $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
763                 } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
764                         $this->output( "...index $index already set on $table table.\n" );
765                 } else {
766                         return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
767                 }
768
769                 return true;
770         }
771
772         /**
773          * Drop a field from an existing table
774          *
775          * @param string $table Name of the table to modify
776          * @param string $field Name of the old field
777          * @param string $patch Path to the patch file
778          * @param bool $fullpath Whether to treat $patch path as a relative or not
779          * @return bool False if this was skipped because schema changes are skipped
780          */
781         protected function dropField( $table, $field, $patch, $fullpath = false ) {
782                 if ( !$this->doTable( $table ) ) {
783                         return true;
784                 }
785
786                 if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
787                         return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
788                 } else {
789                         $this->output( "...$table table does not contain $field field.\n" );
790                 }
791
792                 return true;
793         }
794
795         /**
796          * Drop an index from an existing table
797          *
798          * @param string $table Name of the table to modify
799          * @param string $index Name of the index
800          * @param string $patch Path to the patch file
801          * @param bool $fullpath Whether to treat $patch path as a relative or not
802          * @return bool False if this was skipped because schema changes are skipped
803          */
804         protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
805                 if ( !$this->doTable( $table ) ) {
806                         return true;
807                 }
808
809                 if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
810                         return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
811                 } else {
812                         $this->output( "...$index key doesn't exist.\n" );
813                 }
814
815                 return true;
816         }
817
818         /**
819          * Rename an index from an existing table
820          *
821          * @param string $table Name of the table to modify
822          * @param string $oldIndex Old name of the index
823          * @param string $newIndex New name of the index
824          * @param bool $skipBothIndexExistWarning Whether to warn if both the
825          * old and the new indexes exist.
826          * @param string $patch Path to the patch file
827          * @param bool $fullpath Whether to treat $patch path as a relative or not
828          * @return bool False if this was skipped because schema changes are skipped
829          */
830         protected function renameIndex( $table, $oldIndex, $newIndex,
831                 $skipBothIndexExistWarning, $patch, $fullpath = false
832         ) {
833                 if ( !$this->doTable( $table ) ) {
834                         return true;
835                 }
836
837                 // First requirement: the table must exist
838                 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
839                         $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
840
841                         return true;
842                 }
843
844                 // Second requirement: the new index must be missing
845                 if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
846                         $this->output( "...index $newIndex already set on $table table.\n" );
847                         if ( !$skipBothIndexExistWarning &&
848                                 $this->db->indexExists( $table, $oldIndex, __METHOD__ )
849                         ) {
850                                 $this->output( "...WARNING: $oldIndex still exists, despite it has " .
851                                         "been renamed into $newIndex (which also exists).\n" .
852                                         "            $oldIndex should be manually removed if not needed anymore.\n" );
853                         }
854
855                         return true;
856                 }
857
858                 // Third requirement: the old index must exist
859                 if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
860                         $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
861
862                         return true;
863                 }
864
865                 // Requirements have been satisfied, patch can be applied
866                 return $this->applyPatch(
867                         $patch,
868                         $fullpath,
869                         "Renaming index $oldIndex into $newIndex to table $table"
870                 );
871         }
872
873         /**
874          * If the specified table exists, drop it, or execute the
875          * patch if one is provided.
876          *
877          * Public @since 1.20
878          *
879          * @param string $table Table to drop.
880          * @param string|bool $patch String of patch file that will drop the table. Default: false.
881          * @param bool $fullpath Whether $patch is a full path. Default: false.
882          * @return bool False if this was skipped because schema changes are skipped
883          */
884         public function dropTable( $table, $patch = false, $fullpath = false ) {
885                 if ( !$this->doTable( $table ) ) {
886                         return true;
887                 }
888
889                 if ( $this->db->tableExists( $table, __METHOD__ ) ) {
890                         $msg = "Dropping table $table";
891
892                         if ( $patch === false ) {
893                                 $this->output( "$msg ..." );
894                                 $this->db->dropTable( $table, __METHOD__ );
895                                 $this->output( "done.\n" );
896                         } else {
897                                 return $this->applyPatch( $patch, $fullpath, $msg );
898                         }
899                 } else {
900                         $this->output( "...$table doesn't exist.\n" );
901                 }
902
903                 return true;
904         }
905
906         /**
907          * Modify an existing field
908          *
909          * @param string $table Name of the table to which the field belongs
910          * @param string $field Name of the field to modify
911          * @param string $patch Path to the patch file
912          * @param bool $fullpath Whether to treat $patch path as a relative or not
913          * @return bool False if this was skipped because schema changes are skipped
914          */
915         public function modifyField( $table, $field, $patch, $fullpath = false ) {
916                 if ( !$this->doTable( $table ) ) {
917                         return true;
918                 }
919
920                 $updateKey = "$table-$field-$patch";
921                 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
922                         $this->output( "...$table table does not exist, skipping modify field patch.\n" );
923                 } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
924                         $this->output( "...$field field does not exist in $table table, " .
925                                 "skipping modify field patch.\n" );
926                 } elseif ( $this->updateRowExists( $updateKey ) ) {
927                         $this->output( "...$field in table $table already modified by patch $patch.\n" );
928                 } else {
929                         $apply = $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
930                         if ( $apply ) {
931                                 $this->insertUpdateRow( $updateKey );
932                         }
933                         return $apply;
934                 }
935                 return true;
936         }
937
938         /**
939          * Modify an existing table, similar to modifyField. Intended for changes that
940          *  touch more than one column on a table.
941          *
942          * @param string $table Name of the table to modify
943          * @param string $patch Name of the patch file to apply
944          * @param string|bool $fullpath Whether to treat $patch path as relative or not, defaults to false
945          * @return bool False if this was skipped because of schema changes being skipped
946          */
947         public function modifyTable( $table, $patch, $fullpath = false ) {
948                 if ( !$this->doTable( $table ) ) {
949                         return true;
950                 }
951
952                 $updateKey = "$table-$patch";
953                 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
954                         $this->output( "...$table table does not exist, skipping modify table patch.\n" );
955                 } elseif ( $this->updateRowExists( $updateKey ) ) {
956                         $this->output( "...table $table already modified by patch $patch.\n" );
957                 } else {
958                         $apply = $this->applyPatch( $patch, $fullpath, "Modifying table $table" );
959                         if ( $apply ) {
960                                 $this->insertUpdateRow( $updateKey );
961                         }
962                         return $apply;
963                 }
964                 return true;
965         }
966
967         /**
968          * Set any .htaccess files or equivilent for storage repos
969          *
970          * Some zones (e.g. "temp") used to be public and may have been initialized as such
971          */
972         public function setFileAccess() {
973                 $repo = RepoGroup::singleton()->getLocalRepo();
974                 $zonePath = $repo->getZonePath( 'temp' );
975                 if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
976                         // If the directory was never made, then it will have the right ACLs when it is made
977                         $status = $repo->getBackend()->secure( [
978                                 'dir' => $zonePath,
979                                 'noAccess' => true,
980                                 'noListing' => true
981                         ] );
982                         if ( $status->isOK() ) {
983                                 $this->output( "Set the local repo temp zone container to be private.\n" );
984                         } else {
985                                 $this->output( "Failed to set the local repo temp zone container to be private.\n" );
986                         }
987                 }
988         }
989
990         /**
991          * Purge the objectcache table
992          */
993         public function purgeCache() {
994                 global $wgLocalisationCacheConf;
995                 # We can't guarantee that the user will be able to use TRUNCATE,
996                 # but we know that DELETE is available to us
997                 $this->output( "Purging caches..." );
998                 $this->db->delete( 'objectcache', '*', __METHOD__ );
999                 if ( $wgLocalisationCacheConf['manualRecache'] ) {
1000                         $this->rebuildLocalisationCache();
1001                 }
1002                 $blobStore = new MessageBlobStore();
1003                 $blobStore->clear();
1004                 $this->db->delete( 'module_deps', '*', __METHOD__ );
1005                 $this->output( "done.\n" );
1006         }
1007
1008         /**
1009          * Check the site_stats table is not properly populated.
1010          */
1011         protected function checkStats() {
1012                 $this->output( "...site_stats is populated..." );
1013                 $row = $this->db->selectRow( 'site_stats', '*', [ 'ss_row_id' => 1 ], __METHOD__ );
1014                 if ( $row === false ) {
1015                         $this->output( "data is missing! rebuilding...\n" );
1016                 } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1017                         $this->output( "missing ss_total_pages, rebuilding...\n" );
1018                 } else {
1019                         $this->output( "done.\n" );
1020
1021                         return;
1022                 }
1023                 SiteStatsInit::doAllAndCommit( $this->db );
1024         }
1025
1026         # Common updater functions
1027
1028         /**
1029          * Sets the number of active users in the site_stats table
1030          */
1031         protected function doActiveUsersInit() {
1032                 $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
1033                 if ( $activeUsers == -1 ) {
1034                         $activeUsers = $this->db->selectField( 'recentchanges',
1035                                 'COUNT( DISTINCT rc_user_text )',
1036                                 [ 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ], __METHOD__
1037                         );
1038                         $this->db->update( 'site_stats',
1039                                 [ 'ss_active_users' => intval( $activeUsers ) ],
1040                                 [ 'ss_row_id' => 1 ], __METHOD__, [ 'LIMIT' => 1 ]
1041                         );
1042                 }
1043                 $this->output( "...ss_active_users user count set...\n" );
1044         }
1045
1046         /**
1047          * Populates the log_user_text field in the logging table
1048          */
1049         protected function doLogUsertextPopulation() {
1050                 if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
1051                         $this->output(
1052                                 "Populating log_user_text field, printing progress markers. For large\n" .
1053                                 "databases, you may want to hit Ctrl-C and do this manually with\n" .
1054                                 "maintenance/populateLogUsertext.php.\n"
1055                         );
1056
1057                         $task = $this->maintenance->runChild( 'PopulateLogUsertext' );
1058                         $task->execute();
1059                         $this->output( "done.\n" );
1060                 }
1061         }
1062
1063         /**
1064          * Migrate log params to new table and index for searching
1065          */
1066         protected function doLogSearchPopulation() {
1067                 if ( !$this->updateRowExists( 'populate log_search' ) ) {
1068                         $this->output(
1069                                 "Populating log_search table, printing progress markers. For large\n" .
1070                                 "databases, you may want to hit Ctrl-C and do this manually with\n" .
1071                                 "maintenance/populateLogSearch.php.\n" );
1072
1073                         $task = $this->maintenance->runChild( 'PopulateLogSearch' );
1074                         $task->execute();
1075                         $this->output( "done.\n" );
1076                 }
1077         }
1078
1079         /**
1080          * Updates the timestamps in the transcache table
1081          * @return bool
1082          */
1083         protected function doUpdateTranscacheField() {
1084                 if ( $this->updateRowExists( 'convert transcache field' ) ) {
1085                         $this->output( "...transcache tc_time already converted.\n" );
1086
1087                         return true;
1088                 }
1089
1090                 return $this->applyPatch( 'patch-tc-timestamp.sql', false,
1091                         "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
1092         }
1093
1094         /**
1095          * Update CategoryLinks collation
1096          */
1097         protected function doCollationUpdate() {
1098                 global $wgCategoryCollation;
1099                 if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
1100                         if ( $this->db->selectField(
1101                                 'categorylinks',
1102                                 'COUNT(*)',
1103                                 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
1104                                 __METHOD__
1105                                 ) == 0
1106                         ) {
1107                                 $this->output( "...collations up-to-date.\n" );
1108
1109                                 return;
1110                         }
1111
1112                         $this->output( "Updating category collations..." );
1113                         $task = $this->maintenance->runChild( 'UpdateCollation' );
1114                         $task->execute();
1115                         $this->output( "...done.\n" );
1116                 }
1117         }
1118
1119         /**
1120          * Migrates user options from the user table blob to user_properties
1121          */
1122         protected function doMigrateUserOptions() {
1123                 if ( $this->db->tableExists( 'user_properties' ) ) {
1124                         $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
1125                         $cl->execute();
1126                         $this->output( "done.\n" );
1127                 }
1128         }
1129
1130         /**
1131          * Enable profiling table when it's turned on
1132          */
1133         protected function doEnableProfiling() {
1134                 global $wgProfiler;
1135
1136                 if ( !$this->doTable( 'profiling' ) ) {
1137                         return;
1138                 }
1139
1140                 $profileToDb = false;
1141                 if ( isset( $wgProfiler['output'] ) ) {
1142                         $out = $wgProfiler['output'];
1143                         if ( $out === 'db' ) {
1144                                 $profileToDb = true;
1145                         } elseif ( is_array( $out ) && in_array( 'db', $out ) ) {
1146                                 $profileToDb = true;
1147                         }
1148                 }
1149
1150                 if ( $profileToDb && !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
1151                         $this->applyPatch( 'patch-profiling.sql', false, 'Add profiling table' );
1152                 }
1153         }
1154
1155         /**
1156          * Rebuilds the localisation cache
1157          */
1158         protected function rebuildLocalisationCache() {
1159                 /**
1160                  * @var $cl RebuildLocalisationCache
1161                  */
1162                 $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
1163                 $this->output( "Rebuilding localisation cache...\n" );
1164                 $cl->setForce();
1165                 $cl->execute();
1166                 $this->output( "done.\n" );
1167         }
1168
1169         /**
1170          * Turns off content handler fields during parts of the upgrade
1171          * where they aren't available.
1172          */
1173         protected function disableContentHandlerUseDB() {
1174                 global $wgContentHandlerUseDB;
1175
1176                 if ( $wgContentHandlerUseDB ) {
1177                         $this->output( "Turning off Content Handler DB fields for this part of upgrade.\n" );
1178                         $this->holdContentHandlerUseDB = $wgContentHandlerUseDB;
1179                         $wgContentHandlerUseDB = false;
1180                 }
1181         }
1182
1183         /**
1184          * Turns content handler fields back on.
1185          */
1186         protected function enableContentHandlerUseDB() {
1187                 global $wgContentHandlerUseDB;
1188
1189                 if ( $this->holdContentHandlerUseDB ) {
1190                         $this->output( "Content Handler DB fields should be usable now.\n" );
1191                         $wgContentHandlerUseDB = $this->holdContentHandlerUseDB;
1192                 }
1193         }
1194
1195         /**
1196          * Migrate comments to the new 'comment' table
1197          * @since 1.30
1198          */
1199         protected function migrateComments() {
1200                 global $wgCommentTableSchemaMigrationStage;
1201                 if ( $wgCommentTableSchemaMigrationStage >= MIGRATION_WRITE_NEW &&
1202                         !$this->updateRowExists( 'MigrateComments' )
1203                 ) {
1204                         $this->output(
1205                                 "Migrating comments to the 'comments' table, printing progress markers. For large\n" .
1206                                 "databases, you may want to hit Ctrl-C and do this manually with\n" .
1207                                 "maintenance/migrateComments.php.\n"
1208                         );
1209                         $task = $this->maintenance->runChild( 'MigrateComments', 'migrateComments.php' );
1210                         $task->execute();
1211                         $this->output( "done.\n" );
1212                 }
1213         }
1214
1215 }