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