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