Completed
Branch master (939199)
by
unknown
39:35
created

includes/installer/DatabaseUpdater.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 MediaWiki\MediaWikiServices;
24
25
require_once __DIR__ . '/../../maintenance/Maintenance.php';
26
27
/**
28
 * Class for handling database updates. Roughly based off of updaters.inc, with
29
 * a few improvements :)
30
 *
31
 * @ingroup Deployment
32
 * @since 1.17
33
 */
34
abstract class DatabaseUpdater {
35
	protected static $updateCounter = 0;
36
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
	protected $shared = false;
65
66
	/**
67
	 * @var string[] Scripts to run after database update
68
	 * Should be a subclass of LoggedUpdateMaintenance
69
	 */
70
	protected $postDatabaseUpdateMaintenance = [
71
		DeleteDefaultMessages::class,
72
		PopulateRevisionLength::class,
73
		PopulateRevisionSha1::class,
74
		PopulateImageSha1::class,
75
		FixExtLinksProtocolRelative::class,
76
		PopulateFilearchiveSha1::class,
77
		PopulateBacklinkNamespace::class,
78
		FixDefaultJsonContentPages::class,
79
		CleanupEmptyCategories::class,
80
		AddRFCAndPMIDInterwiki::class,
81
	];
82
83
	/**
84
	 * File handle for SQL output.
85
	 *
86
	 * @var resource
87
	 */
88
	protected $fileHandle = null;
89
90
	/**
91
	 * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
92
	 *
93
	 * @var bool
94
	 */
95
	protected $skipSchema = false;
96
97
	/**
98
	 * Hold the value of $wgContentHandlerUseDB during the upgrade.
99
	 */
100
	protected $holdContentHandlerUseDB = true;
101
102
	/**
103
	 * Constructor
104
	 *
105
	 * @param Database $db To perform updates on
106
	 * @param bool $shared Whether to perform updates on shared tables
107
	 * @param Maintenance $maintenance Maintenance object which created us
108
	 */
109
	protected function __construct( Database &$db, $shared, Maintenance $maintenance = null ) {
110
		$this->db = $db;
111
		$this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
112
		$this->shared = $shared;
113
		if ( $maintenance ) {
114
			$this->maintenance = $maintenance;
115
			$this->fileHandle = $maintenance->fileHandle;
116
		} else {
117
			$this->maintenance = new FakeMaintenance;
118
		}
119
		$this->maintenance->setDB( $db );
120
		$this->initOldGlobals();
121
		$this->loadExtensions();
122
		Hooks::run( 'LoadExtensionSchemaUpdates', [ $this ] );
123
	}
124
125
	/**
126
	 * Initialize all of the old globals. One day this should all become
127
	 * something much nicer
128
	 */
129
	private function initOldGlobals() {
130
		global $wgExtNewTables, $wgExtNewFields, $wgExtPGNewFields,
131
			$wgExtPGAlteredFields, $wgExtNewIndexes, $wgExtModifiedFields;
132
133
		# For extensions only, should be populated via hooks
134
		# $wgDBtype should be checked to specifiy the proper file
135
		$wgExtNewTables = []; // table, dir
136
		$wgExtNewFields = []; // table, column, dir
137
		$wgExtPGNewFields = []; // table, column, column attributes; for PostgreSQL
138
		$wgExtPGAlteredFields = []; // table, column, new type, conversion method; for PostgreSQL
139
		$wgExtNewIndexes = []; // table, index, dir
140
		$wgExtModifiedFields = []; // table, index, dir
141
	}
142
143
	/**
144
	 * Loads LocalSettings.php, if needed, and initialises everything needed for
145
	 * LoadExtensionSchemaUpdates hook.
146
	 */
147
	private function loadExtensions() {
148
		if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
149
			return; // already loaded
150
		}
151
		$vars = Installer::getExistingLocalSettings();
152
153
		$registry = ExtensionRegistry::getInstance();
154
		$queue = $registry->getQueue();
155
		// Don't accidentally load extensions in the future
156
		$registry->clearQueue();
157
158
		// This will automatically add "AutoloadClasses" to $wgAutoloadClasses
159
		$data = $registry->readFromQueue( $queue );
160
		$hooks = [ 'wgHooks' => [ 'LoadExtensionSchemaUpdates' => [] ] ];
161 View Code Duplication
		if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
162
			$hooks = $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'];
163
		}
164
		if ( $vars && isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
165
			$hooks = array_merge_recursive( $hooks, $vars['wgHooks']['LoadExtensionSchemaUpdates'] );
166
		}
167
		global $wgHooks, $wgAutoloadClasses;
168
		$wgHooks['LoadExtensionSchemaUpdates'] = $hooks;
169
		if ( $vars && isset( $vars['wgAutoloadClasses'] ) ) {
170
			$wgAutoloadClasses += $vars['wgAutoloadClasses'];
171
		}
172
	}
173
174
	/**
175
	 * @param Database $db
176
	 * @param bool $shared
177
	 * @param Maintenance $maintenance
178
	 *
179
	 * @throws MWException
180
	 * @return DatabaseUpdater
181
	 */
182
	public static function newForDB( Database $db, $shared = false, $maintenance = null ) {
183
		$type = $db->getType();
184
		if ( in_array( $type, Installer::getDBTypes() ) ) {
185
			$class = ucfirst( $type ) . 'Updater';
186
187
			return new $class( $db, $shared, $maintenance );
188
		} else {
189
			throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
190
		}
191
	}
192
193
	/**
194
	 * Get a database connection to run updates
195
	 *
196
	 * @return Database
197
	 */
198
	public function getDB() {
199
		return $this->db;
200
	}
201
202
	/**
203
	 * Output some text. If we're running from web, escape the text first.
204
	 *
205
	 * @param string $str Text to output
206
	 */
207
	public function output( $str ) {
208
		if ( $this->maintenance->isQuiet() ) {
209
			return;
210
		}
211
		global $wgCommandLineMode;
212
		if ( !$wgCommandLineMode ) {
213
			$str = htmlspecialchars( $str );
214
		}
215
		echo $str;
216
		flush();
217
	}
218
219
	/**
220
	 * Add a new update coming from an extension. This should be called by
221
	 * extensions while executing the LoadExtensionSchemaUpdates hook.
222
	 *
223
	 * @since 1.17
224
	 *
225
	 * @param array $update The update to run. Format is [ $callback, $params... ]
226
	 *   $callback is the method to call; either a DatabaseUpdater method name or a callable.
227
	 *   Must be serializable (ie. no anonymous functions allowed). The rest of the parameters
228
	 *   (if any) will be passed to the callback. The first parameter passed to the callback
229
	 *   is always this object.
230
	 */
231
	public function addExtensionUpdate( array $update ) {
232
		$this->extensionUpdates[] = $update;
233
	}
234
235
	/**
236
	 * Convenience wrapper for addExtensionUpdate() when adding a new table (which
237
	 * is the most common usage of updaters in an extension)
238
	 *
239
	 * @since 1.18
240
	 *
241
	 * @param string $tableName Name of table to create
242
	 * @param string $sqlPath Full path to the schema file
243
	 */
244
	public function addExtensionTable( $tableName, $sqlPath ) {
245
		$this->extensionUpdates[] = [ 'addTable', $tableName, $sqlPath, true ];
246
	}
247
248
	/**
249
	 * @since 1.19
250
	 *
251
	 * @param string $tableName
252
	 * @param string $indexName
253
	 * @param string $sqlPath
254
	 */
255
	public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
256
		$this->extensionUpdates[] = [ 'addIndex', $tableName, $indexName, $sqlPath, true ];
257
	}
258
259
	/**
260
	 *
261
	 * @since 1.19
262
	 *
263
	 * @param string $tableName
264
	 * @param string $columnName
265
	 * @param string $sqlPath
266
	 */
267
	public function addExtensionField( $tableName, $columnName, $sqlPath ) {
268
		$this->extensionUpdates[] = [ 'addField', $tableName, $columnName, $sqlPath, true ];
269
	}
270
271
	/**
272
	 *
273
	 * @since 1.20
274
	 *
275
	 * @param string $tableName
276
	 * @param string $columnName
277
	 * @param string $sqlPath
278
	 */
279
	public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
280
		$this->extensionUpdates[] = [ 'dropField', $tableName, $columnName, $sqlPath, true ];
281
	}
282
283
	/**
284
	 * Drop an index from an extension table
285
	 *
286
	 * @since 1.21
287
	 *
288
	 * @param string $tableName The table name
289
	 * @param string $indexName The index name
290
	 * @param string $sqlPath The path to the SQL change path
291
	 */
292
	public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
293
		$this->extensionUpdates[] = [ 'dropIndex', $tableName, $indexName, $sqlPath, true ];
294
	}
295
296
	/**
297
	 *
298
	 * @since 1.20
299
	 *
300
	 * @param string $tableName
301
	 * @param string $sqlPath
302
	 */
303
	public function dropExtensionTable( $tableName, $sqlPath ) {
304
		$this->extensionUpdates[] = [ 'dropTable', $tableName, $sqlPath, true ];
305
	}
306
307
	/**
308
	 * Rename an index on an extension table
309
	 *
310
	 * @since 1.21
311
	 *
312
	 * @param string $tableName The table name
313
	 * @param string $oldIndexName The old index name
314
	 * @param string $newIndexName The new index name
315
	 * @param string $sqlPath The path to the SQL change path
316
	 * @param bool $skipBothIndexExistWarning Whether to warn if both the old
317
	 * and the new indexes exist. [facultative; by default, false]
318
	 */
319
	public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
320
		$sqlPath, $skipBothIndexExistWarning = false
321
	) {
322
		$this->extensionUpdates[] = [
323
			'renameIndex',
324
			$tableName,
325
			$oldIndexName,
326
			$newIndexName,
327
			$skipBothIndexExistWarning,
328
			$sqlPath,
329
			true
330
		];
331
	}
332
333
	/**
334
	 * @since 1.21
335
	 *
336
	 * @param string $tableName The table name
337
	 * @param string $fieldName The field to be modified
338
	 * @param string $sqlPath The path to the SQL change path
339
	 */
340
	public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
341
		$this->extensionUpdates[] = [ 'modifyField', $tableName, $fieldName, $sqlPath, true ];
342
	}
343
344
	/**
345
	 *
346
	 * @since 1.20
347
	 *
348
	 * @param string $tableName
349
	 * @return bool
350
	 */
351
	public function tableExists( $tableName ) {
352
		return ( $this->db->tableExists( $tableName, __METHOD__ ) );
353
	}
354
355
	/**
356
	 * Add a maintenance script to be run after the database updates are complete.
357
	 *
358
	 * Script should subclass LoggedUpdateMaintenance
359
	 *
360
	 * @since 1.19
361
	 *
362
	 * @param string $class Name of a Maintenance subclass
363
	 */
364
	public function addPostDatabaseUpdateMaintenance( $class ) {
365
		$this->postDatabaseUpdateMaintenance[] = $class;
366
	}
367
368
	/**
369
	 * Get the list of extension-defined updates
370
	 *
371
	 * @return array
372
	 */
373
	protected function getExtensionUpdates() {
374
		return $this->extensionUpdates;
375
	}
376
377
	/**
378
	 * @since 1.17
379
	 *
380
	 * @return string[]
381
	 */
382
	public function getPostDatabaseUpdateMaintenance() {
383
		return $this->postDatabaseUpdateMaintenance;
384
	}
385
386
	/**
387
	 * @since 1.21
388
	 *
389
	 * Writes the schema updates desired to a file for the DB Admin to run.
390
	 * @param array $schemaUpdate
391
	 */
392
	private function writeSchemaUpdateFile( $schemaUpdate = [] ) {
393
		$updates = $this->updatesSkipped;
394
		$this->updatesSkipped = [];
395
396
		foreach ( $updates as $funcList ) {
397
			$func = $funcList[0];
398
			$arg = $funcList[1];
399
			$origParams = $funcList[2];
400
			call_user_func_array( $func, $arg );
401
			flush();
402
			$this->updatesSkipped[] = $origParams;
403
		}
404
	}
405
406
	/**
407
	 * Get appropriate schema variables in the current database connection.
408
	 *
409
	 * This should be called after any request data has been imported, but before
410
	 * any write operations to the database. The result should be passed to the DB
411
	 * setSchemaVars() method.
412
	 *
413
	 * @return array
414
	 * @since 1.28
415
	 */
416
	public function getSchemaVars() {
417
		return []; // DB-type specific
418
	}
419
420
	/**
421
	 * Do all the updates
422
	 *
423
	 * @param array $what What updates to perform
424
	 */
425
	public function doUpdates( $what = [ 'core', 'extensions', 'stats' ] ) {
426
		global $wgVersion;
427
428
		$this->db->setSchemaVars( $this->getSchemaVars() );
429
430
		$what = array_flip( $what );
431
		$this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
432
		if ( isset( $what['core'] ) ) {
433
			$this->runUpdates( $this->getCoreUpdateList(), false );
434
		}
435
		if ( isset( $what['extensions'] ) ) {
436
			$this->runUpdates( $this->getOldGlobalUpdates(), false );
437
			$this->runUpdates( $this->getExtensionUpdates(), true );
438
		}
439
440
		if ( isset( $what['stats'] ) ) {
441
			$this->checkStats();
442
		}
443
444
		$this->setAppliedUpdates( $wgVersion, $this->updates );
445
446
		if ( $this->fileHandle ) {
447
			$this->skipSchema = false;
448
			$this->writeSchemaUpdateFile();
449
			$this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
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
	 * @param string $version
487
	 * @param array $updates
488
	 */
489
	protected function setAppliedUpdates( $version, $updates = [] ) {
490
		$this->db->clearFlag( DBO_DDLMODE );
491
		if ( !$this->canUseNewUpdatelog() ) {
492
			return;
493
		}
494
		$key = "updatelist-$version-" . time() . self::$updateCounter;
495
		self::$updateCounter++;
496
		$this->db->insert( 'updatelog',
497
			[ 'ul_key' => $key, 'ul_value' => serialize( $updates ) ],
498
			__METHOD__ );
499
		$this->db->setFlag( DBO_DDLMODE );
500
	}
501
502
	/**
503
	 * Helper function: check if the given key is present in the updatelog table.
504
	 * Obviously, only use this for updates that occur after the updatelog table was
505
	 * created!
506
	 * @param string $key Name of the key to check for
507
	 * @return bool
508
	 */
509
	public function updateRowExists( $key ) {
510
		$row = $this->db->selectRow(
511
			'updatelog',
512
			# Bug 65813
513
			'1 AS X',
514
			[ 'ul_key' => $key ],
515
			__METHOD__
516
		);
517
518
		return (bool)$row;
519
	}
520
521
	/**
522
	 * Helper function: Add a key to the updatelog table
523
	 * Obviously, only use this for updates that occur after the updatelog table was
524
	 * created!
525
	 * @param string $key Name of key to insert
526
	 * @param string $val [optional] Value to insert along with the key
527
	 */
528
	public function insertUpdateRow( $key, $val = null ) {
529
		$this->db->clearFlag( DBO_DDLMODE );
530
		$values = [ 'ul_key' => $key ];
531
		if ( $val && $this->canUseNewUpdatelog() ) {
532
			$values['ul_value'] = $val;
533
		}
534
		$this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
535
		$this->db->setFlag( DBO_DDLMODE );
536
	}
537
538
	/**
539
	 * Updatelog was changed in 1.17 to have a ul_value column so we can record
540
	 * more information about what kind of updates we've done (that's what this
541
	 * class does). Pre-1.17 wikis won't have this column, and really old wikis
542
	 * might not even have updatelog at all
543
	 *
544
	 * @return bool
545
	 */
546
	protected function canUseNewUpdatelog() {
547
		return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
548
			$this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
549
	}
550
551
	/**
552
	 * Returns whether updates should be executed on the database table $name.
553
	 * Updates will be prevented if the table is a shared table and it is not
554
	 * specified to run updates on shared tables.
555
	 *
556
	 * @param string $name Table name
557
	 * @return bool
558
	 */
559
	protected function doTable( $name ) {
560
		global $wgSharedDB, $wgSharedTables;
561
562
		// Don't bother to check $wgSharedTables if there isn't a shared database
563
		// or the user actually also wants to do updates on the shared database.
564
		if ( $wgSharedDB === null || $this->shared ) {
565
			return true;
566
		}
567
568
		if ( in_array( $name, $wgSharedTables ) ) {
569
			$this->output( "...skipping update to shared table $name.\n" );
570
			return false;
571
		} else {
572
			return true;
573
		}
574
	}
575
576
	/**
577
	 * Before 1.17, we used to handle updates via stuff like
578
	 * $wgExtNewTables/Fields/Indexes. This is nasty :) We refactored a lot
579
	 * of this in 1.17 but we want to remain back-compatible for a while. So
580
	 * load up these old global-based things into our update list.
581
	 *
582
	 * @return array
583
	 */
584
	protected function getOldGlobalUpdates() {
585
		global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
586
			$wgExtNewIndexes;
587
588
		$updates = [];
589
590 View Code Duplication
		foreach ( $wgExtNewTables as $tableRecord ) {
591
			$updates[] = [
592
				'addTable', $tableRecord[0], $tableRecord[1], true
593
			];
594
		}
595
596
		foreach ( $wgExtNewFields as $fieldRecord ) {
597
			$updates[] = [
598
				'addField', $fieldRecord[0], $fieldRecord[1],
599
				$fieldRecord[2], true
600
			];
601
		}
602
603 View Code Duplication
		foreach ( $wgExtNewIndexes as $fieldRecord ) {
604
			$updates[] = [
605
				'addIndex', $fieldRecord[0], $fieldRecord[1],
606
				$fieldRecord[2], true
607
			];
608
		}
609
610
		foreach ( $wgExtModifiedFields as $fieldRecord ) {
611
			$updates[] = [
612
				'modifyField', $fieldRecord[0], $fieldRecord[1],
613
				$fieldRecord[2], true
614
			];
615
		}
616
617
		return $updates;
618
	}
619
620
	/**
621
	 * Get an array of updates to perform on the database. Should return a
622
	 * multi-dimensional array. The main key is the MediaWiki version (1.12,
623
	 * 1.13...) with the values being arrays of updates, identical to how
624
	 * updaters.inc did it (for now)
625
	 *
626
	 * @return array
627
	 */
628
	abstract protected function getCoreUpdateList();
629
630
	/**
631
	 * Append an SQL fragment to the open file handle.
632
	 *
633
	 * @param string $filename File name to open
634
	 */
635
	public function copyFile( $filename ) {
636
		$this->db->sourceFile(
637
			$filename,
638
			null,
639
			null,
640
			__METHOD__,
641
			[ $this, 'appendLine' ]
642
		);
643
	}
644
645
	/**
646
	 * Append a line to the open filehandle.  The line is assumed to
647
	 * be a complete SQL statement.
648
	 *
649
	 * This is used as a callback for sourceLine().
650
	 *
651
	 * @param string $line Text to append to the file
652
	 * @return bool False to skip actually executing the file
653
	 * @throws MWException
654
	 */
655
	public function appendLine( $line ) {
656
		$line = rtrim( $line ) . ";\n";
657
		if ( fwrite( $this->fileHandle, $line ) === false ) {
658
			throw new MWException( "trouble writing file" );
659
		}
660
661
		return false;
662
	}
663
664
	/**
665
	 * Applies a SQL patch
666
	 *
667
	 * @param string $path Path to the patch file
668
	 * @param bool $isFullPath Whether to treat $path as a relative or not
669
	 * @param string $msg Description of the patch
670
	 * @return bool False if patch is skipped.
671
	 */
672
	protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
673
		if ( $msg === null ) {
674
			$msg = "Applying $path patch";
675
		}
676
		if ( $this->skipSchema ) {
677
			$this->output( "...skipping schema change ($msg).\n" );
678
679
			return false;
680
		}
681
682
		$this->output( "$msg ..." );
683
684
		if ( !$isFullPath ) {
685
			$path = $this->patchPath( $this->db, $path );
686
		}
687
		if ( $this->fileHandle !== null ) {
688
			$this->copyFile( $path );
689
		} else {
690
			$this->db->sourceFile( $path );
691
		}
692
		$this->output( "done.\n" );
693
694
		return true;
695
	}
696
697
	/**
698
	 * Get the full path of a patch file. Originally based on archive()
699
	 * from updaters.inc. Keep in mind this always returns a patch, as
700
	 * it fails back to MySQL if no DB-specific patch can be found
701
	 *
702
	 * @param IDatabase $db
703
	 * @param string $patch The name of the patch, like patch-something.sql
704
	 * @return string Full path to patch file
705
	 */
706 View Code Duplication
	public function patchPath( IDatabase $db, $patch ) {
707
		global $IP;
708
709
		$dbType = $db->getType();
710
		if ( file_exists( "$IP/maintenance/$dbType/archives/$patch" ) ) {
711
			return "$IP/maintenance/$dbType/archives/$patch";
712
		} else {
713
			return "$IP/maintenance/archives/$patch";
714
		}
715
	}
716
717
	/**
718
	 * Add a new table to the database
719
	 *
720
	 * @param string $name Name of the new table
721
	 * @param string $patch Path to the patch file
722
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
723
	 * @return bool False if this was skipped because schema changes are skipped
724
	 */
725 View Code Duplication
	protected function addTable( $name, $patch, $fullpath = false ) {
726
		if ( !$this->doTable( $name ) ) {
727
			return true;
728
		}
729
730
		if ( $this->db->tableExists( $name, __METHOD__ ) ) {
731
			$this->output( "...$name table already exists.\n" );
732
		} else {
733
			return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
734
		}
735
736
		return true;
737
	}
738
739
	/**
740
	 * Add a new field to an existing table
741
	 *
742
	 * @param string $table Name of the table to modify
743
	 * @param string $field Name of the new field
744
	 * @param string $patch Path to the patch file
745
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
746
	 * @return bool False if this was skipped because schema changes are skipped
747
	 */
748 View Code Duplication
	protected function addField( $table, $field, $patch, $fullpath = false ) {
749
		if ( !$this->doTable( $table ) ) {
750
			return true;
751
		}
752
753
		if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
754
			$this->output( "...$table table does not exist, skipping new field patch.\n" );
755
		} elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
756
			$this->output( "...have $field field in $table table.\n" );
757
		} else {
758
			return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
759
		}
760
761
		return true;
762
	}
763
764
	/**
765
	 * Add a new index to an existing table
766
	 *
767
	 * @param string $table Name of the table to modify
768
	 * @param string $index Name of the new index
769
	 * @param string $patch Path to the patch file
770
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
771
	 * @return bool False if this was skipped because schema changes are skipped
772
	 */
773 View Code Duplication
	protected function addIndex( $table, $index, $patch, $fullpath = false ) {
774
		if ( !$this->doTable( $table ) ) {
775
			return true;
776
		}
777
778
		if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
779
			$this->output( "...skipping: '$table' table doesn't exist yet.\n" );
780
		} elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
781
			$this->output( "...index $index already set on $table table.\n" );
782
		} else {
783
			return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
784
		}
785
786
		return true;
787
	}
788
789
	/**
790
	 * Drop a field from an existing table
791
	 *
792
	 * @param string $table Name of the table to modify
793
	 * @param string $field Name of the old field
794
	 * @param string $patch Path to the patch file
795
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
796
	 * @return bool False if this was skipped because schema changes are skipped
797
	 */
798 View Code Duplication
	protected function dropField( $table, $field, $patch, $fullpath = false ) {
799
		if ( !$this->doTable( $table ) ) {
800
			return true;
801
		}
802
803
		if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
804
			return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
805
		} else {
806
			$this->output( "...$table table does not contain $field field.\n" );
807
		}
808
809
		return true;
810
	}
811
812
	/**
813
	 * Drop an index from an existing table
814
	 *
815
	 * @param string $table Name of the table to modify
816
	 * @param string $index Name of the index
817
	 * @param string $patch Path to the patch file
818
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
819
	 * @return bool False if this was skipped because schema changes are skipped
820
	 */
821 View Code Duplication
	protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
822
		if ( !$this->doTable( $table ) ) {
823
			return true;
824
		}
825
826
		if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
827
			return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
828
		} else {
829
			$this->output( "...$index key doesn't exist.\n" );
830
		}
831
832
		return true;
833
	}
834
835
	/**
836
	 * Rename an index from an existing table
837
	 *
838
	 * @param string $table Name of the table to modify
839
	 * @param string $oldIndex Old name of the index
840
	 * @param string $newIndex New name of the index
841
	 * @param bool $skipBothIndexExistWarning Whether to warn if both the
842
	 * old and the new indexes exist.
843
	 * @param string $patch Path to the patch file
844
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
845
	 * @return bool False if this was skipped because schema changes are skipped
846
	 */
847
	protected function renameIndex( $table, $oldIndex, $newIndex,
848
		$skipBothIndexExistWarning, $patch, $fullpath = false
849
	) {
850
		if ( !$this->doTable( $table ) ) {
851
			return true;
852
		}
853
854
		// First requirement: the table must exist
855
		if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
856
			$this->output( "...skipping: '$table' table doesn't exist yet.\n" );
857
858
			return true;
859
		}
860
861
		// Second requirement: the new index must be missing
862 View Code Duplication
		if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
863
			$this->output( "...index $newIndex already set on $table table.\n" );
864
			if ( !$skipBothIndexExistWarning &&
865
				$this->db->indexExists( $table, $oldIndex, __METHOD__ )
866
			) {
867
				$this->output( "...WARNING: $oldIndex still exists, despite it has " .
868
					"been renamed into $newIndex (which also exists).\n" .
869
					"            $oldIndex should be manually removed if not needed anymore.\n" );
870
			}
871
872
			return true;
873
		}
874
875
		// Third requirement: the old index must exist
876
		if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->db->indexExists($... $oldIndex, __METHOD__) of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
877
			$this->output( "...skipping: index $oldIndex doesn't exist.\n" );
878
879
			return true;
880
		}
881
882
		// Requirements have been satisfied, patch can be applied
883
		return $this->applyPatch(
884
			$patch,
885
			$fullpath,
886
			"Renaming index $oldIndex into $newIndex to table $table"
887
		);
888
	}
889
890
	/**
891
	 * If the specified table exists, drop it, or execute the
892
	 * patch if one is provided.
893
	 *
894
	 * Public @since 1.20
895
	 *
896
	 * @param string $table Table to drop.
897
	 * @param string|bool $patch String of patch file that will drop the table. Default: false.
898
	 * @param bool $fullpath Whether $patch is a full path. Default: false.
899
	 * @return bool False if this was skipped because schema changes are skipped
900
	 */
901
	public function dropTable( $table, $patch = false, $fullpath = false ) {
902
		if ( !$this->doTable( $table ) ) {
903
			return true;
904
		}
905
906
		if ( $this->db->tableExists( $table, __METHOD__ ) ) {
907
			$msg = "Dropping table $table";
908
909
			if ( $patch === false ) {
910
				$this->output( "$msg ..." );
911
				$this->db->dropTable( $table, __METHOD__ );
912
				$this->output( "done.\n" );
913
			} else {
914
				return $this->applyPatch( $patch, $fullpath, $msg );
915
			}
916
		} else {
917
			$this->output( "...$table doesn't exist.\n" );
918
		}
919
920
		return true;
921
	}
922
923
	/**
924
	 * Modify an existing field
925
	 *
926
	 * @param string $table Name of the table to which the field belongs
927
	 * @param string $field Name of the field to modify
928
	 * @param string $patch Path to the patch file
929
	 * @param bool $fullpath Whether to treat $patch path as a relative or not
930
	 * @return bool False if this was skipped because schema changes are skipped
931
	 */
932
	public function modifyField( $table, $field, $patch, $fullpath = false ) {
933
		if ( !$this->doTable( $table ) ) {
934
			return true;
935
		}
936
937
		$updateKey = "$table-$field-$patch";
938
		if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
939
			$this->output( "...$table table does not exist, skipping modify field patch.\n" );
940
		} elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
941
			$this->output( "...$field field does not exist in $table table, " .
942
				"skipping modify field patch.\n" );
943
		} elseif ( $this->updateRowExists( $updateKey ) ) {
944
			$this->output( "...$field in table $table already modified by patch $patch.\n" );
945
		} else {
946
			$this->insertUpdateRow( $updateKey );
947
948
			return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
949
		}
950
951
		return true;
952
	}
953
954
	/**
955
	 * Set any .htaccess files or equivilent for storage repos
956
	 *
957
	 * Some zones (e.g. "temp") used to be public and may have been initialized as such
958
	 */
959
	public function setFileAccess() {
960
		$repo = RepoGroup::singleton()->getLocalRepo();
961
		$zonePath = $repo->getZonePath( 'temp' );
962
		if ( $repo->getBackend()->directoryExists( [ 'dir' => $zonePath ] ) ) {
963
			// If the directory was never made, then it will have the right ACLs when it is made
964
			$status = $repo->getBackend()->secure( [
965
				'dir' => $zonePath,
966
				'noAccess' => true,
967
				'noListing' => true
968
			] );
969
			if ( $status->isOK() ) {
970
				$this->output( "Set the local repo temp zone container to be private.\n" );
971
			} else {
972
				$this->output( "Failed to set the local repo temp zone container to be private.\n" );
973
			}
974
		}
975
	}
976
977
	/**
978
	 * Purge the objectcache table
979
	 */
980
	public function purgeCache() {
981
		global $wgLocalisationCacheConf;
982
		# We can't guarantee that the user will be able to use TRUNCATE,
983
		# but we know that DELETE is available to us
984
		$this->output( "Purging caches..." );
985
		$this->db->delete( 'objectcache', '*', __METHOD__ );
986
		if ( $wgLocalisationCacheConf['manualRecache'] ) {
987
			$this->rebuildLocalisationCache();
988
		}
989
		$blobStore = new MessageBlobStore();
990
		$blobStore->clear();
991
		$this->db->delete( 'module_deps', '*', __METHOD__ );
992
		$this->output( "done.\n" );
993
	}
994
995
	/**
996
	 * Check the site_stats table is not properly populated.
997
	 */
998
	protected function checkStats() {
999
		$this->output( "...site_stats is populated..." );
1000
		$row = $this->db->selectRow( 'site_stats', '*', [ 'ss_row_id' => 1 ], __METHOD__ );
1001
		if ( $row === false ) {
1002
			$this->output( "data is missing! rebuilding...\n" );
1003
		} elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
1004
			$this->output( "missing ss_total_pages, rebuilding...\n" );
1005
		} else {
1006
			$this->output( "done.\n" );
1007
1008
			return;
1009
		}
1010
		SiteStatsInit::doAllAndCommit( $this->db );
1011
	}
1012
1013
	# Common updater functions
1014
1015
	/**
1016
	 * Sets the number of active users in the site_stats table
1017
	 */
1018
	protected function doActiveUsersInit() {
1019
		$activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
1020
		if ( $activeUsers == -1 ) {
1021
			$activeUsers = $this->db->selectField( 'recentchanges',
1022
				'COUNT( DISTINCT rc_user_text )',
1023
				[ 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ], __METHOD__
1024
			);
1025
			$this->db->update( 'site_stats',
1026
				[ 'ss_active_users' => intval( $activeUsers ) ],
1027
				[ 'ss_row_id' => 1 ], __METHOD__, [ 'LIMIT' => 1 ]
1028
			);
1029
		}
1030
		$this->output( "...ss_active_users user count set...\n" );
1031
	}
1032
1033
	/**
1034
	 * Populates the log_user_text field in the logging table
1035
	 */
1036 View Code Duplication
	protected function doLogUsertextPopulation() {
1037
		if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
1038
			$this->output(
1039
				"Populating log_user_text field, printing progress markers. For large\n" .
1040
				"databases, you may want to hit Ctrl-C and do this manually with\n" .
1041
				"maintenance/populateLogUsertext.php.\n"
1042
			);
1043
1044
			$task = $this->maintenance->runChild( 'PopulateLogUsertext' );
1045
			$task->execute();
1046
			$this->output( "done.\n" );
1047
		}
1048
	}
1049
1050
	/**
1051
	 * Migrate log params to new table and index for searching
1052
	 */
1053 View Code Duplication
	protected function doLogSearchPopulation() {
1054
		if ( !$this->updateRowExists( 'populate log_search' ) ) {
1055
			$this->output(
1056
				"Populating log_search table, printing progress markers. For large\n" .
1057
				"databases, you may want to hit Ctrl-C and do this manually with\n" .
1058
				"maintenance/populateLogSearch.php.\n" );
1059
1060
			$task = $this->maintenance->runChild( 'PopulateLogSearch' );
1061
			$task->execute();
1062
			$this->output( "done.\n" );
1063
		}
1064
	}
1065
1066
	/**
1067
	 * Updates the timestamps in the transcache table
1068
	 * @return bool
1069
	 */
1070
	protected function doUpdateTranscacheField() {
1071
		if ( $this->updateRowExists( 'convert transcache field' ) ) {
1072
			$this->output( "...transcache tc_time already converted.\n" );
1073
1074
			return true;
1075
		}
1076
1077
		return $this->applyPatch( 'patch-tc-timestamp.sql', false,
1078
			"Converting tc_time from UNIX epoch to MediaWiki timestamp" );
1079
	}
1080
1081
	/**
1082
	 * Update CategoryLinks collation
1083
	 */
1084
	protected function doCollationUpdate() {
1085
		global $wgCategoryCollation;
1086
		if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
1087
			if ( $this->db->selectField(
1088
				'categorylinks',
1089
				'COUNT(*)',
1090
				'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
1091
				__METHOD__
1092
				) == 0
1093
			) {
1094
				$this->output( "...collations up-to-date.\n" );
1095
1096
				return;
1097
			}
1098
1099
			$this->output( "Updating category collations..." );
1100
			$task = $this->maintenance->runChild( 'UpdateCollation' );
1101
			$task->execute();
1102
			$this->output( "...done.\n" );
1103
		}
1104
	}
1105
1106
	/**
1107
	 * Migrates user options from the user table blob to user_properties
1108
	 */
1109
	protected function doMigrateUserOptions() {
1110
		if ( $this->db->tableExists( 'user_properties' ) ) {
1111
			$cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
1112
			$cl->execute();
1113
			$this->output( "done.\n" );
1114
		}
1115
	}
1116
1117
	/**
1118
	 * Enable profiling table when it's turned on
1119
	 */
1120
	protected function doEnableProfiling() {
1121
		global $wgProfiler;
1122
1123
		if ( !$this->doTable( 'profiling' ) ) {
1124
			return;
1125
		}
1126
1127
		$profileToDb = false;
1128 View Code Duplication
		if ( isset( $wgProfiler['output'] ) ) {
1129
			$out = $wgProfiler['output'];
1130
			if ( $out === 'db' ) {
1131
				$profileToDb = true;
1132
			} elseif ( is_array( $out ) && in_array( 'db', $out ) ) {
1133
				$profileToDb = true;
1134
			}
1135
		}
1136
1137
		if ( $profileToDb && !$this->db->tableExists( 'profiling', __METHOD__ ) ) {
1138
			$this->applyPatch( 'patch-profiling.sql', false, 'Add profiling table' );
1139
		}
1140
	}
1141
1142
	/**
1143
	 * Rebuilds the localisation cache
1144
	 */
1145
	protected function rebuildLocalisationCache() {
1146
		/**
1147
		 * @var $cl RebuildLocalisationCache
1148
		 */
1149
		$cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
1150
		$this->output( "Rebuilding localisation cache...\n" );
1151
		$cl->setForce();
1152
		$cl->execute();
1153
		$this->output( "done.\n" );
1154
	}
1155
1156
	/**
1157
	 * Turns off content handler fields during parts of the upgrade
1158
	 * where they aren't available.
1159
	 */
1160
	protected function disableContentHandlerUseDB() {
1161
		global $wgContentHandlerUseDB;
1162
1163
		if ( $wgContentHandlerUseDB ) {
1164
			$this->output( "Turning off Content Handler DB fields for this part of upgrade.\n" );
1165
			$this->holdContentHandlerUseDB = $wgContentHandlerUseDB;
1166
			$wgContentHandlerUseDB = false;
1167
		}
1168
	}
1169
1170
	/**
1171
	 * Turns content handler fields back on.
1172
	 */
1173
	protected function enableContentHandlerUseDB() {
1174
		global $wgContentHandlerUseDB;
1175
1176
		if ( $this->holdContentHandlerUseDB ) {
1177
			$this->output( "Content Handler DB fields should be usable now.\n" );
1178
			$wgContentHandlerUseDB = $this->holdContentHandlerUseDB;
1179
		}
1180
	}
1181
}
1182