Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.20% covered (danger)
36.20%
59 / 163
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommandLineInstaller
36.20% covered (danger)
36.20%
59 / 163
11.11% covered (danger)
11.11%
1 / 9
353.18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
1
 canExecuteWithoutLocalSettings
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 finalSetup
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDbType
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 execute
5.33% covered (danger)
5.33%
4 / 75
0.00% covered (danger)
0.00%
0 / 1
262.18
 generateStrongPassword
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 setDbPassOption
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 setPassOption
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 validateParamsAndArgs
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * CLI-based MediaWiki installation and configuration.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Maintenance
8 */
9
10use MediaWiki\Installer\Installer;
11use MediaWiki\Installer\InstallerOverrides;
12use MediaWiki\Installer\InstallException;
13use MediaWiki\Maintenance\Maintenance;
14use MediaWiki\Settings\SettingsBuilder;
15
16// @codeCoverageIgnoreStart
17require_once __DIR__ . '/Maintenance.php';
18
19define( 'MW_CONFIG_CALLBACK', [ Installer::class, 'overrideConfig' ] );
20define( 'MEDIAWIKI_INSTALL', true );
21// @codeCoverageIgnoreEnd
22
23/**
24 * Maintenance script to install and configure MediaWiki
25 *
26 * Default values for the options are defined in MainConfigSchema.php
27 * (see the mapping in includes/installer/CliInstaller.php)
28 * Default for --dbpath (SQLite-specific) is defined in SqliteInstaller::getGlobalDefaults
29 *
30 * @ingroup Maintenance
31 */
32class CommandLineInstaller extends Maintenance {
33    public function __construct() {
34        parent::__construct();
35        $this->addDescription( "CLI-based MediaWiki installation and configuration.\n" .
36            "Default options are indicated in parentheses.\n" .
37            "If no options are provided, the script will run in interactive mode." );
38
39        $this->addArg( 'name', 'The name of the wiki', false );
40        $this->addArg( 'admin', 'The username of the wiki administrator.', false );
41
42        $this->addOption( 'pass', 'The password for the wiki administrator.', false, true );
43        $this->addOption(
44            'passfile',
45            'An alternative way to provide pass option, as the contents of this file',
46            false,
47            true
48        );
49        $this->addOption(
50            'scriptpath',
51            'The relative path of the wiki in the web server (/' . basename( dirname( __DIR__ ) ) . ')',
52            false,
53            true
54        );
55        $this->addOption(
56            'server',
57            'The base URL of the web server the wiki will be on (http://localhost)',
58            false,
59            true
60        );
61
62        $this->addOption( 'lang', 'The language to use (en)', false, true );
63
64        $this->addOption( 'dbtype', 'The type of database (mysql)', false, true );
65        $this->addOption( 'dbserver', 'The database host (localhost)', false, true );
66        $this->addOption( 'dbssl', 'Connect to the database over SSL' );
67        $this->addOption( 'dbport', 'The database port; only for PostgreSQL (5432)', false, true );
68        $this->addOption( 'dbname', 'The database name (my_wiki)', false, true );
69        $this->addOption( 'dbpath', 'The path for the SQLite DB (MW_INSTALL_PATH/data)', false, true );
70        $this->addOption( 'dbprefix', 'Optional database table name prefix', false, true );
71        $this->addOption( 'installdbuser', 'The user to use for installing (root)', false, true );
72        $this->addOption( 'installdbpass', 'The password for the DB user to install as.', false, true );
73        $this->addOption( 'dbuser', 'The user to use for normal operations (wikiuser)', false, true );
74        $this->addOption( 'dbpass', 'The password for the DB user for normal operations', false, true );
75        $this->addOption(
76            'dbpassfile',
77            'An alternative way to provide dbpass option, as the contents of this file',
78            false,
79            true
80        );
81        $this->addOption( 'confpath', 'Path to write LocalSettings.php to (' . MW_INSTALL_PATH . ')', false, true );
82        $this->addOption( 'dbschema', 'The schema for the MediaWiki DB in '
83            . 'PostgreSQL (mediawiki)', false, true );
84
85        $this->addOption( 'env-checks', "Run environment checks only, don't change anything" );
86
87        $this->addOption( 'with-extensions', "Detect and include extensions" );
88        $this->addOption( 'extensions', 'Comma-separated list of extensions to install',
89            false, true, false, true );
90        $this->addOption( 'skins', 'Comma-separated list of skins to install (default: all)',
91            false, true, false, true );
92        $this->addOption( 'with-developmentsettings', 'Load DevelopmentSettings.php in LocalSettings.php' );
93    }
94
95    public function canExecuteWithoutLocalSettings(): bool {
96        return true;
97    }
98
99    public function finalSetup( SettingsBuilder $settingsBuilder ) {
100        parent::finalSetup( $settingsBuilder );
101        Installer::overrideConfig( $settingsBuilder );
102    }
103
104    /** @inheritDoc */
105    public function getDbType() {
106        if ( $this->hasOption( 'env-checks' ) ) {
107            return Maintenance::DB_NONE;
108        }
109        return parent::getDbType();
110    }
111
112    /** @inheritDoc */
113    public function execute() {
114        if ( $this->hasOption( 'help' ) ) {
115            $this->maybeHelp();
116            return;
117        }
118
119        // Manually check for required arguments, as 0 arguments allows interactive mode to be used
120        if ( $this->getArg( 0 ) && !$this->getArg( 1 ) ) {
121            $this->fatalError( 'Argument <' . $this->getArgName( 1 ) . '> is required!' );
122        }
123
124        // No arguments, means interactive mode
125        if ( !$this->getArg( 0 ) || !$this->getArg( 1 ) ) {
126            $this->output( "Hello, I'm the MediaWiki installer!\n\n" );
127            $this->output( "This script will guide you through the process of installing MediaWiki.\n" );
128            $this->output( "If you actually want to see the help for this script, use --help.\n\n" );
129
130            $siteName = $this->prompt( 'Enter the name of the wiki:', "Wiki" );
131            $language = $this->prompt( 'Enter the language code of the wiki:', 'en' );
132            $server = $this->prompt(
133                'Enter the base URL of the web server the wiki will be on (without a path):',
134                'http://localhost'
135            );
136            $adminName = $this->prompt( 'Enter the username of the MediaWiki account that will be created ' .
137                'at the end of the installation and given administrator rights:', "Admin" );
138            $adminPass = $this->prompt(
139                'Enter the password for the wiki administrator:',
140                $this->generateStrongPassword()
141            );
142            $dbType = $this->prompt( 'Enter the type of database (for example mysql or sqlite):', 'mysql' );
143            // Assume that sqlite is the only db type that needs a path
144            $dbPath = $dbType == 'sqlite' ?
145                $this->prompt(
146                    'Enter the path for the SQLite DB (advised to be outside the web root):',
147                    MW_INSTALL_PATH . '/data'
148                ) :
149                null;
150            $dbName = $this->prompt( 'Enter the name of the database:', 'my_wiki' );
151            // Assume that everything other than sqlite needs a server and credentials
152            $dbUser = $dbType == 'sqlite' ? null : $this->prompt( 'Enter the database user:', 'wikiuser' );
153            $dbPass = $dbType == 'sqlite' ? null : $this->prompt( 'Enter the database password:', '' );
154            $dbServer = $dbType == 'sqlite' ? null : $this->prompt( 'Enter the database server:', 'localhost' );
155
156            $this->output( "\n" );
157            $this->setArg( 0, $siteName );
158            $this->setArg( 1, $adminName );
159            $this->setOption( 'lang', $language );
160            $this->setOption( 'server', $server );
161            $this->setOption( 'pass', $adminPass );
162            $this->setOption( 'dbtype', $dbType );
163            $this->setOption( 'dbpath', $dbPath );
164            $this->setOption( 'dbname', $dbName );
165            $this->setOption( 'dbuser', $dbUser );
166            $this->setOption( 'dbpass', $dbPass );
167            $this->setOption( 'dbserver', $dbServer );
168            if ( !$this->promptYesNo( 'Do you want to continue with the installation?', true ) ) {
169                $this->output( "Installation aborted.\n" );
170                return false;
171            }
172        }
173
174        $siteName = $this->getArg( 0, 'MediaWiki' ); // Will not be set if used with --env-checks
175        $adminName = $this->getArg( 1 );
176        $envChecksOnly = $this->hasOption( 'env-checks' );
177
178        $scriptpath = $this->getOption( 'scriptpath', false );
179        if ( $scriptpath === false ) {
180            $this->setOption( 'scriptpath', '/' . basename( dirname( __DIR__ ) ) );
181        }
182
183        $this->setDbPassOption();
184        if ( !$envChecksOnly ) {
185            $this->setPassOption();
186        }
187
188        try {
189            $installer = InstallerOverrides::getCliInstaller( $siteName, $adminName, $this->parameters->getOptions() );
190        } catch ( InstallException $e ) {
191            $this->error( $e->getStatus() );
192            return false;
193        }
194
195        $status = $installer->doEnvironmentChecks();
196        if ( $status->isGood() ) {
197            $installer->showMessage( 'config-env-good' );
198        } else {
199            return false;
200        }
201        if ( !$envChecksOnly ) {
202            $status = $installer->execute();
203            if ( !$status->isGood() ) {
204                return false;
205            }
206            $installer->writeConfigurationFile( $this->getOption( 'confpath', MW_INSTALL_PATH ) );
207            $installer->showMessage(
208                'config-install-success',
209                $installer->getVar( 'wgServer' ),
210                $installer->getVar( 'wgScriptPath' )
211            );
212        }
213        return true;
214    }
215
216    private function generateStrongPassword(): string {
217        $strongPassword = '';
218        $strongPasswordLength = 20;
219        $strongPasswordChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-={}|;:,.<>?';
220        $strongPasswordCharsLength = strlen( $strongPasswordChars );
221        for ( $i = 0; $i < $strongPasswordLength; $i++ ) {
222            $strongPassword .= $strongPasswordChars[ rand( 0, $strongPasswordCharsLength - 1 ) ];
223        }
224        return $strongPassword;
225    }
226
227    private function setDbPassOption() {
228        $dbpassfile = $this->getOption( 'dbpassfile' );
229        if ( $dbpassfile !== null ) {
230            if ( $this->getOption( 'dbpass' ) !== null ) {
231                $this->error( 'WARNING: You have provided the options "dbpass" and "dbpassfile". '
232                    . 'The content of "dbpassfile" overrides "dbpass".' );
233            }
234            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
235            $dbpass = @file_get_contents( $dbpassfile ); // returns false on failure
236            if ( $dbpass === false ) {
237                $this->fatalError( "Couldn't open $dbpassfile" );
238            }
239            $this->setOption( 'dbpass', trim( $dbpass, "\r\n" ) );
240        }
241    }
242
243    private function setPassOption() {
244        $passfile = $this->getOption( 'passfile' );
245        if ( $passfile !== null ) {
246            if ( $this->getOption( 'pass' ) !== null ) {
247                $this->error( 'WARNING: You have provided the option --pass or --passfile. '
248                    . 'The content of "passfile" overrides "pass".' );
249            }
250            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
251            $pass = @file_get_contents( $passfile ); // returns false on failure
252            if ( $pass === false ) {
253                $this->fatalError( "Couldn't open $passfile" );
254            }
255            $this->setOption( 'pass', trim( $pass, "\r\n" ) );
256        } elseif ( $this->getOption( 'pass' ) === null ) {
257            $this->fatalError( 'You need to provide the option "pass" or "passfile"' );
258        }
259    }
260
261    public function validateParamsAndArgs() {
262        if ( !$this->hasOption( 'env-checks' ) ) {
263            $this->parameters->validate();
264        }
265    }
266}
267
268// @codeCoverageIgnoreStart
269$maintClass = CommandLineInstaller::class;
270
271require_once RUN_MAINTENANCE_IF_MAIN;
272// @codeCoverageIgnoreEnd