DatabaseInstaller.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. <?php
  2. /**
  3. * DBMS-specific installation helper.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Deployment
  22. */
  23. use MediaWiki\MediaWikiServices;
  24. use Wikimedia\Rdbms\LBFactorySingle;
  25. use Wikimedia\Rdbms\Database;
  26. use Wikimedia\Rdbms\IDatabase;
  27. use Wikimedia\Rdbms\DBExpectedError;
  28. use Wikimedia\Rdbms\DBConnectionError;
  29. /**
  30. * Base class for DBMS-specific installation helper classes.
  31. *
  32. * @ingroup Deployment
  33. * @since 1.17
  34. */
  35. abstract class DatabaseInstaller {
  36. /**
  37. * The Installer object.
  38. *
  39. * @var WebInstaller
  40. */
  41. public $parent;
  42. /**
  43. * @var string Set by subclasses
  44. */
  45. public static $minimumVersion;
  46. /**
  47. * @var string Set by subclasses
  48. */
  49. protected static $notMinimumVersionMessage;
  50. /**
  51. * The database connection.
  52. *
  53. * @var Database
  54. */
  55. public $db = null;
  56. /**
  57. * Internal variables for installation.
  58. *
  59. * @var array
  60. */
  61. protected $internalDefaults = [];
  62. /**
  63. * Array of MW configuration globals this class uses.
  64. *
  65. * @var array
  66. */
  67. protected $globalNames = [];
  68. /**
  69. * Whether the provided version meets the necessary requirements for this type
  70. *
  71. * @param string $serverVersion Output of Database::getServerVersion()
  72. * @return Status
  73. * @since 1.30
  74. */
  75. public static function meetsMinimumRequirement( $serverVersion ) {
  76. if ( version_compare( $serverVersion, static::$minimumVersion ) < 0 ) {
  77. return Status::newFatal(
  78. static::$notMinimumVersionMessage, static::$minimumVersion, $serverVersion
  79. );
  80. }
  81. return Status::newGood();
  82. }
  83. /**
  84. * Return the internal name, e.g. 'mysql', or 'sqlite'.
  85. */
  86. abstract public function getName();
  87. /**
  88. * @return bool Returns true if the client library is compiled in.
  89. */
  90. abstract public function isCompiled();
  91. /**
  92. * Checks for installation prerequisites other than those checked by isCompiled()
  93. * @since 1.19
  94. * @return Status
  95. */
  96. public function checkPrerequisites() {
  97. return Status::newGood();
  98. }
  99. /**
  100. * Get HTML for a web form that configures this database. Configuration
  101. * at this time should be the minimum needed to connect and test
  102. * whether install or upgrade is required.
  103. *
  104. * If this is called, $this->parent can be assumed to be a WebInstaller.
  105. */
  106. abstract public function getConnectForm();
  107. /**
  108. * Set variables based on the request array, assuming it was submitted
  109. * via the form returned by getConnectForm(). Validate the connection
  110. * settings by attempting to connect with them.
  111. *
  112. * If this is called, $this->parent can be assumed to be a WebInstaller.
  113. *
  114. * @return Status
  115. */
  116. abstract public function submitConnectForm();
  117. /**
  118. * Get HTML for a web form that retrieves settings used for installation.
  119. * $this->parent can be assumed to be a WebInstaller.
  120. * If the DB type has no settings beyond those already configured with
  121. * getConnectForm(), this should return false.
  122. * @return bool
  123. */
  124. public function getSettingsForm() {
  125. return false;
  126. }
  127. /**
  128. * Set variables based on the request array, assuming it was submitted via
  129. * the form return by getSettingsForm().
  130. *
  131. * @return Status
  132. */
  133. public function submitSettingsForm() {
  134. return Status::newGood();
  135. }
  136. /**
  137. * Open a connection to the database using the administrative user/password
  138. * currently defined in the session, without any caching. Returns a status
  139. * object. On success, the status object will contain a Database object in
  140. * its value member.
  141. *
  142. * @return Status
  143. */
  144. abstract public function openConnection();
  145. /**
  146. * Create the database and return a Status object indicating success or
  147. * failure.
  148. *
  149. * @return Status
  150. */
  151. abstract public function setupDatabase();
  152. /**
  153. * Connect to the database using the administrative user/password currently
  154. * defined in the session. Returns a status object. On success, the status
  155. * object will contain a Database object in its value member.
  156. *
  157. * This will return a cached connection if one is available.
  158. *
  159. * @return Status
  160. * @suppress PhanUndeclaredMethod
  161. */
  162. public function getConnection() {
  163. if ( $this->db ) {
  164. return Status::newGood( $this->db );
  165. }
  166. $status = $this->openConnection();
  167. if ( $status->isOK() ) {
  168. $this->db = $status->value;
  169. // Enable autocommit
  170. $this->db->clearFlag( DBO_TRX );
  171. $this->db->commit( __METHOD__ );
  172. }
  173. return $status;
  174. }
  175. /**
  176. * Apply a SQL source file to the database as part of running an installation step.
  177. *
  178. * @param string $sourceFileMethod
  179. * @param string $stepName
  180. * @param bool $archiveTableMustNotExist
  181. * @return Status
  182. */
  183. private function stepApplySourceFile(
  184. $sourceFileMethod,
  185. $stepName,
  186. $archiveTableMustNotExist = false
  187. ) {
  188. $status = $this->getConnection();
  189. if ( !$status->isOK() ) {
  190. return $status;
  191. }
  192. $this->db->selectDB( $this->getVar( 'wgDBname' ) );
  193. if ( $archiveTableMustNotExist && $this->db->tableExists( 'archive', __METHOD__ ) ) {
  194. $status->warning( "config-$stepName-tables-exist" );
  195. $this->enableLB();
  196. return $status;
  197. }
  198. $this->db->setFlag( DBO_DDLMODE ); // For Oracle's handling of schema files
  199. $this->db->begin( __METHOD__ );
  200. $error = $this->db->sourceFile(
  201. call_user_func( [ $this, $sourceFileMethod ], $this->db )
  202. );
  203. if ( $error !== true ) {
  204. $this->db->reportQueryError( $error, 0, '', __METHOD__ );
  205. $this->db->rollback( __METHOD__ );
  206. $status->fatal( "config-$stepName-tables-failed", $error );
  207. } else {
  208. $this->db->commit( __METHOD__ );
  209. }
  210. // Resume normal operations
  211. if ( $status->isOK() ) {
  212. $this->enableLB();
  213. }
  214. return $status;
  215. }
  216. /**
  217. * Create database tables from scratch.
  218. *
  219. * @return Status
  220. */
  221. public function createTables() {
  222. return $this->stepApplySourceFile( 'getSchemaPath', 'install', true );
  223. }
  224. /**
  225. * Insert update keys into table to prevent running unneded updates.
  226. *
  227. * @return Status
  228. */
  229. public function insertUpdateKeys() {
  230. return $this->stepApplySourceFile( 'getUpdateKeysPath', 'updates', false );
  231. }
  232. /**
  233. * Return a path to the DBMS-specific SQL file if it exists,
  234. * otherwise default SQL file
  235. *
  236. * @param IDatabase $db
  237. * @param string $filename
  238. * @return string
  239. */
  240. private function getSqlFilePath( $db, $filename ) {
  241. global $IP;
  242. $dbmsSpecificFilePath = "$IP/maintenance/" . $db->getType() . "/$filename";
  243. if ( file_exists( $dbmsSpecificFilePath ) ) {
  244. return $dbmsSpecificFilePath;
  245. } else {
  246. return "$IP/maintenance/$filename";
  247. }
  248. }
  249. /**
  250. * Return a path to the DBMS-specific schema file,
  251. * otherwise default to tables.sql
  252. *
  253. * @param IDatabase $db
  254. * @return string
  255. */
  256. public function getSchemaPath( $db ) {
  257. return $this->getSqlFilePath( $db, 'tables.sql' );
  258. }
  259. /**
  260. * Return a path to the DBMS-specific update key file,
  261. * otherwise default to update-keys.sql
  262. *
  263. * @param IDatabase $db
  264. * @return string
  265. */
  266. public function getUpdateKeysPath( $db ) {
  267. return $this->getSqlFilePath( $db, 'update-keys.sql' );
  268. }
  269. /**
  270. * Create the tables for each extension the user enabled
  271. * @return Status
  272. */
  273. public function createExtensionTables() {
  274. $status = $this->getConnection();
  275. if ( !$status->isOK() ) {
  276. return $status;
  277. }
  278. // Now run updates to create tables for old extensions
  279. DatabaseUpdater::newForDB( $this->db )->doUpdates( [ 'extensions' ] );
  280. return $status;
  281. }
  282. /**
  283. * Get the DBMS-specific options for LocalSettings.php generation.
  284. *
  285. * @return string
  286. */
  287. abstract public function getLocalSettings();
  288. /**
  289. * Override this to provide DBMS-specific schema variables, to be
  290. * substituted into tables.sql and other schema files.
  291. * @return array
  292. */
  293. public function getSchemaVars() {
  294. return [];
  295. }
  296. /**
  297. * Set appropriate schema variables in the current database connection.
  298. *
  299. * This should be called after any request data has been imported, but before
  300. * any write operations to the database.
  301. */
  302. public function setupSchemaVars() {
  303. $status = $this->getConnection();
  304. if ( $status->isOK() ) {
  305. // @phan-suppress-next-line PhanUndeclaredMethod
  306. $status->value->setSchemaVars( $this->getSchemaVars() );
  307. } else {
  308. $msg = __METHOD__ . ': unexpected error while establishing'
  309. . ' a database connection with message: '
  310. . $status->getMessage()->plain();
  311. throw new MWException( $msg );
  312. }
  313. }
  314. /**
  315. * Set up LBFactory so that wfGetDB() etc. works.
  316. * We set up a special LBFactory instance which returns the current
  317. * installer connection.
  318. */
  319. public function enableLB() {
  320. $status = $this->getConnection();
  321. if ( !$status->isOK() ) {
  322. throw new MWException( __METHOD__ . ': unexpected DB connection error' );
  323. }
  324. MediaWikiServices::resetGlobalInstance();
  325. $services = MediaWikiServices::getInstance();
  326. $connection = $status->value;
  327. $services->redefineService( 'DBLoadBalancerFactory', function () use ( $connection ) {
  328. return LBFactorySingle::newFromConnection( $connection );
  329. } );
  330. }
  331. /**
  332. * Perform database upgrades
  333. *
  334. * @suppress SecurityCheck-XSS Escaping provided by $this->outputHandler
  335. * @return bool
  336. */
  337. public function doUpgrade() {
  338. $this->setupSchemaVars();
  339. $this->enableLB();
  340. $ret = true;
  341. ob_start( [ $this, 'outputHandler' ] );
  342. $up = DatabaseUpdater::newForDB( $this->db );
  343. try {
  344. $up->doUpdates();
  345. $up->purgeCache();
  346. } catch ( MWException $e ) {
  347. echo "\nAn error occurred:\n";
  348. echo $e->getText();
  349. $ret = false;
  350. } catch ( Exception $e ) {
  351. echo "\nAn error occurred:\n";
  352. echo $e->getMessage();
  353. $ret = false;
  354. }
  355. ob_end_flush();
  356. return $ret;
  357. }
  358. /**
  359. * Allow DB installers a chance to make last-minute changes before installation
  360. * occurs. This happens before setupDatabase() or createTables() is called, but
  361. * long after the constructor. Helpful for things like modifying setup steps :)
  362. */
  363. public function preInstall() {
  364. }
  365. /**
  366. * Allow DB installers a chance to make checks before upgrade.
  367. */
  368. public function preUpgrade() {
  369. }
  370. /**
  371. * Get an array of MW configuration globals that will be configured by this class.
  372. * @return array
  373. */
  374. public function getGlobalNames() {
  375. return $this->globalNames;
  376. }
  377. /**
  378. * Construct and initialise parent.
  379. * This is typically only called from Installer::getDBInstaller()
  380. * @param WebInstaller $parent
  381. */
  382. public function __construct( $parent ) {
  383. $this->parent = $parent;
  384. }
  385. /**
  386. * Convenience function.
  387. * Check if a named extension is present.
  388. *
  389. * @param string $name
  390. * @return bool
  391. */
  392. protected static function checkExtension( $name ) {
  393. return extension_loaded( $name );
  394. }
  395. /**
  396. * Get the internationalised name for this DBMS.
  397. * @return string
  398. */
  399. public function getReadableName() {
  400. // Messages: config-type-mysql, config-type-postgres, config-type-sqlite
  401. return wfMessage( 'config-type-' . $this->getName() )->text();
  402. }
  403. /**
  404. * Get a name=>value map of MW configuration globals for the default values.
  405. * @return array
  406. */
  407. public function getGlobalDefaults() {
  408. $defaults = [];
  409. foreach ( $this->getGlobalNames() as $var ) {
  410. if ( isset( $GLOBALS[$var] ) ) {
  411. $defaults[$var] = $GLOBALS[$var];
  412. }
  413. }
  414. return $defaults;
  415. }
  416. /**
  417. * Get a name=>value map of internal variables used during installation.
  418. * @return array
  419. */
  420. public function getInternalDefaults() {
  421. return $this->internalDefaults;
  422. }
  423. /**
  424. * Get a variable, taking local defaults into account.
  425. * @param string $var
  426. * @param mixed|null $default
  427. * @return mixed
  428. */
  429. public function getVar( $var, $default = null ) {
  430. $defaults = $this->getGlobalDefaults();
  431. $internal = $this->getInternalDefaults();
  432. if ( isset( $defaults[$var] ) ) {
  433. $default = $defaults[$var];
  434. } elseif ( isset( $internal[$var] ) ) {
  435. $default = $internal[$var];
  436. }
  437. return $this->parent->getVar( $var, $default );
  438. }
  439. /**
  440. * Convenience alias for $this->parent->setVar()
  441. * @param string $name
  442. * @param mixed $value
  443. */
  444. public function setVar( $name, $value ) {
  445. $this->parent->setVar( $name, $value );
  446. }
  447. /**
  448. * Get a labelled text box to configure a local variable.
  449. *
  450. * @param string $var
  451. * @param string $label
  452. * @param array $attribs
  453. * @param string $helpData
  454. * @return string
  455. */
  456. public function getTextBox( $var, $label, $attribs = [], $helpData = "" ) {
  457. $name = $this->getName() . '_' . $var;
  458. $value = $this->getVar( $var );
  459. if ( !isset( $attribs ) ) {
  460. $attribs = [];
  461. }
  462. return $this->parent->getTextBox( [
  463. 'var' => $var,
  464. 'label' => $label,
  465. 'attribs' => $attribs,
  466. 'controlName' => $name,
  467. 'value' => $value,
  468. 'help' => $helpData
  469. ] );
  470. }
  471. /**
  472. * Get a labelled password box to configure a local variable.
  473. * Implements password hiding.
  474. *
  475. * @param string $var
  476. * @param string $label
  477. * @param array $attribs
  478. * @param string $helpData
  479. * @return string
  480. */
  481. public function getPasswordBox( $var, $label, $attribs = [], $helpData = "" ) {
  482. $name = $this->getName() . '_' . $var;
  483. $value = $this->getVar( $var );
  484. if ( !isset( $attribs ) ) {
  485. $attribs = [];
  486. }
  487. return $this->parent->getPasswordBox( [
  488. 'var' => $var,
  489. 'label' => $label,
  490. 'attribs' => $attribs,
  491. 'controlName' => $name,
  492. 'value' => $value,
  493. 'help' => $helpData
  494. ] );
  495. }
  496. /**
  497. * Get a labelled checkbox to configure a local boolean variable.
  498. *
  499. * @param string $var
  500. * @param string $label
  501. * @param array $attribs Optional.
  502. * @param string $helpData Optional.
  503. * @return string
  504. */
  505. public function getCheckBox( $var, $label, $attribs = [], $helpData = "" ) {
  506. $name = $this->getName() . '_' . $var;
  507. $value = $this->getVar( $var );
  508. return $this->parent->getCheckBox( [
  509. 'var' => $var,
  510. 'label' => $label,
  511. 'attribs' => $attribs,
  512. 'controlName' => $name,
  513. 'value' => $value,
  514. 'help' => $helpData
  515. ] );
  516. }
  517. /**
  518. * Get a set of labelled radio buttons.
  519. *
  520. * @param array $params Parameters are:
  521. * var: The variable to be configured (required)
  522. * label: The message name for the label (required)
  523. * itemLabelPrefix: The message name prefix for the item labels (required)
  524. * values: List of allowed values (required)
  525. * itemAttribs Array of attribute arrays, outer key is the value name (optional)
  526. *
  527. * @return string
  528. */
  529. public function getRadioSet( $params ) {
  530. $params['controlName'] = $this->getName() . '_' . $params['var'];
  531. $params['value'] = $this->getVar( $params['var'] );
  532. return $this->parent->getRadioSet( $params );
  533. }
  534. /**
  535. * Convenience function to set variables based on form data.
  536. * Assumes that variables containing "password" in the name are (potentially
  537. * fake) passwords.
  538. * @param array $varNames
  539. * @return array
  540. */
  541. public function setVarsFromRequest( $varNames ) {
  542. return $this->parent->setVarsFromRequest( $varNames, $this->getName() . '_' );
  543. }
  544. /**
  545. * Determine whether an existing installation of MediaWiki is present in
  546. * the configured administrative connection. Returns true if there is
  547. * such a wiki, false if the database doesn't exist.
  548. *
  549. * Traditionally, this is done by testing for the existence of either
  550. * the revision table or the cur table.
  551. *
  552. * @return bool
  553. */
  554. public function needsUpgrade() {
  555. $status = $this->getConnection();
  556. if ( !$status->isOK() ) {
  557. return false;
  558. }
  559. try {
  560. $this->db->selectDB( $this->getVar( 'wgDBname' ) );
  561. } catch ( DBConnectionError $e ) {
  562. // Don't catch DBConnectionError
  563. throw $e;
  564. } catch ( DBExpectedError $e ) {
  565. return false;
  566. }
  567. return $this->db->tableExists( 'cur', __METHOD__ ) ||
  568. $this->db->tableExists( 'revision', __METHOD__ );
  569. }
  570. /**
  571. * Get a standard install-user fieldset.
  572. *
  573. * @return string
  574. */
  575. public function getInstallUserBox() {
  576. return Html::openElement( 'fieldset' ) .
  577. Html::element( 'legend', [], wfMessage( 'config-db-install-account' )->text() ) .
  578. $this->getTextBox(
  579. '_InstallUser',
  580. 'config-db-username',
  581. [ 'dir' => 'ltr' ],
  582. $this->parent->getHelpBox( 'config-db-install-username' )
  583. ) .
  584. $this->getPasswordBox(
  585. '_InstallPassword',
  586. 'config-db-password',
  587. [ 'dir' => 'ltr' ],
  588. $this->parent->getHelpBox( 'config-db-install-password' )
  589. ) .
  590. Html::closeElement( 'fieldset' );
  591. }
  592. /**
  593. * Submit a standard install user fieldset.
  594. * @return Status
  595. */
  596. public function submitInstallUserBox() {
  597. $this->setVarsFromRequest( [ '_InstallUser', '_InstallPassword' ] );
  598. return Status::newGood();
  599. }
  600. /**
  601. * Get a standard web-user fieldset
  602. * @param string|bool $noCreateMsg Message to display instead of the creation checkbox.
  603. * Set this to false to show a creation checkbox (default).
  604. *
  605. * @return string
  606. */
  607. public function getWebUserBox( $noCreateMsg = false ) {
  608. $wrapperStyle = $this->getVar( '_SameAccount' ) ? 'display: none' : '';
  609. $s = Html::openElement( 'fieldset' ) .
  610. Html::element( 'legend', [], wfMessage( 'config-db-web-account' )->text() ) .
  611. $this->getCheckBox(
  612. '_SameAccount', 'config-db-web-account-same',
  613. [ 'class' => 'hideShowRadio', 'rel' => 'dbOtherAccount' ]
  614. ) .
  615. Html::openElement( 'div', [ 'id' => 'dbOtherAccount', 'style' => $wrapperStyle ] ) .
  616. $this->getTextBox( 'wgDBuser', 'config-db-username' ) .
  617. $this->getPasswordBox( 'wgDBpassword', 'config-db-password' ) .
  618. $this->parent->getHelpBox( 'config-db-web-help' );
  619. if ( $noCreateMsg ) {
  620. $s .= Html::warningBox( wfMessage( $noCreateMsg )->plain(), 'config-warning-box' );
  621. } else {
  622. $s .= $this->getCheckBox( '_CreateDBAccount', 'config-db-web-create' );
  623. }
  624. $s .= Html::closeElement( 'div' ) . Html::closeElement( 'fieldset' );
  625. return $s;
  626. }
  627. /**
  628. * Submit the form from getWebUserBox().
  629. *
  630. * @return Status
  631. */
  632. public function submitWebUserBox() {
  633. $this->setVarsFromRequest(
  634. [ 'wgDBuser', 'wgDBpassword', '_SameAccount', '_CreateDBAccount' ]
  635. );
  636. if ( $this->getVar( '_SameAccount' ) ) {
  637. $this->setVar( 'wgDBuser', $this->getVar( '_InstallUser' ) );
  638. $this->setVar( 'wgDBpassword', $this->getVar( '_InstallPassword' ) );
  639. }
  640. if ( $this->getVar( '_CreateDBAccount' ) && strval( $this->getVar( 'wgDBpassword' ) ) == '' ) {
  641. return Status::newFatal( 'config-db-password-empty', $this->getVar( 'wgDBuser' ) );
  642. }
  643. return Status::newGood();
  644. }
  645. /**
  646. * Common function for databases that don't understand the MySQLish syntax of interwiki.sql.
  647. *
  648. * @return Status
  649. */
  650. public function populateInterwikiTable() {
  651. $status = $this->getConnection();
  652. if ( !$status->isOK() ) {
  653. return $status;
  654. }
  655. $this->db->selectDB( $this->getVar( 'wgDBname' ) );
  656. if ( $this->db->selectRow( 'interwiki', '1', [], __METHOD__ ) ) {
  657. $status->warning( 'config-install-interwiki-exists' );
  658. return $status;
  659. }
  660. global $IP;
  661. Wikimedia\suppressWarnings();
  662. $rows = file( "$IP/maintenance/interwiki.list",
  663. FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
  664. Wikimedia\restoreWarnings();
  665. $interwikis = [];
  666. if ( !$rows ) {
  667. return Status::newFatal( 'config-install-interwiki-list' );
  668. }
  669. foreach ( $rows as $row ) {
  670. $row = preg_replace( '/^\s*([^#]*?)\s*(#.*)?$/', '\\1', $row ); // strip comments - whee
  671. if ( $row == "" ) {
  672. continue;
  673. }
  674. $row .= "|";
  675. $interwikis[] = array_combine(
  676. [ 'iw_prefix', 'iw_url', 'iw_local', 'iw_api', 'iw_wikiid' ],
  677. explode( '|', $row )
  678. );
  679. }
  680. $this->db->insert( 'interwiki', $interwikis, __METHOD__ );
  681. return Status::newGood();
  682. }
  683. public function outputHandler( $string ) {
  684. return htmlspecialchars( $string );
  685. }
  686. }