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

includes/installer/DatabaseUpdater.php (3 issues)

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;
0 ignored issues
show
The property maintenance does not seem to exist. Did you mean postDatabaseUpdateMaintenance?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
115
			$this->fileHandle = $maintenance->fileHandle;
116
		} else {
117
			$this->maintenance = new FakeMaintenance;
0 ignored issues
show
The property maintenance does not seem to exist. Did you mean postDatabaseUpdateMaintenance?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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() ) {
0 ignored issues
show
The property maintenance does not seem to exist. Did you mean postDatabaseUpdateMaintenance?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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__ ) ) {
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