MediaWiki
REL1_22
|
00001 <?php 00024 require_once __DIR__ . '/../../maintenance/Maintenance.php'; 00025 00033 abstract class DatabaseUpdater { 00034 00040 protected $updates = array(); 00041 00047 protected $updatesSkipped = array(); 00048 00053 protected $extensionUpdates = array(); 00054 00060 protected $db; 00061 00062 protected $shared = false; 00063 00068 protected $postDatabaseUpdateMaintenance = array( 00069 'DeleteDefaultMessages', 00070 'PopulateRevisionLength', 00071 'PopulateRevisionSha1', 00072 'PopulateImageSha1', 00073 'FixExtLinksProtocolRelative', 00074 'PopulateFilearchiveSha1', 00075 ); 00076 00082 protected $fileHandle = null; 00083 00089 protected $skipSchema = false; 00090 00098 protected function __construct( DatabaseBase &$db, $shared, Maintenance $maintenance = null ) { 00099 $this->db = $db; 00100 $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files 00101 $this->shared = $shared; 00102 if ( $maintenance ) { 00103 $this->maintenance = $maintenance; 00104 $this->fileHandle = $maintenance->fileHandle; 00105 } else { 00106 $this->maintenance = new FakeMaintenance; 00107 } 00108 $this->maintenance->setDB( $db ); 00109 $this->initOldGlobals(); 00110 $this->loadExtensions(); 00111 wfRunHooks( 'LoadExtensionSchemaUpdates', array( $this ) ); 00112 } 00113 00118 private function initOldGlobals() { 00119 global $wgExtNewTables, $wgExtNewFields, $wgExtPGNewFields, 00120 $wgExtPGAlteredFields, $wgExtNewIndexes, $wgExtModifiedFields; 00121 00122 # For extensions only, should be populated via hooks 00123 # $wgDBtype should be checked to specifiy the proper file 00124 $wgExtNewTables = array(); // table, dir 00125 $wgExtNewFields = array(); // table, column, dir 00126 $wgExtPGNewFields = array(); // table, column, column attributes; for PostgreSQL 00127 $wgExtPGAlteredFields = array(); // table, column, new type, conversion method; for PostgreSQL 00128 $wgExtNewIndexes = array(); // table, index, dir 00129 $wgExtModifiedFields = array(); // table, index, dir 00130 } 00131 00136 private function loadExtensions() { 00137 if ( !defined( 'MEDIAWIKI_INSTALL' ) ) { 00138 return; // already loaded 00139 } 00140 $vars = Installer::getExistingLocalSettings(); 00141 if ( !$vars ) { 00142 return; // no LocalSettings found 00143 } 00144 if ( !isset( $vars['wgHooks'] ) || !isset( $vars['wgHooks']['LoadExtensionSchemaUpdates'] ) ) { 00145 return; 00146 } 00147 global $wgHooks, $wgAutoloadClasses; 00148 $wgHooks['LoadExtensionSchemaUpdates'] = $vars['wgHooks']['LoadExtensionSchemaUpdates']; 00149 $wgAutoloadClasses = $wgAutoloadClasses + $vars['wgAutoloadClasses']; 00150 } 00151 00159 public static function newForDB( &$db, $shared = false, $maintenance = null ) { 00160 $type = $db->getType(); 00161 if ( in_array( $type, Installer::getDBTypes() ) ) { 00162 $class = ucfirst( $type ) . 'Updater'; 00163 00164 return new $class( $db, $shared, $maintenance ); 00165 } else { 00166 throw new MWException( __METHOD__ . ' called for unsupported $wgDBtype' ); 00167 } 00168 } 00169 00175 public function getDB() { 00176 return $this->db; 00177 } 00178 00184 public function output( $str ) { 00185 if ( $this->maintenance->isQuiet() ) { 00186 return; 00187 } 00188 global $wgCommandLineMode; 00189 if ( !$wgCommandLineMode ) { 00190 $str = htmlspecialchars( $str ); 00191 } 00192 echo $str; 00193 flush(); 00194 } 00195 00209 public function addExtensionUpdate( array $update ) { 00210 $this->extensionUpdates[] = $update; 00211 } 00212 00222 public function addExtensionTable( $tableName, $sqlPath ) { 00223 $this->extensionUpdates[] = array( 'addTable', $tableName, $sqlPath, true ); 00224 } 00225 00233 public function addExtensionIndex( $tableName, $indexName, $sqlPath ) { 00234 $this->extensionUpdates[] = array( 'addIndex', $tableName, $indexName, $sqlPath, true ); 00235 } 00236 00245 public function addExtensionField( $tableName, $columnName, $sqlPath ) { 00246 $this->extensionUpdates[] = array( 'addField', $tableName, $columnName, $sqlPath, true ); 00247 } 00248 00257 public function dropExtensionField( $tableName, $columnName, $sqlPath ) { 00258 $this->extensionUpdates[] = array( 'dropField', $tableName, $columnName, $sqlPath, true ); 00259 } 00260 00270 public function dropExtensionIndex( $tableName, $indexName, $sqlPath ) { 00271 $this->extensionUpdates[] = array( 'dropIndex', $tableName, $indexName, $sqlPath, true ); 00272 } 00273 00281 public function dropExtensionTable( $tableName, $sqlPath ) { 00282 $this->extensionUpdates[] = array( 'dropTable', $tableName, $sqlPath, true ); 00283 } 00284 00297 public function renameExtensionIndex( $tableName, $oldIndexName, $newIndexName, 00298 $sqlPath, $skipBothIndexExistWarning = false 00299 ) { 00300 $this->extensionUpdates[] = array( 00301 'renameIndex', 00302 $tableName, 00303 $oldIndexName, 00304 $newIndexName, 00305 $skipBothIndexExistWarning, 00306 $sqlPath, 00307 true 00308 ); 00309 } 00310 00318 public function modifyExtensionField( $tableName, $fieldName, $sqlPath ) { 00319 $this->extensionUpdates[] = array( 'modifyField', $tableName, $fieldName, $sqlPath, true ); 00320 } 00321 00329 public function tableExists( $tableName ) { 00330 return ( $this->db->tableExists( $tableName, __METHOD__ ) ); 00331 } 00332 00342 public function addPostDatabaseUpdateMaintenance( $class ) { 00343 $this->postDatabaseUpdateMaintenance[] = $class; 00344 } 00345 00351 protected function getExtensionUpdates() { 00352 return $this->extensionUpdates; 00353 } 00354 00360 public function getPostDatabaseUpdateMaintenance() { 00361 return $this->postDatabaseUpdateMaintenance; 00362 } 00363 00369 private function writeSchemaUpdateFile( $schemaUpdate = array() ) { 00370 $updates = $this->updatesSkipped; 00371 $this->updatesSkipped = array(); 00372 00373 foreach ( $updates as $funcList ) { 00374 $func = $funcList[0]; 00375 $arg = $funcList[1]; 00376 $origParams = $funcList[2]; 00377 call_user_func_array( $func, $arg ); 00378 flush(); 00379 $this->updatesSkipped[] = $origParams; 00380 } 00381 } 00382 00388 public function doUpdates( $what = array( 'core', 'extensions', 'stats' ) ) { 00389 global $wgVersion; 00390 00391 $this->db->begin( __METHOD__ ); 00392 $what = array_flip( $what ); 00393 $this->skipSchema = isset( $what['noschema'] ) || $this->fileHandle !== null; 00394 if ( isset( $what['core'] ) ) { 00395 $this->runUpdates( $this->getCoreUpdateList(), false ); 00396 } 00397 if ( isset( $what['extensions'] ) ) { 00398 $this->runUpdates( $this->getOldGlobalUpdates(), false ); 00399 $this->runUpdates( $this->getExtensionUpdates(), true ); 00400 } 00401 00402 if ( isset( $what['stats'] ) ) { 00403 $this->checkStats(); 00404 } 00405 00406 $this->setAppliedUpdates( $wgVersion, $this->updates ); 00407 00408 if ( $this->fileHandle ) { 00409 $this->skipSchema = false; 00410 $this->writeSchemaUpdateFile(); 00411 $this->setAppliedUpdates( "$wgVersion-schema", $this->updatesSkipped ); 00412 } 00413 00414 $this->db->commit( __METHOD__ ); 00415 } 00416 00424 private function runUpdates( array $updates, $passSelf ) { 00425 $updatesDone = array(); 00426 $updatesSkipped = array(); 00427 foreach ( $updates as $params ) { 00428 $origParams = $params; 00429 $func = array_shift( $params ); 00430 if ( !is_array( $func ) && method_exists( $this, $func ) ) { 00431 $func = array( $this, $func ); 00432 } elseif ( $passSelf ) { 00433 array_unshift( $params, $this ); 00434 } 00435 $ret = call_user_func_array( $func, $params ); 00436 flush(); 00437 if ( $ret !== false ) { 00438 $updatesDone[] = $origParams; 00439 } else { 00440 $updatesSkipped[] = array( $func, $params, $origParams ); 00441 } 00442 } 00443 $this->updatesSkipped = array_merge( $this->updatesSkipped, $updatesSkipped ); 00444 $this->updates = array_merge( $this->updates, $updatesDone ); 00445 } 00446 00451 protected function setAppliedUpdates( $version, $updates = array() ) { 00452 $this->db->clearFlag( DBO_DDLMODE ); 00453 if ( !$this->canUseNewUpdatelog() ) { 00454 return; 00455 } 00456 $key = "updatelist-$version-" . time(); 00457 $this->db->insert( 'updatelog', 00458 array( 'ul_key' => $key, 'ul_value' => serialize( $updates ) ), 00459 __METHOD__ ); 00460 $this->db->setFlag( DBO_DDLMODE ); 00461 } 00462 00471 public function updateRowExists( $key ) { 00472 $row = $this->db->selectRow( 00473 'updatelog', 00474 '1', 00475 array( 'ul_key' => $key ), 00476 __METHOD__ 00477 ); 00478 00479 return (bool)$row; 00480 } 00481 00489 public function insertUpdateRow( $key, $val = null ) { 00490 $this->db->clearFlag( DBO_DDLMODE ); 00491 $values = array( 'ul_key' => $key ); 00492 if ( $val && $this->canUseNewUpdatelog() ) { 00493 $values['ul_value'] = $val; 00494 } 00495 $this->db->insert( 'updatelog', $values, __METHOD__, 'IGNORE' ); 00496 $this->db->setFlag( DBO_DDLMODE ); 00497 } 00498 00507 protected function canUseNewUpdatelog() { 00508 return $this->db->tableExists( 'updatelog', __METHOD__ ) && 00509 $this->db->fieldExists( 'updatelog', 'ul_value', __METHOD__ ); 00510 } 00511 00520 protected function doTable( $name ) { 00521 global $wgSharedDB, $wgSharedTables; 00522 00523 // Don't bother to check $wgSharedTables if there isn't a shared database 00524 // or the user actually also wants to do updates on the shared database. 00525 if ( $wgSharedDB === null || $this->shared ) { 00526 return true; 00527 } 00528 00529 return !in_array( $name, $wgSharedTables ); 00530 } 00531 00540 protected function getOldGlobalUpdates() { 00541 global $wgExtNewFields, $wgExtNewTables, $wgExtModifiedFields, 00542 $wgExtNewIndexes; 00543 00544 $updates = array(); 00545 00546 foreach ( $wgExtNewTables as $tableRecord ) { 00547 $updates[] = array( 00548 'addTable', $tableRecord[0], $tableRecord[1], true 00549 ); 00550 } 00551 00552 foreach ( $wgExtNewFields as $fieldRecord ) { 00553 $updates[] = array( 00554 'addField', $fieldRecord[0], $fieldRecord[1], 00555 $fieldRecord[2], true 00556 ); 00557 } 00558 00559 foreach ( $wgExtNewIndexes as $fieldRecord ) { 00560 $updates[] = array( 00561 'addIndex', $fieldRecord[0], $fieldRecord[1], 00562 $fieldRecord[2], true 00563 ); 00564 } 00565 00566 foreach ( $wgExtModifiedFields as $fieldRecord ) { 00567 $updates[] = array( 00568 'modifyField', $fieldRecord[0], $fieldRecord[1], 00569 $fieldRecord[2], true 00570 ); 00571 } 00572 00573 return $updates; 00574 } 00575 00584 abstract protected function getCoreUpdateList(); 00585 00591 public function copyFile( $filename ) { 00592 $this->db->sourceFile( $filename, false, false, false, 00593 array( $this, 'appendLine' ) 00594 ); 00595 } 00596 00607 public function appendLine( $line ) { 00608 $line = rtrim( $line ) . ";\n"; 00609 if ( fwrite( $this->fileHandle, $line ) === false ) { 00610 throw new MWException( "trouble writing file" ); 00611 } 00612 00613 return false; 00614 } 00615 00624 protected function applyPatch( $path, $isFullPath = false, $msg = null ) { 00625 if ( $msg === null ) { 00626 $msg = "Applying $path patch"; 00627 } 00628 if ( $this->skipSchema ) { 00629 $this->output( "...skipping schema change ($msg).\n" ); 00630 00631 return false; 00632 } 00633 00634 $this->output( "$msg ..." ); 00635 00636 if ( !$isFullPath ) { 00637 $path = $this->db->patchPath( $path ); 00638 } 00639 if ( $this->fileHandle !== null ) { 00640 $this->copyFile( $path ); 00641 } else { 00642 $this->db->sourceFile( $path ); 00643 } 00644 $this->output( "done.\n" ); 00645 00646 return true; 00647 } 00648 00657 protected function addTable( $name, $patch, $fullpath = false ) { 00658 if ( !$this->doTable( $name ) ) { 00659 return true; 00660 } 00661 00662 if ( $this->db->tableExists( $name, __METHOD__ ) ) { 00663 $this->output( "...$name table already exists.\n" ); 00664 } else { 00665 return $this->applyPatch( $patch, $fullpath, "Creating $name table" ); 00666 } 00667 00668 return true; 00669 } 00670 00680 protected function addField( $table, $field, $patch, $fullpath = false ) { 00681 if ( !$this->doTable( $table ) ) { 00682 return true; 00683 } 00684 00685 if ( !$this->db->tableExists( $table, __METHOD__ ) ) { 00686 $this->output( "...$table table does not exist, skipping new field patch.\n" ); 00687 } elseif ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { 00688 $this->output( "...have $field field in $table table.\n" ); 00689 } else { 00690 return $this->applyPatch( $patch, $fullpath, "Adding $field field to table $table" ); 00691 } 00692 00693 return true; 00694 } 00695 00705 protected function addIndex( $table, $index, $patch, $fullpath = false ) { 00706 if ( !$this->doTable( $table ) ) { 00707 return true; 00708 } 00709 00710 if ( !$this->db->tableExists( $table, __METHOD__ ) ) { 00711 $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); 00712 } elseif ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { 00713 $this->output( "...index $index already set on $table table.\n" ); 00714 } else { 00715 return $this->applyPatch( $patch, $fullpath, "Adding index $index to table $table" ); 00716 } 00717 00718 return true; 00719 } 00720 00730 protected function dropField( $table, $field, $patch, $fullpath = false ) { 00731 if ( !$this->doTable( $table ) ) { 00732 return true; 00733 } 00734 00735 if ( $this->db->fieldExists( $table, $field, __METHOD__ ) ) { 00736 return $this->applyPatch( $patch, $fullpath, "Table $table contains $field field. Dropping" ); 00737 } else { 00738 $this->output( "...$table table does not contain $field field.\n" ); 00739 } 00740 00741 return true; 00742 } 00743 00753 protected function dropIndex( $table, $index, $patch, $fullpath = false ) { 00754 if ( !$this->doTable( $table ) ) { 00755 return true; 00756 } 00757 00758 if ( $this->db->indexExists( $table, $index, __METHOD__ ) ) { 00759 return $this->applyPatch( $patch, $fullpath, "Dropping $index index from table $table" ); 00760 } else { 00761 $this->output( "...$index key doesn't exist.\n" ); 00762 } 00763 00764 return true; 00765 } 00766 00779 protected function renameIndex( $table, $oldIndex, $newIndex, 00780 $skipBothIndexExistWarning, $patch, $fullpath = false 00781 ) { 00782 if ( !$this->doTable( $table ) ) { 00783 return true; 00784 } 00785 00786 // First requirement: the table must exist 00787 if ( !$this->db->tableExists( $table, __METHOD__ ) ) { 00788 $this->output( "...skipping: '$table' table doesn't exist yet.\n" ); 00789 00790 return true; 00791 } 00792 00793 // Second requirement: the new index must be missing 00794 if ( $this->db->indexExists( $table, $newIndex, __METHOD__ ) ) { 00795 $this->output( "...index $newIndex already set on $table table.\n" ); 00796 if ( !$skipBothIndexExistWarning && 00797 $this->db->indexExists( $table, $oldIndex, __METHOD__ ) 00798 ) { 00799 $this->output( "...WARNING: $oldIndex still exists, despite it has " . 00800 "been renamed into $newIndex (which also exists).\n" . 00801 " $oldIndex should be manually removed if not needed anymore.\n" ); 00802 } 00803 00804 return true; 00805 } 00806 00807 // Third requirement: the old index must exist 00808 if ( !$this->db->indexExists( $table, $oldIndex, __METHOD__ ) ) { 00809 $this->output( "...skipping: index $oldIndex doesn't exist.\n" ); 00810 00811 return true; 00812 } 00813 00814 // Requirements have been satisfied, patch can be applied 00815 return $this->applyPatch( 00816 $patch, 00817 $fullpath, 00818 "Renaming index $oldIndex into $newIndex to table $table" 00819 ); 00820 } 00821 00833 public function dropTable( $table, $patch = false, $fullpath = false ) { 00834 if ( !$this->doTable( $table ) ) { 00835 return true; 00836 } 00837 00838 if ( $this->db->tableExists( $table, __METHOD__ ) ) { 00839 $msg = "Dropping table $table"; 00840 00841 if ( $patch === false ) { 00842 $this->output( "$msg ..." ); 00843 $this->db->dropTable( $table, __METHOD__ ); 00844 $this->output( "done.\n" ); 00845 } else { 00846 return $this->applyPatch( $patch, $fullpath, $msg ); 00847 } 00848 } else { 00849 $this->output( "...$table doesn't exist.\n" ); 00850 } 00851 00852 return true; 00853 } 00854 00864 public function modifyField( $table, $field, $patch, $fullpath = false ) { 00865 if ( !$this->doTable( $table ) ) { 00866 return true; 00867 } 00868 00869 $updateKey = "$table-$field-$patch"; 00870 if ( !$this->db->tableExists( $table, __METHOD__ ) ) { 00871 $this->output( "...$table table does not exist, skipping modify field patch.\n" ); 00872 } elseif ( !$this->db->fieldExists( $table, $field, __METHOD__ ) ) { 00873 $this->output( "...$field field does not exist in $table table, " . 00874 "skipping modify field patch.\n" ); 00875 } elseif ( $this->updateRowExists( $updateKey ) ) { 00876 $this->output( "...$field in table $table already modified by patch $patch.\n" ); 00877 } else { 00878 $this->insertUpdateRow( $updateKey ); 00879 00880 return $this->applyPatch( $patch, $fullpath, "Modifying $field field of table $table" ); 00881 } 00882 00883 return true; 00884 } 00885 00889 public function purgeCache() { 00890 global $wgLocalisationCacheConf; 00891 # We can't guarantee that the user will be able to use TRUNCATE, 00892 # but we know that DELETE is available to us 00893 $this->output( "Purging caches..." ); 00894 $this->db->delete( 'objectcache', '*', __METHOD__ ); 00895 if ( $wgLocalisationCacheConf['manualRecache'] ) { 00896 $this->rebuildLocalisationCache(); 00897 } 00898 MessageBlobStore::clear(); 00899 $this->output( "done.\n" ); 00900 } 00901 00905 protected function checkStats() { 00906 $this->output( "...site_stats is populated..." ); 00907 $row = $this->db->selectRow( 'site_stats', '*', array( 'ss_row_id' => 1 ), __METHOD__ ); 00908 if ( $row === false ) { 00909 $this->output( "data is missing! rebuilding...\n" ); 00910 } elseif ( isset( $row->site_stats ) && $row->ss_total_pages == -1 ) { 00911 $this->output( "missing ss_total_pages, rebuilding...\n" ); 00912 } else { 00913 $this->output( "done.\n" ); 00914 00915 return; 00916 } 00917 SiteStatsInit::doAllAndCommit( $this->db ); 00918 } 00919 00920 # Common updater functions 00921 00925 protected function doActiveUsersInit() { 00926 $activeUsers = $this->db->selectField( 'site_stats', 'ss_active_users', false, __METHOD__ ); 00927 if ( $activeUsers == -1 ) { 00928 $activeUsers = $this->db->selectField( 'recentchanges', 00929 'COUNT( DISTINCT rc_user_text )', 00930 array( 'rc_user != 0', 'rc_bot' => 0, "rc_log_type != 'newusers'" ), __METHOD__ 00931 ); 00932 $this->db->update( 'site_stats', 00933 array( 'ss_active_users' => intval( $activeUsers ) ), 00934 array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 ) 00935 ); 00936 } 00937 $this->output( "...ss_active_users user count set...\n" ); 00938 } 00939 00943 protected function doLogUsertextPopulation() { 00944 if ( !$this->updateRowExists( 'populate log_usertext' ) ) { 00945 $this->output( 00946 "Populating log_user_text field, printing progress markers. For large\n" . 00947 "databases, you may want to hit Ctrl-C and do this manually with\n" . 00948 "maintenance/populateLogUsertext.php.\n" 00949 ); 00950 00951 $task = $this->maintenance->runChild( 'PopulateLogUsertext' ); 00952 $task->execute(); 00953 $this->output( "done.\n" ); 00954 } 00955 } 00956 00960 protected function doLogSearchPopulation() { 00961 if ( !$this->updateRowExists( 'populate log_search' ) ) { 00962 $this->output( 00963 "Populating log_search table, printing progress markers. For large\n" . 00964 "databases, you may want to hit Ctrl-C and do this manually with\n" . 00965 "maintenance/populateLogSearch.php.\n" ); 00966 00967 $task = $this->maintenance->runChild( 'PopulateLogSearch' ); 00968 $task->execute(); 00969 $this->output( "done.\n" ); 00970 } 00971 } 00972 00976 protected function doUpdateTranscacheField() { 00977 if ( $this->updateRowExists( 'convert transcache field' ) ) { 00978 $this->output( "...transcache tc_time already converted.\n" ); 00979 00980 return true; 00981 } 00982 00983 return $this->applyPatch( 'patch-tc-timestamp.sql', false, 00984 "Converting tc_time from UNIX epoch to MediaWiki timestamp" ); 00985 } 00986 00990 protected function doCollationUpdate() { 00991 global $wgCategoryCollation; 00992 if ( $this->db->fieldExists( 'categorylinks', 'cl_collation', __METHOD__ ) ) { 00993 if ( $this->db->selectField( 00994 'categorylinks', 00995 'COUNT(*)', 00996 'cl_collation != ' . $this->db->addQuotes( $wgCategoryCollation ), 00997 __METHOD__ 00998 ) == 0 00999 ) { 01000 $this->output( "...collations up-to-date.\n" ); 01001 01002 return; 01003 } 01004 01005 $this->output( "Updating category collations..." ); 01006 $task = $this->maintenance->runChild( 'UpdateCollation' ); 01007 $task->execute(); 01008 $this->output( "...done.\n" ); 01009 } 01010 } 01011 01015 protected function doMigrateUserOptions() { 01016 if ( $this->db->tableExists( 'user_properties' ) ) { 01017 $cl = $this->maintenance->runChild( 'ConvertUserOptions', 'convertUserOptions.php' ); 01018 $cl->execute(); 01019 $this->output( "done.\n" ); 01020 } 01021 } 01022 01026 protected function rebuildLocalisationCache() { 01030 $cl = $this->maintenance->runChild( 'RebuildLocalisationCache', 'rebuildLocalisationCache.php' ); 01031 $this->output( "Rebuilding localisation cache...\n" ); 01032 $cl->setForce(); 01033 $cl->execute(); 01034 $this->output( "done.\n" ); 01035 } 01036 }