Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CliInstaller
0.00% covered (danger)
0.00%
0 / 128
0.00% covered (danger)
0.00%
0 / 16
2070
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
380
 validateExtensions
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 execute
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 writeConfigurationFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 startStage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 endStage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 showMessage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showSuccess
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showWarning
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showError
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMessageText
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatMessage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 showStatusMessage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 envCheckPath
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 envGetDefaultServer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 dirIsExecutable
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * @license GPL-2.0-or-later
5 * @file
6 * @ingroup Installer
7 */
8
9namespace MediaWiki\Installer;
10
11use MediaWiki\Context\RequestContext;
12use MediaWiki\Installer\Task\Task;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Message\Message;
15use MediaWiki\Parser\Sanitizer;
16use MediaWiki\Password\UserPasswordPolicy;
17use MediaWiki\User\User;
18use StatusValue;
19use Wikimedia\Message\MessageSpecifier;
20
21/**
22 * Class for the core installer command line interface.
23 *
24 * @ingroup Installer
25 * @since 1.17
26 */
27class CliInstaller extends Installer {
28    /** @var bool */
29    private $specifiedScriptPath = false;
30
31    private const OPTION_MAP = [
32        'dbtype' => 'wgDBtype',
33        'dbserver' => 'wgDBserver',
34        'dbname' => 'wgDBname',
35        'dbuser' => 'wgDBuser',
36        'dbpass' => 'wgDBpassword',
37        'dbprefix' => 'wgDBprefix',
38        'dbtableoptions' => 'wgDBTableOptions',
39        'dbport' => 'wgDBport',
40        'dbssl' => 'wgDBssl',
41        'dbschema' => 'wgDBmwschema',
42        'dbpath' => 'wgSQLiteDataDir',
43        'server' => 'wgServer',
44        'scriptpath' => 'wgScriptPath',
45    ];
46
47    /**
48     * @param string $siteName
49     * @param string|null $admin
50     * @param array $options
51     * @throws InstallException
52     */
53    public function __construct( $siteName, $admin = null, array $options = [] ) {
54        global $wgPasswordPolicy;
55
56        parent::__construct();
57
58        if ( isset( $options['scriptpath'] ) ) {
59            $this->specifiedScriptPath = true;
60        }
61
62        foreach ( self::OPTION_MAP as $opt => $global ) {
63            if ( isset( $options[$opt] ) ) {
64                $GLOBALS[$global] = $options[$opt];
65                $this->setVar( $global, $options[$opt] );
66            }
67        }
68
69        if ( isset( $options['lang'] ) ) {
70            global $wgLanguageCode;
71            $this->setVar( '_UserLang', $options['lang'] );
72            $wgLanguageCode = $options['lang'];
73            $this->setVar( 'wgLanguageCode', $wgLanguageCode );
74            $lang = MediaWikiServices::getInstance()->getLanguageFactory()
75                ->getLanguage( $options['lang'] );
76            RequestContext::getMain()->setLanguage( $lang );
77        }
78
79        $this->setVar( 'wgSitename', $siteName );
80
81        $contLang = MediaWikiServices::getInstance()->getContentLanguage();
82        $metaNS = $contLang->ucfirst( str_replace( ' ', '_', $siteName ) );
83        if ( $metaNS == 'MediaWiki' ) {
84            $metaNS = 'Project';
85        }
86        $this->setVar( 'wgMetaNamespace', $metaNS );
87
88        if ( !isset( $options['installdbuser'] ) ) {
89            $this->setVar( '_InstallUser',
90                $this->getVar( 'wgDBuser' ) );
91            $this->setVar( '_InstallPassword',
92                $this->getVar( 'wgDBpassword' ) );
93        } else {
94            $this->setVar( '_InstallUser',
95                $options['installdbuser'] );
96            $this->setVar( '_InstallPassword',
97                $options['installdbpass'] ?? "" );
98
99            // Assume that if we're given the installer user, we'll create the account.
100            $this->setVar( '_CreateDBAccount', true );
101        }
102
103        if ( $admin ) {
104            $this->setVar( '_AdminName', $admin );
105            if ( isset( $options['pass'] ) ) {
106                $adminUser = User::newFromName( $admin );
107                if ( !$adminUser ) {
108                    throw new InstallException( StatusValue::newFatal( 'config-admin-name-invalid' ) );
109                }
110                $upp = new UserPasswordPolicy(
111                    $wgPasswordPolicy['policies'],
112                    $wgPasswordPolicy['checks']
113                );
114                $status = $upp->checkUserPasswordForGroups( $adminUser, $options['pass'],
115                    [ 'bureaucrat', 'sysop', 'interface-admin' ] ); // per Installer::createSysop()
116                if ( !$status->isGood() ) {
117                    throw new InstallException( StatusValue::newFatal(
118                        $status->getMessage( 'config-admin-error-password-invalid' ) ) );
119                }
120                $this->setVar( '_AdminPassword', $options['pass'] );
121            }
122        }
123
124        // Detect and inject any extension found
125        if ( isset( $options['extensions'] ) ) {
126            $status = $this->validateExtensions(
127                'extension', 'extensions', $options['extensions'] );
128            if ( !$status->isOK() ) {
129                throw new InstallException( $status );
130            }
131            $this->setVar( '_Extensions', $status->value );
132        } elseif ( isset( $options['with-extensions'] ) ) {
133            $status = $this->findExtensions();
134            if ( !$status->isOK() ) {
135                throw new InstallException( $status );
136            }
137            $this->setVar( '_Extensions', array_keys( $status->value ) );
138        }
139
140        // Set up the default skins
141        if ( isset( $options['skins'] ) ) {
142            $status = $this->validateExtensions( 'skin', 'skins', $options['skins'] );
143            if ( !$status->isOK() ) {
144                throw new InstallException( $status );
145            }
146            $skins = $status->value;
147        } else {
148            $status = $this->findExtensions( 'skins' );
149            if ( !$status->isOK() ) {
150                throw new InstallException( $status );
151            }
152            $skins = array_keys( $status->value );
153        }
154        $this->setVar( '_Skins', $skins );
155
156        if ( $skins ) {
157            $skinNames = array_map( 'strtolower', $skins );
158            $this->setVar( 'wgDefaultSkin', $this->getDefaultSkin( $skinNames ) );
159        }
160
161        $this->setVar( '_WithDevelopmentSettings', isset( $options['with-developmentsettings'] ) );
162    }
163
164    /**
165     * @param string $type
166     * @param string $directory
167     * @param string|string[] $nameLists
168     */
169    private function validateExtensions( string $type, string $directory, $nameLists ): StatusValue {
170        $extensions = [];
171        $status = new StatusValue();
172        foreach ( (array)$nameLists as $nameList ) {
173            foreach ( explode( ',', $nameList ) as $name ) {
174                $name = trim( $name );
175                if ( $name === '' ) {
176                    continue;
177                }
178                $extStatus = $this->getExtensionInfo( $type, $directory, $name );
179                if ( $extStatus->isOK() ) {
180                    $extensions[] = $name;
181                } else {
182                    $status->merge( $extStatus );
183                }
184            }
185        }
186        $extensions = array_unique( $extensions );
187        $status->value = $extensions;
188        return $status;
189    }
190
191    /**
192     * Main entry point.
193     * @return StatusValue
194     */
195    public function execute() {
196        // If APC is available, use that as the MainCacheType, instead of nothing.
197        // This is hacky and should be consolidated with WebInstallerOptions.
198        // This is here instead of in __construct(), because it should run after
199        // doEnvironmentChecks(), which populates '_Caches'.
200        if ( count( $this->getVar( '_Caches' ) ) ) {
201            // We detected a CACHE_ACCEL implementation, use it.
202            $this->setVar( '_MainCacheType', 'accel' );
203        }
204
205        $vars = Installer::getExistingLocalSettings();
206        if ( $vars ) {
207            $status = StatusValue::newFatal( "config-localsettings-cli-upgrade" );
208            $this->showStatusMessage( $status );
209            return $status;
210        }
211
212        $status = $this->performInstallation(
213            $this->startStage( ... ),
214            $this->endStage( ... )
215        );
216        if ( $status->isOK() ) {
217            return StatusValue::newGood();
218        } else {
219            return $status;
220        }
221    }
222
223    /**
224     * Write LocalSettings.php to a given path
225     *
226     * @param string $path Full path to write LocalSettings.php to
227     */
228    public function writeConfigurationFile( $path ) {
229        $ls = InstallerOverrides::getLocalSettingsGenerator( $this );
230        $ls->writeFile( "$path/LocalSettings.php" );
231    }
232
233    /**
234     * @param Task $task
235     */
236    public function startStage( $task ) {
237        // @phan-suppress-next-line SecurityCheck-XSS -- it's CLI
238        echo $this->formatMessage( $task->getDescriptionMessage() ) . '... ';
239    }
240
241    /**
242     * @param Task $task
243     * @param StatusValue $status
244     */
245    public function endStage( $task, $status ) {
246        $this->showStatusMessage( $status );
247        if ( $status->isOK() ) {
248            $this->showMessage( 'config-install-step-done' );
249        } else {
250            $this->showError( 'config-install-step-failed' );
251        }
252    }
253
254    /** @inheritDoc */
255    public function showMessage( $msg, ...$params ) {
256        // @phan-suppress-next-line SecurityCheck-XSS
257        echo $this->getMessageText( $msg, $params ) . "\n";
258        flush();
259    }
260
261    /** @inheritDoc */
262    public function showSuccess( $msg, ...$params ) {
263        // @phan-suppress-next-line SecurityCheck-XSS
264        echo $this->getMessageText( $msg, $params ) . "\n";
265        flush();
266    }
267
268    /** @inheritDoc */
269    public function showWarning( $msg, ...$params ) {
270        // @phan-suppress-next-line SecurityCheck-XSS
271        echo $this->getMessageText( $msg, $params ) . "\n";
272        flush();
273    }
274
275    /** @inheritDoc */
276    public function showError( $msg, ...$params ) {
277        // @phan-suppress-next-line SecurityCheck-XSS
278        echo "***{$this->getMessageText( $msg, $params )}***\n";
279        flush();
280    }
281
282    /**
283     * @param string|MessageSpecifier $msg
284     * @param (string|int|float)[] $params Message parameters
285     * @return string
286     */
287    protected function getMessageText( $msg, $params ) {
288        return $this->formatMessage( wfMessage( $msg, $params ) );
289    }
290
291    /**
292     * @param Message $message
293     * @return string
294     */
295    protected function formatMessage( $message ) {
296        $text = $message->parse();
297        $text = preg_replace( '/<a href="(.*?)".*?>(.*?)<\/a>/', '$2 &lt;$1&gt;', $text );
298        return Sanitizer::stripAllTags( $text );
299    }
300
301    public function showStatusMessage( StatusValue $status ) {
302        // Show errors at the end in CLI installer to make them easier to notice
303        foreach ( $status->getMessages( 'warning' ) as $msg ) {
304            $this->showMessage( $msg );
305        }
306        foreach ( $status->getMessages( 'error' ) as $msg ) {
307            $this->showMessage( $msg );
308        }
309    }
310
311    /** @inheritDoc */
312    public function envCheckPath() {
313        if ( !$this->specifiedScriptPath ) {
314            $this->showMessage( 'config-no-cli-uri', $this->getVar( "wgScriptPath" ) );
315        }
316
317        return parent::envCheckPath();
318    }
319
320    /** @inheritDoc */
321    protected function envGetDefaultServer() {
322        // Use a basic value if the user didn't pass in --server
323        return 'http://localhost';
324    }
325
326    /** @inheritDoc */
327    public function dirIsExecutable( $dir, $url ) {
328        $this->showMessage( 'config-no-cli-uploads-check', $dir );
329
330        return false;
331    }
332}