Merge "mediawiki.Title: Generalise filename-matching in newFromImg"
[lhc/web/wiklou.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
24 require_once __DIR__ . '/../../maintenance/Maintenance.php';
25
26 /**
27 * Class for handling database updates. Roughly based off of updaters.inc, with
28 * a few improvements :)
29 *
30 * @ingroup Deployment
31 * @since 1.17
32 */
33 abstract class DatabaseUpdater {
34
35 /**
36 * Array of updates to perform on the database
37 *
38 * @var array
39 */
40 protected $updates = array();
41
42 /**
43 * Array of updates that were skipped
44 *
45 * @var array
46 */
47 protected $updatesSkipped = array();
48
49 /**
50 * List of extension-provided database updates
51 * @var array
52 */
53 protected $extensionUpdates = array();
54
55 /**
56 * Handle to the database subclass
57 *
58 * @var DatabaseBase
59 */
60 protected $db;
61
62 protected $shared = false;
63
64 /**
65 * Scripts to run after database update
66 * Should be a subclass of LoggedUpdateMaintenance
67 */
68 protected $postDatabaseUpdateMaintenance = array(
69 'DeleteDefaultMessages',
70 'PopulateRevisionLength',
71 'PopulateRevisionSha1',
72 'PopulateImageSha1',
73 'FixExtLinksProtocolRelative',
74 'PopulateFilearchiveSha1',
75 );
76
77 /**
78 * File handle for SQL output.
79 *
80 * @var Filehandle
81 */
82 protected $fileHandle = null;
83
84 /**
85 * Flag specifying whether or not to skip schema (e.g. SQL-only) updates.
86 *
87 * @var bool
88 */
89 protected $skipSchema = false;
90
91 /**
92 * Constructor
93 *
94 * @param $db DatabaseBase object to perform updates on
95 * @param bool $shared Whether to perform updates on shared tables
96 * @param $maintenance Maintenance Maintenance object which created us
97 */
98 protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) {
99 $this->db = $db;
100 $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
101 $this->shared = $shared;
102 if ( $maintenance ) {
103 $this->maintenance = $maintenance;
104 $this->fileHandle = $maintenance->fileHandle;
105 } else {
106 $this->maintenance = new FakeMaintenance;
107 }
108 $this->maintenance->setDB( $db );
109 $this->initOldGlobals();
110 $this->loadExtensions();
111 wfRunHooks( 'LoadExtensionSchemaUpdates', array( $this ) );
112 }
113
114 /**
115 * Initialize all of the old globals. One day this should all become
116 * something much nicer
117 */
118 private function initOldGlobals() {
119 global $wgExtNewTables, $wgExtNewFields, $wgExtPGNewFields,
120 $wgExtPGAlteredFields, $wgExtNewIndexes, $wgExtModifiedFields;
121
122 # For extensions only, should be populated via hooks
123 # $wgDBtype should be checked to specifiy the proper file
124 $wgExtNewTables = array(); // table, dir
125 $wgExtNewFields = array(); // table, column, dir
126 $wgExtPGNewFields = array(); // table, column, column attributes; for PostgreSQL
127 $wgExtPGAlteredFields = array(); // table, column, new type, conversion method; for PostgreSQL
128 $wgExtNewIndexes = array(); // table, index, dir
129 $wgExtModifiedFields = array(); // table, index, dir
130 }
131
132 /**
133 * Loads LocalSettings.php, if needed, and initialises everything needed for
134 * LoadExtensionSchemaUpdates hook.
135 */
136 private function loadExtensions() {
137 if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
138 return; // already loaded
139 }
140 $vars = Installer::getExistingLocalSettings();
141 if ( !$vars ) {
142 return; // no LocalSettings found
143 }
144 if ( !isset( $vars['wgHooks'] ) || !isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
145 return;
146 }
147 global $wgHooks, $wgAutoloadClasses;
148 $wgHooks['LoadExtensionSchemaUpdates'] = $vars['wgHooks']['LoadExtensionSchemaUpdates'];
149 $wgAutoloadClasses = $wgAutoloadClasses + $vars['wgAutoloadClasses'];
150 }
151
152 /**
153 * @throws MWException
154 * @param DatabaseBase $db
155 * @param bool $shared
156 * @param null $maintenance
157 * @return DatabaseUpdater
158 */
159 public static function newForDB( &$db, $shared = false, $maintenance = null ) {
160 $type = $db->getType();
161 if ( in_array( $type, Installer::getDBTypes() ) ) {
162 $class = ucfirst( $type ) . 'Updater';
163
164 return new $class( $db, $shared, $maintenance );
165 } else {
166 throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' );
167 }
168 }
169
170 /**
171 * Get a database connection to run updates
172 *
173 * @return DatabaseBase
174 */
175 public function getDB() {
176 return $this->db;
177 }
178
179 /**
180 * Output some text. If we're running from web, escape the text first.
181 *
182 * @param string $str Text to output
183 */
184 public function output( $str ) {
185 if ( $this->maintenance->isQuiet() ) {
186 return;
187 }
188 global $wgCommandLineMode;
189 if ( !$wgCommandLineMode ) {
190 $str = htmlspecialchars( $str );
191 }
192 echo $str;
193 flush();
194 }
195
196 /**
197 * Add a new update coming from an extension. This should be called by
198 * extensions while executing the LoadExtensionSchemaUpdates hook.
199 *
200 * @since 1.17
201 *
202 * @param array $update the update to run. Format is the following:
203 * first item is the callback function, it also can be a
204 * simple string with the name of a function in this class,
205 * following elements are parameters to the function.
206 * Note that callback functions will receive this object as
207 * first parameter.
208 */
209 public function addExtensionUpdate( array $update ) {
210 $this->extensionUpdates[] = $update;
211 }
212
213 /**
214 * Convenience wrapper for addExtensionUpdate() when adding a new table (which
215 * is the most common usage of updaters in an extension)
216 *
217 * @since 1.18
218 *
219 * @param string $tableName Name of table to create
220 * @param string $sqlPath Full path to the schema file
221 */
222 public function addExtensionTable( $tableName, $sqlPath ) {
223 $this->extensionUpdates[] = array( 'addTable', $tableName, $sqlPath, true );
224 }
225
226 /**
227 * @since 1.19
228 *
229 * @param $tableName string
230 * @param $indexName string
231 * @param $sqlPath string
232 */
233 public function addExtensionIndex( $tableName, $indexName, $sqlPath ) {
234 $this->extensionUpdates[] = array( 'addIndex', $tableName, $indexName, $sqlPath, true );
235 }
236
237 /**
238 *
239 * @since 1.19
240 *
241 * @param $tableName string
242 * @param $columnName string
243 * @param $sqlPath string
244 */
245 public function addExtensionField( $tableName, $columnName, $sqlPath ) {
246 $this->extensionUpdates[] = array( 'addField', $tableName, $columnName, $sqlPath, true );
247 }
248
249 /**
250 *
251 * @since 1.20
252 *
253 * @param $tableName string
254 * @param $columnName string
255 * @param $sqlPath string
256 */
257 public function dropExtensionField( $tableName, $columnName, $sqlPath ) {
258 $this->extensionUpdates[] = array( 'dropField', $tableName, $columnName, $sqlPath, true );
259 }
260
261 /**
262 * Drop an index from an extension table
263 *
264 * @since 1.21
265 *
266 * @param string $tableName The table name
267 * @param string $indexName The index name
268 * @param string $sqlPath The path to the SQL change path
269 */
270 public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) {
271 $this->extensionUpdates[] = array( 'dropIndex', $tableName, $indexName, $sqlPath, true );
272 }
273
274 /**
275 *
276 * @since 1.20
277 *
278 * @param $tableName string
279 * @param $sqlPath string
280 */
281 public function dropExtensionTable( $tableName, $sqlPath ) {
282 $this->extensionUpdates[] = array( 'dropTable', $tableName, $sqlPath, true );
283 }
284
285 /**
286 * Rename an index on an extension table
287 *
288 * @since 1.21
289 *
290 * @param string $tableName The table name
291 * @param string $oldIndexName The old index name
292 * @param string $newIndexName The new index name
293 * @param $skipBothIndexExistWarning Boolean: Whether to warn if both the old
294 * and the new indexes exist. [facultative; by default, false]
295 * @param string $sqlPath The path to the SQL change path
296 */
297 public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName,
298 $sqlPath, $skipBothIndexExistWarning = false
299 ) {
300 $this->extensionUpdates[] = array(
301 'renameIndex',
302 $tableName,
303 $oldIndexName,
304 $newIndexName,
305 $skipBothIndexExistWarning,
306 $sqlPath,
307 true
308 );
309 }
310
311 /**
312 * @since 1.21
313 *
314 * @param string $tableName The table name
315 * @param string $fieldName The field to be modified
316 * @param string $sqlPath The path to the SQL change path
317 */
318 public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) {
319 $this->extensionUpdates[] = array( 'modifyField', $tableName, $fieldName, $sqlPath, true );
320 }
321
322 /**
323 *
324 * @since 1.20
325 *
326 * @param $tableName string
327 * @return bool
328 */
329 public function tableExists( $tableName ) {
330 return ( $this->db->tableExists( $tableName, __METHOD__ ) );
331 }
332
333 /**
334 * Add a maintenance script to be run after the database updates are complete.
335 *
336 * Script should subclass LoggedUpdateMaintenance
337 *
338 * @since 1.19
339 *
340 * @param string $class Name of a Maintenance subclass
341 */
342 public function addPostDatabaseUpdateMaintenance( $class ) {
343 $this->postDatabaseUpdateMaintenance[] = $class;
344 }
345
346 /**
347 * Get the list of extension-defined updates
348 *
349 * @return Array
350 */
351 protected function getExtensionUpdates() {
352 return $this->extensionUpdates;
353 }
354
355 /**
356 * @since 1.17
357 *
358 * @return array
359 */
360 public function getPostDatabaseUpdateMaintenance() {
361 return $this->postDatabaseUpdateMaintenance;
362 }
363
364 /**
365 * @since 1.21
366 *
367 * Writes the schema updates desired to a file for the DB Admin to run.
368 */
369 private function writeSchemaUpdateFile( $schemaUpdate = array() ) {
370 $updates = $this->updatesSkipped;
371 $this->updatesSkipped = array();
372
373 foreach ( $updates as $funcList ) {
374 $func = $funcList[0];
375 $arg = $funcList[1];
376 $origParams = $funcList[2];
377 call_user_func_array( $func, $arg );
378 flush();
379 $this->updatesSkipped[] = $origParams;
380 }
381 }
382
383 /**
384 * Do all the updates
385 *
386 * @param array $what what updates to perform
387 */
388 public function doUpdates( $what = array( 'core', 'extensions', 'stats' ) ) {
389 global $wgVersion;
390
391 $this->db->begin( __METHOD__ );
392 $what = array_flip( $what );
393 $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null;
394 if ( isset( $what['core'] ) ) {
395 $this->runUpdates( $this->getCoreUpdateList(), false );
396 }
397 if ( isset( $what['extensions'] ) ) {
398 $this->runUpdates( $this->getOldGlobalUpdates(), false );
399 $this->runUpdates( $this->getExtensionUpdates(), true );
400 }
401
402 if ( isset( $what['stats'] ) ) {
403 $this->checkStats();
404 }
405
406 $this->setAppliedUpdates( $wgVersion, $this->updates );
407
408 if ( $this->fileHandle ) {
409 $this->skipSchema = false;
410 $this->writeSchemaUpdateFile();
411 $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped );
412 }
413
414 $this->db->commit( __METHOD__ );
415 }
416
417 /**
418 * Helper function for doUpdates()
419 *
420 * @param array $updates of updates to run
421 * @param $passSelf Boolean: whether to pass this object we calling external
422 * functions
423 */
424 private function runUpdates( array $updates, $passSelf ) {
425 $updatesDone = array();
426 $updatesSkipped = array();
427 foreach ( $updates as $params ) {
428 $origParams = $params;
429 $func = array_shift( $params );
430 if ( !is_array( $func ) && method_exists( $this, $func ) ) {
431 $func = array( $this, $func );
432 } elseif ( $passSelf ) {
433 array_unshift( $params, $this );
434 }
435 $ret = call_user_func_array( $func, $params );
436 flush();
437 if ( $ret !== false ) {
438 $updatesDone[] = $origParams;
439 } else {
440 $updatesSkipped[] = array( $func, $params, $origParams );
441 }
442 }
443 $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped );
444 $this->updates = array_merge( $this->updates, $updatesDone );
445 }
446
447 /**
448 * @param $version
449 * @param $updates array
450 */
451 protected function setAppliedUpdates( $version, $updates = array() ) {
452 $this->db->clearFlag( DBO_DDLMODE );
453 if ( !$this->canUseNewUpdatelog() ) {
454 return;
455 }
456 $key = "updatelist-$version-" . time();
457 $this->db->insert( 'updatelog',
458 array( 'ul_key' => $key, 'ul_value' => serialize( $updates ) ),
459 __METHOD__ );
460 $this->db->setFlag( DBO_DDLMODE );
461 }
462
463 /**
464 * Helper function: check if the given key is present in the updatelog table.
465 * Obviously, only use this for updates that occur after the updatelog table was
466 * created!
467 * @param string $key Name of the key to check for
468 *
469 * @return bool
470 */
471 public function updateRowExists( $key ) {
472 $row = $this->db->selectRow(
473 'updatelog',
474 '1',
475 array( 'ul_key' => $key ),
476 __METHOD__
477 );
478
479 return (bool)$row;
480 }
481
482 /**
483 * Helper function: Add a key to the updatelog table
484 * Obviously, only use this for updates that occur after the updatelog table was
485 * created!
486 * @param string $key Name of key to insert
487 * @param string $val [optional] value to insert along with the key
488 */
489 public function insertUpdateRow( $key, $val = null ) {
490 $this->db->clearFlag( DBO_DDLMODE );
491 $values = array( 'ul_key' => $key );
492 if ( $val && $this->canUseNewUpdatelog() ) {
493 $values['ul_value'] = $val;
494 }
495 $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' );
496 $this->db->setFlag( DBO_DDLMODE );
497 }
498
499 /**
500 * Updatelog was changed in 1.17 to have a ul_value column so we can record
501 * more information about what kind of updates we've done (that's what this
502 * class does). Pre-1.17 wikis won't have this column, and really old wikis
503 * might not even have updatelog at all
504 *
505 * @return boolean
506 */
507 protected function canUseNewUpdatelog() {
508 return $this->db->tableExists( 'updatelog', __METHOD__ ) &&
509 $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ );
510 }
511
512 /**
513 * Returns whether updates should be executed on the database table $name.
514 * Updates will be prevented if the table is a shared table and it is not
515 * specified to run updates on shared tables.
516 *
517 * @param string $name table name
518 * @return bool
519 */
520 protected function doTable( $name ) {
521 global $wgSharedDB, $wgSharedTables;
522
523 // Don't bother to check $wgSharedTables if there isn't a shared database
524 // or the user actually also wants to do updates on the shared database.
525 if ( $wgSharedDB === null || $this->shared ) {
526 return true;
527 }
528
529 return !in_array( $name, $wgSharedTables );
530 }
531
532 /**
533 * Before 1.17, we used to handle updates via stuff like
534 * $wgExtNewTables/Fields/Indexes. This is nasty :) We refactored a lot
535 * of this in 1.17 but we want to remain back-compatible for a while. So
536 * load up these old global-based things into our update list.
537 *
538 * @return array
539 */
540 protected function getOldGlobalUpdates() {
541 global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields,
542 $wgExtNewIndexes;
543
544 $updates = array();
545
546 foreach ( $wgExtNewTables as $tableRecord ) {
547 $updates[] = array(
548 'addTable', $tableRecord[0], $tableRecord[1], true
549 );
550 }
551
552 foreach ( $wgExtNewFields as $fieldRecord ) {
553 $updates[] = array(
554 'addField', $fieldRecord[0], $fieldRecord[1],
555 $fieldRecord[2], true
556 );
557 }
558
559 foreach ( $wgExtNewIndexes as $fieldRecord ) {
560 $updates[] = array(
561 'addIndex', $fieldRecord[0], $fieldRecord[1],
562 $fieldRecord[2], true
563 );
564 }
565
566 foreach ( $wgExtModifiedFields as $fieldRecord ) {
567 $updates[] = array(
568 'modifyField', $fieldRecord[0], $fieldRecord[1],
569 $fieldRecord[2], true
570 );
571 }
572
573 return $updates;
574 }
575
576 /**
577 * Get an array of updates to perform on the database. Should return a
578 * multi-dimensional array. The main key is the MediaWiki version (1.12,
579 * 1.13...) with the values being arrays of updates, identical to how
580 * updaters.inc did it (for now)
581 *
582 * @return Array
583 */
584 abstract protected function getCoreUpdateList();
585
586 /**
587 * Append an SQL fragment to the open file handle.
588 *
589 * @param string $filename File name to open
590 */
591 public function copyFile( $filename ) {
592 $this->db->sourceFile( $filename, false, false, false,
593 array( $this, 'appendLine' )
594 );
595 }
596
597 /**
598 * Append a line to the open filehandle. The line is assumed to
599 * be a complete SQL statement.
600 *
601 * This is used as a callback for for sourceLine().
602 *
603 * @param string $line text to append to the file
604 * @return Boolean false to skip actually executing the file
605 * @throws MWException
606 */
607 public function appendLine( $line ) {
608 $line = rtrim( $line ) . ";\n";
609 if ( fwrite( $this->fileHandle, $line ) === false ) {
610 throw new MWException( "trouble writing file" );
611 }
612
613 return false;
614 }
615
616 /**
617 * Applies a SQL patch
618 *
619 * @param string $path Path to the patch file
620 * @param $isFullPath Boolean Whether to treat $path as a relative or not
621 * @param string $msg Description of the patch
622 * @return boolean false if patch is skipped.
623 */
624 protected function applyPatch( $path, $isFullPath = false, $msg = null ) {
625 if ( $msg === null ) {
626 $msg = "Applying $path patch";
627 }
628 if ( $this->skipSchema ) {
629 $this->output( "...skipping schema change ($msg).\n" );
630
631 return false;
632 }
633
634 $this->output( "$msg ..." );
635
636 if ( !$isFullPath ) {
637 $path = $this->db->patchPath( $path );
638 }
639 if ( $this->fileHandle !== null ) {
640 $this->copyFile( $path );
641 } else {
642 $this->db->sourceFile( $path );
643 }
644 $this->output( "done.\n" );
645
646 return true;
647 }
648
649 /**
650 * Add a new table to the database
651 *
652 * @param string $name Name of the new table
653 * @param string $patch Path to the patch file
654 * @param $fullpath Boolean Whether to treat $patch path as a relative or not
655 * @return Boolean false if this was skipped because schema changes are skipped
656 */
657 protected function addTable( $name, $patch, $fullpath = false ) {
658 if ( !$this->doTable( $name ) ) {
659 return true;
660 }
661
662 if ( $this->db->tableExists( $name, __METHOD__ ) ) {
663 $this->output( "...$name table already exists.\n" );
664 } else {
665 return $this->applyPatch( $patch, $fullpath, "Creating $name table" );
666 }
667
668 return true;
669 }
670
671 /**
672 * Add a new field to an existing table
673 *
674 * @param string $table Name of the table to modify
675 * @param string $field Name of the new field
676 * @param string $patch Path to the patch file
677 * @param $fullpath Boolean Whether to treat $patch path as a relative or not
678 * @return Boolean false if this was skipped because schema changes are skipped
679 */
680 protected function addField( $table, $field, $patch, $fullpath = false ) {
681 if ( !$this->doTable( $table ) ) {
682 return true;
683 }
684
685 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
686 $this->output( "...$table table does not exist, skipping new field patch.\n" );
687 } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
688 $this->output( "...have $field field in $table table.\n" );
689 } else {
690 return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" );
691 }
692
693 return true;
694 }
695
696 /**
697 * Add a new index to an existing table
698 *
699 * @param string $table Name of the table to modify
700 * @param string $index Name of the new index
701 * @param string $patch Path to the patch file
702 * @param $fullpath Boolean Whether to treat $patch path as a relative or not
703 * @return Boolean false if this was skipped because schema changes are skipped
704 */
705 protected function addIndex( $table, $index, $patch, $fullpath = false ) {
706 if ( !$this->doTable( $table ) ) {
707 return true;
708 }
709
710 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
711 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
712 } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
713 $this->output( "...index $index already set on $table table.\n" );
714 } else {
715 return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" );
716 }
717
718 return true;
719 }
720
721 /**
722 * Drop a field from an existing table
723 *
724 * @param string $table Name of the table to modify
725 * @param string $field Name of the old field
726 * @param string $patch Path to the patch file
727 * @param $fullpath Boolean Whether to treat $patch path as a relative or not
728 * @return Boolean false if this was skipped because schema changes are skipped
729 */
730 protected function dropField( $table, $field, $patch, $fullpath = false ) {
731 if ( !$this->doTable( $table ) ) {
732 return true;
733 }
734
735 if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) {
736 return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" );
737 } else {
738 $this->output( "...$table table does not contain $field field.\n" );
739 }
740
741 return true;
742 }
743
744 /**
745 * Drop an index from an existing table
746 *
747 * @param string $table Name of the table to modify
748 * @param string $index Name of the index
749 * @param string $patch Path to the patch file
750 * @param $fullpath Boolean: Whether to treat $patch path as a relative or not
751 * @return Boolean false if this was skipped because schema changes are skipped
752 */
753 protected function dropIndex( $table, $index, $patch, $fullpath = false ) {
754 if ( !$this->doTable( $table ) ) {
755 return true;
756 }
757
758 if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) {
759 return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" );
760 } else {
761 $this->output( "...$index key doesn't exist.\n" );
762 }
763
764 return true;
765 }
766
767 /**
768 * Rename an index from an existing table
769 *
770 * @param string $table Name of the table to modify
771 * @param string $oldIndex Old name of the index
772 * @param string $newIndex New name of the index
773 * @param $skipBothIndexExistWarning Boolean: Whether to warn if both the
774 * old and the new indexes exist.
775 * @param string $patch Path to the patch file
776 * @param $fullpath Boolean: Whether to treat $patch path as a relative or not
777 * @return Boolean false if this was skipped because schema changes are skipped
778 */
779 protected function renameIndex( $table, $oldIndex, $newIndex,
780 $skipBothIndexExistWarning, $patch, $fullpath = false
781 ) {
782 if ( !$this->doTable( $table ) ) {
783 return true;
784 }
785
786 // First requirement: the table must exist
787 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
788 $this->output( "...skipping: '$table' table doesn't exist yet.\n" );
789
790 return true;
791 }
792
793 // Second requirement: the new index must be missing
794 if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) {
795 $this->output( "...index $newIndex already set on $table table.\n" );
796 if ( !$skipBothIndexExistWarning &&
797 $this->db->indexExists( $table, $oldIndex, __METHOD__ )
798 ) {
799 $this->output( "...WARNING: $oldIndex still exists, despite it has " .
800 "been renamed into $newIndex (which also exists).\n" .
801 " $oldIndex should be manually removed if not needed anymore.\n" );
802 }
803
804 return true;
805 }
806
807 // Third requirement: the old index must exist
808 if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) {
809 $this->output( "...skipping: index $oldIndex doesn't exist.\n" );
810
811 return true;
812 }
813
814 // Requirements have been satisfied, patch can be applied
815 return $this->applyPatch(
816 $patch,
817 $fullpath,
818 "Renaming index $oldIndex into $newIndex to table $table"
819 );
820 }
821
822 /**
823 * If the specified table exists, drop it, or execute the
824 * patch if one is provided.
825 *
826 * Public @since 1.20
827 *
828 * @param $table string
829 * @param $patch string|false
830 * @param $fullpath bool
831 * @return Boolean false if this was skipped because schema changes are skipped
832 */
833 public function dropTable( $table, $patch = false, $fullpath = false ) {
834 if ( !$this->doTable( $table ) ) {
835 return true;
836 }
837
838 if ( $this->db->tableExists( $table, __METHOD__ ) ) {
839 $msg = "Dropping table $table";
840
841 if ( $patch === false ) {
842 $this->output( "$msg ..." );
843 $this->db->dropTable( $table, __METHOD__ );
844 $this->output( "done.\n" );
845 } else {
846 return $this->applyPatch( $patch, $fullpath, $msg );
847 }
848 } else {
849 $this->output( "...$table doesn't exist.\n" );
850 }
851
852 return true;
853 }
854
855 /**
856 * Modify an existing field
857 *
858 * @param string $table name of the table to which the field belongs
859 * @param string $field name of the field to modify
860 * @param string $patch path to the patch file
861 * @param $fullpath Boolean: whether to treat $patch path as a relative or not
862 * @return Boolean false if this was skipped because schema changes are skipped
863 */
864 public function modifyField( $table, $field, $patch, $fullpath = false ) {
865 if ( !$this->doTable( $table ) ) {
866 return true;
867 }
868
869 $updateKey = "$table-$field-$patch";
870 if ( !$this->db->tableExists( $table, __METHOD__ ) ) {
871 $this->output( "...$table table does not exist, skipping modify field patch.\n" );
872 } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) {
873 $this->output( "...$field field does not exist in $table table, " .
874 "skipping modify field patch.\n" );
875 } elseif ( $this->updateRowExists( $updateKey ) ) {
876 $this->output( "...$field in table $table already modified by patch $patch.\n" );
877 } else {
878 $this->insertUpdateRow( $updateKey );
879
880 return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" );
881 }
882
883 return true;
884 }
885
886 /**
887 * Purge the objectcache table
888 */
889 public function purgeCache() {
890 global $wgLocalisationCacheConf;
891 # We can't guarantee that the user will be able to use TRUNCATE,
892 # but we know that DELETE is available to us
893 $this->output( "Purging caches..." );
894 $this->db->delete( 'objectcache', '*', __METHOD__ );
895 if ( $wgLocalisationCacheConf['manualRecache'] ) {
896 $this->rebuildLocalisationCache();
897 }
898 MessageBlobStore::clear();
899 $this->output( "done.\n" );
900 }
901
902 /**
903 * Check the site_stats table is not properly populated.
904 */
905 protected function checkStats() {
906 $this->output( "...site_stats is populated..." );
907 $row = $this->db->selectRow( 'site_stats', '*', array( 'ss_row_id' => 1 ), __METHOD__ );
908 if ( $row === false ) {
909 $this->output( "data is missing! rebuilding...\n" );
910 } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) {
911 $this->output( "missing ss_total_pages, rebuilding...\n" );
912 } else {
913 $this->output( "done.\n" );
914
915 return;
916 }
917 SiteStatsInit::doAllAndCommit( $this->db );
918 }
919
920 # Common updater functions
921
922 /**
923 * Sets the number of active users in the site_stats table
924 */
925 protected function doActiveUsersInit() {
926 $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ );
927 if ( $activeUsers == -1 ) {
928 $activeUsers = $this->db->selectField( 'recentchanges',
929 'COUNT( DISTINCT rc_user_text )',
930 array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ), __METHOD__
931 );
932 $this->db->update( 'site_stats',
933 array( 'ss_active_users' => intval( $activeUsers ) ),
934 array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 )
935 );
936 }
937 $this->output( "...ss_active_users user count set...\n" );
938 }
939
940 /**
941 * Populates the log_user_text field in the logging table
942 */
943 protected function doLogUsertextPopulation() {
944 if ( !$this->updateRowExists( 'populate log_usertext' ) ) {
945 $this->output(
946 "Populating log_user_text field, printing progress markers. For large\n" .
947 "databases, you may want to hit Ctrl-C and do this manually with\n" .
948 "maintenance/populateLogUsertext.php.\n"
949 );
950
951 $task = $this->maintenance->runChild( 'PopulateLogUsertext' );
952 $task->execute();
953 $this->output( "done.\n" );
954 }
955 }
956
957 /**
958 * Migrate log params to new table and index for searching
959 */
960 protected function doLogSearchPopulation() {
961 if ( !$this->updateRowExists( 'populate log_search' ) ) {
962 $this->output(
963 "Populating log_search table, printing progress markers. For large\n" .
964 "databases, you may want to hit Ctrl-C and do this manually with\n" .
965 "maintenance/populateLogSearch.php.\n" );
966
967 $task = $this->maintenance->runChild( 'PopulateLogSearch' );
968 $task->execute();
969 $this->output( "done.\n" );
970 }
971 }
972
973 /**
974 * Updates the timestamps in the transcache table
975 */
976 protected function doUpdateTranscacheField() {
977 if ( $this->updateRowExists( 'convert transcache field' ) ) {
978 $this->output( "...transcache tc_time already converted.\n" );
979
980 return true;
981 }
982
983 return $this->applyPatch( 'patch-tc-timestamp.sql', false,
984 "Converting tc_time from UNIX epoch to MediaWiki timestamp" );
985 }
986
987 /**
988 * Update CategoryLinks collation
989 */
990 protected function doCollationUpdate() {
991 global $wgCategoryCollation;
992 if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) {
993 if ( $this->db->selectField(
994 'categorylinks',
995 'COUNT(*)',
996 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ),
997 __METHOD__
998 ) == 0
999 ) {
1000 $this->output( "...collations up-to-date.\n" );
1001
1002 return;
1003 }
1004
1005 $this->output( "Updating category collations..." );
1006 $task = $this->maintenance->runChild( 'UpdateCollation' );
1007 $task->execute();
1008 $this->output( "...done.\n" );
1009 }
1010 }
1011
1012 /**
1013 * Migrates user options from the user table blob to user_properties
1014 */
1015 protected function doMigrateUserOptions() {
1016 if ( $this->db->tableExists( 'user_properties' ) ) {
1017 $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' );
1018 $cl->execute();
1019 $this->output( "done.\n" );
1020 }
1021 }
1022
1023 /**
1024 * Rebuilds the localisation cache
1025 */
1026 protected function rebuildLocalisationCache() {
1027 /**
1028 * @var $cl RebuildLocalisationCache
1029 */
1030 $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' );
1031 $this->output( "Rebuilding localisation cache...\n" );
1032 $cl->setForce();
1033 $cl->execute();
1034 $this->output( "done.\n" );
1035 }
1036 }