DatabaseUpdater.php 42 KB

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