Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.50% covered (danger)
1.50%
7 / 467
0.00% covered (danger)
0.00%
0 / 47
CRAP
0.00% covered (danger)
0.00%
0 / 1
WebInstaller
1.50% covered (danger)
1.50%
7 / 467
0.00% covered (danger)
0.00%
0 / 47
17812.68
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
600
 getLowestUnhappy
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 startSession
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 getFingerprint
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 showError
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 errorHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 finish
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 reset
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getUrl
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getPageByName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nextTabIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setupLanguage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getAcceptLanguage
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 startPageWrapper
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getPageListItem
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 endPageWrapper
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpBox
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getInfoBox
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 showSuccess
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 showMessage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 showWarning
0.00% covered (danger)
0.00%
0 / 5
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
 label
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 getTextBox
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 getTextArea
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 getPasswordBox
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getCheckBox
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
42
 getRadioSet
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getRadioElements
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
 showStatusBox
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setVarsFromRequest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 getDocUrl
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 makeLinkItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getLocalSettingsLocation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 envCheckPath
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 detectWebPaths
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 envGetDefaultServer
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultServer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputLS
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 outputCss
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getPhpErrors
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsUpgrade
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doUpgrade
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 outputHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Core installer web interface.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Installer
8 */
9
10namespace MediaWiki\Installer;
11
12use Exception;
13use MediaWiki\Context\RequestContext;
14use MediaWiki\Html\Html;
15use MediaWiki\Installer\Task\TaskFactory;
16use MediaWiki\Installer\Task\TaskList;
17use MediaWiki\Installer\Task\TaskRunner;
18use MediaWiki\Languages\LanguageNameUtils;
19use MediaWiki\MediaWikiServices;
20use MediaWiki\Message\Message;
21use MediaWiki\Request\ContentSecurityPolicy;
22use MediaWiki\Request\WebRequest;
23use MediaWiki\Status\Status;
24use Wikimedia\HtmlArmor\HtmlArmor;
25
26/**
27 * Class for the core installer web interface.
28 *
29 * @ingroup Installer
30 * @since 1.17
31 */
32class WebInstaller extends Installer {
33
34    /**
35     * @var WebInstallerOutput
36     */
37    public $output;
38
39    /**
40     * WebRequest object.
41     *
42     * @var WebRequest
43     */
44    public $request;
45
46    /**
47     * Cached session array.
48     *
49     * @var array[]
50     */
51    protected $session;
52
53    /**
54     * Captured PHP error text. Temporary.
55     *
56     * @var string[]
57     */
58    protected $phpErrors;
59
60    /**
61     * The main sequence of page names. These will be displayed in turn.
62     *
63     * To add a new installer page:
64     *    * Add it to this WebInstaller::$pageSequence property
65     *    * Add a "config-page-<name>" message
66     *    * Add a "WebInstaller<name>" class
67     *
68     * @var string[]
69     */
70    public $pageSequence = [
71        'Language',
72        'ExistingWiki',
73        'Welcome',
74        'DBConnect',
75        'Upgrade',
76        'DBSettings',
77        'Name',
78        'Options',
79        'Install',
80        'Complete',
81    ];
82
83    /**
84     * Out of sequence pages, selectable by the user at any time.
85     *
86     * @var string[]
87     */
88    protected $otherPages = [
89        'Restart',
90        'ReleaseNotes',
91        'Copying',
92        'UpgradeDoc', // Can't use Upgrade due to Upgrade step
93    ];
94
95    /**
96     * Array of pages which have declared that they have been submitted, have validated
97     * their input, and need no further processing.
98     *
99     * @var bool[]
100     */
101    protected $happyPages;
102
103    /**
104     * List of "skipped" pages. These are pages that will automatically continue
105     * to the next page on any GET request. To avoid breaking the "back" button,
106     * they need to be skipped during a back operation.
107     *
108     * @var bool[]
109     */
110    protected $skippedPages;
111
112    /**
113     * Flag indicating that session data may have been lost.
114     *
115     * @var bool
116     */
117    public $showSessionWarning = false;
118
119    /**
120     * Numeric index of the page we're on
121     *
122     * @var int
123     */
124    protected $tabIndex = 1;
125
126    /**
127     * Numeric index of the help box
128     *
129     * @var int
130     */
131    protected $helpBoxId = 1;
132
133    /**
134     * Name of the page we're on
135     *
136     * @var string
137     */
138    protected $currentPageName;
139
140    public function __construct( WebRequest $request ) {
141        parent::__construct();
142        $this->output = new WebInstallerOutput( $this );
143        $this->request = $request;
144    }
145
146    /**
147     * Main entry point.
148     *
149     * @param array[] $session Initial session array
150     *
151     * @return array[] New session array
152     */
153    public function execute( array $session ) {
154        $this->session = $session;
155
156        if ( isset( $session['settings'] ) ) {
157            $this->settings = $session['settings'] + $this->settings;
158            // T187586 MediaWikiServices works with globals
159            foreach ( $this->settings as $key => $val ) {
160                $GLOBALS[$key] = $val;
161            }
162        }
163
164        $this->setupLanguage();
165
166        if ( ( $this->getVar( '_InstallDone' ) || $this->getVar( '_UpgradeDone' ) )
167            && $this->request->getVal( 'localsettings' )
168        ) {
169            $this->outputLS();
170            return $this->session;
171        }
172
173        $isCSS = $this->request->getCheck( 'css' );
174        if ( $isCSS ) {
175            $this->outputCss();
176            return $this->session;
177        }
178
179        $this->happyPages = $session['happyPages'] ?? [];
180
181        $this->skippedPages = $session['skippedPages'] ?? [];
182
183        $lowestUnhappy = $this->getLowestUnhappy();
184
185        # Get the page name.
186        $pageName = $this->request->getVal( 'page', '' );
187
188        if ( in_array( $pageName, $this->otherPages ) ) {
189            # Out of sequence
190            $pageId = false;
191            $page = $this->getPageByName( $pageName );
192        } else {
193            # Main sequence
194            if ( !$pageName || !in_array( $pageName, $this->pageSequence ) ) {
195                $pageId = $lowestUnhappy;
196            } else {
197                $pageId = array_search( $pageName, $this->pageSequence );
198            }
199
200            # If necessary, move back to the lowest-numbered unhappy page
201            if ( $pageId > $lowestUnhappy ) {
202                $pageId = $lowestUnhappy;
203                if ( $lowestUnhappy == 0 ) {
204                    # Knocked back to start, possible loss of session data.
205                    $this->showSessionWarning = true;
206                }
207            }
208
209            $pageName = $this->pageSequence[$pageId];
210            $page = $this->getPageByName( $pageName );
211        }
212
213        # If a back button was submitted, go back without submitting the form data.
214        if ( $this->request->wasPosted() && $this->request->getBool( 'submit-back' ) ) {
215            if ( $this->request->getVal( 'lastPage' ) ) {
216                $nextPage = $this->request->getVal( 'lastPage' );
217            } elseif ( $pageId !== false ) {
218                # Main sequence page
219                # Skip the skipped pages
220                $nextPageId = $pageId;
221
222                do {
223                    $nextPageId--;
224                    $nextPage = $this->pageSequence[$nextPageId];
225                } while ( isset( $this->skippedPages[$nextPage] ) );
226            } else {
227                $nextPage = $this->pageSequence[$lowestUnhappy];
228            }
229
230            $this->output->redirect( $this->getUrl( [ 'page' => $nextPage ] ) );
231
232            return $this->finish();
233        }
234
235        # Execute the page.
236        $this->currentPageName = $page->getName();
237        $this->startPageWrapper( $pageName );
238
239        if ( $page->isSlow() ) {
240            $this->disableTimeLimit();
241        }
242
243        $result = $page->execute();
244
245        $this->endPageWrapper();
246
247        if ( $result == 'skip' ) {
248            # Page skipped without explicit submission.
249            # Skip it when we click "back" so that we don't just go forward again.
250            $this->skippedPages[$pageName] = true;
251            $result = 'continue';
252        } else {
253            unset( $this->skippedPages[$pageName] );
254        }
255
256        # If it was posted, the page can request a continue to the next page.
257        if ( $result === 'continue' && !$this->output->headerDone() ) {
258            if ( $pageId !== false ) {
259                $this->happyPages[$pageId] = true;
260            }
261
262            $lowestUnhappy = $this->getLowestUnhappy();
263
264            if ( $this->request->getVal( 'lastPage' ) ) {
265                $nextPage = $this->request->getVal( 'lastPage' );
266            } elseif ( $pageId !== false ) {
267                $nextPage = $this->pageSequence[$pageId + 1];
268            } else {
269                $nextPage = $this->pageSequence[$lowestUnhappy];
270            }
271
272            if ( array_search( $nextPage, $this->pageSequence ) > $lowestUnhappy ) {
273                $nextPage = $this->pageSequence[$lowestUnhappy];
274            }
275
276            $this->output->redirect( $this->getUrl( [ 'page' => $nextPage ] ) );
277        }
278
279        return $this->finish();
280    }
281
282    /**
283     * Find the next page in sequence that hasn't been completed
284     * @return int
285     */
286    public function getLowestUnhappy() {
287        if ( count( $this->happyPages ) == 0 ) {
288            return 0;
289        } else {
290            return max( array_keys( $this->happyPages ) ) + 1;
291        }
292    }
293
294    /**
295     * Start the PHP session. This may be called before execute() to start the PHP session.
296     *
297     * @throws Exception
298     * @return bool
299     */
300    public function startSession() {
301        if ( wfIniGetBool( 'session.auto_start' ) || session_id() ) {
302            // Done already
303            return true;
304        }
305
306        // Use secure cookies if we are on HTTPS
307        $options = [];
308        if ( $this->request->getProtocol() === 'https' ) {
309            $options['cookie_secure'] = '1';
310        }
311
312        $this->phpErrors = [];
313        set_error_handler( $this->errorHandler( ... ) );
314        try {
315            session_name( 'mw_installer_session' );
316            session_start( $options );
317        } catch ( Exception $e ) {
318            restore_error_handler();
319            throw $e;
320        }
321        restore_error_handler();
322
323        if ( $this->phpErrors ) {
324            return false;
325        }
326
327        return true;
328    }
329
330    /**
331     * Get a hash of data identifying this MW installation.
332     *
333     * This is used by mw-config/index.php to prevent multiple installations of MW
334     * on the same cookie domain from interfering with each other.
335     *
336     * @return string
337     */
338    public function getFingerprint() {
339        // Get the base URL of the installation
340        $url = $this->request->getFullRequestURL();
341        if ( preg_match( '!^(.*\?)!', $url, $m ) ) {
342            // Trim query string
343            $url = $m[1];
344        }
345        if ( preg_match( '!^(.*)/[^/]*/[^/]*$!', $url, $m ) ) {
346            // This... seems to try to get the base path from
347            // the /mw-config/index.php. Kinda scary though?
348            $url = $m[1];
349        }
350
351        return md5( serialize( [
352            'local path' => dirname( __DIR__ ),
353            'url' => $url,
354            'version' => MW_VERSION
355        ] ) );
356    }
357
358    /** @inheritDoc */
359    public function showError( $msg, ...$params ) {
360        if ( !( $msg instanceof Message ) ) {
361            $msg = wfMessage(
362                $msg,
363                array_map( 'htmlspecialchars', $params )
364            );
365        }
366        $text = $msg->useDatabase( false )->parse();
367        $box = Html::errorBox( $text, '', 'config-error-box' );
368        $this->output->addHTML( $box );
369    }
370
371    /**
372     * Temporary error handler for session start debugging.
373     *
374     * @param int $errno Unused
375     * @param string $errstr
376     */
377    public function errorHandler( $errno, $errstr ) {
378        $this->phpErrors[] = $errstr;
379    }
380
381    /**
382     * Clean up from execute()
383     *
384     * @return array[]
385     */
386    public function finish() {
387        $this->output->output();
388
389        $this->session['happyPages'] = $this->happyPages;
390        $this->session['skippedPages'] = $this->skippedPages;
391        $this->session['settings'] = $this->settings;
392
393        return $this->session;
394    }
395
396    /**
397     * We're restarting the installation, reset the session, happyPages, etc
398     */
399    public function reset() {
400        $this->session = [];
401        $this->happyPages = [];
402        $this->settings = [];
403    }
404
405    /**
406     * Get a URL for submission back to the same script.
407     *
408     * @param string[] $query
409     *
410     * @return string
411     */
412    public function getUrl( $query = [] ) {
413        $url = $this->request->getRequestURL();
414        # Remove existing query
415        $url = preg_replace( '/\?.*$/', '', $url );
416
417        if ( $query ) {
418            $url .= '?' . wfArrayToCgi( $query );
419        }
420
421        return $url;
422    }
423
424    /**
425     * Get a WebInstallerPage by name.
426     *
427     * @param string $pageName
428     * @return WebInstallerPage
429     */
430    public function getPageByName( $pageName ) {
431        $pageClass = 'MediaWiki\\Installer\\WebInstaller' . $pageName;
432
433        return new $pageClass( $this );
434    }
435
436    /**
437     * Get a session variable.
438     *
439     * @param string $name
440     * @param array|null $default
441     *
442     * @return array|null
443     */
444    public function getSession( $name, $default = null ) {
445        return $this->session[$name] ?? $default;
446    }
447
448    /**
449     * Set a session variable.
450     *
451     * @param string $name Key for the variable
452     * @param mixed $value
453     */
454    public function setSession( $name, $value ) {
455        $this->session[$name] = $value;
456    }
457
458    /**
459     * Get the next tabindex attribute value.
460     *
461     * @return int
462     */
463    public function nextTabIndex() {
464        return $this->tabIndex++;
465    }
466
467    /**
468     * Initializes language-related variables.
469     */
470    public function setupLanguage() {
471        global $wgLang, $wgLanguageCode;
472
473        if ( $this->getSession( 'test' ) === null && !$this->request->wasPosted() ) {
474            $wgLanguageCode = $this->getAcceptLanguage();
475            $wgLang = MediaWikiServices::getInstance()->getLanguageFactory()
476                ->getLanguage( $wgLanguageCode );
477            RequestContext::getMain()->setLanguage( $wgLang );
478            $this->setVar( 'wgLanguageCode', $wgLanguageCode );
479            $this->setVar( '_UserLang', $wgLanguageCode );
480        } else {
481            $wgLanguageCode = $this->getVar( 'wgLanguageCode' );
482        }
483    }
484
485    /**
486     * Retrieves MediaWiki language from Accept-Language HTTP header.
487     *
488     * @return string
489     * @return-taint none It can only return a known-good code.
490     */
491    public function getAcceptLanguage() {
492        global $wgLanguageCode;
493
494        $mwLanguages = MediaWikiServices::getInstance()
495            ->getLanguageNameUtils()
496            ->getLanguageNames( LanguageNameUtils::AUTONYMS, LanguageNameUtils::SUPPORTED );
497        $headerLanguages = array_keys( $this->request->getAcceptLang() );
498
499        foreach ( $headerLanguages as $lang ) {
500            if ( isset( $mwLanguages[$lang] ) ) {
501                return $lang;
502            }
503        }
504
505        return $wgLanguageCode;
506    }
507
508    /**
509     * Called by execute() before page output starts, to show a page list.
510     *
511     * @param string $currentPageName
512     */
513    private function startPageWrapper( $currentPageName ) {
514        $s = "<div class=\"config-page-wrapper\">\n";
515        $s .= "<div class=\"config-page\">\n";
516        $s .= "<div class=\"config-page-list cdx-card\"><span class=\"cdx-card__text\">";
517        $s .= "<span class=\"cdx-card__text__description\"><ul>\n";
518        $lastHappy = -1;
519
520        foreach ( $this->pageSequence as $id => $pageName ) {
521            $happy = !empty( $this->happyPages[$id] );
522            $s .= $this->getPageListItem(
523                $pageName,
524                $happy || $lastHappy == $id - 1,
525                $currentPageName
526            );
527
528            if ( $happy ) {
529                $lastHappy = $id;
530            }
531        }
532
533        $s .= "</ul><br/><ul>\n";
534        $s .= $this->getPageListItem( 'Restart', true, $currentPageName );
535        // End list pane
536        $s .= "</ul></span></span></div>\n";
537
538        // Messages:
539        // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
540        // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
541        // config-page-complete, config-page-restart, config-page-releasenotes,
542        // config-page-copying, config-page-upgradedoc, config-page-existingwiki
543        $s .= Html::element( 'h2', [],
544            wfMessage( 'config-page-' . strtolower( $currentPageName ) )->text() );
545
546        $this->output->addHTMLNoFlush( $s );
547    }
548
549    /**
550     * Get a list item for the page list.
551     *
552     * @param string $pageName
553     * @param bool $enabled
554     * @param string $currentPageName
555     *
556     * @return string
557     */
558    private function getPageListItem( $pageName, $enabled, $currentPageName ) {
559        $s = "<li class=\"config-page-list-item\">";
560
561        // Messages:
562        // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
563        // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
564        // config-page-complete, config-page-restart, config-page-releasenotes,
565        // config-page-copying, config-page-upgradedoc, config-page-existingwiki
566        $name = wfMessage( 'config-page-' . strtolower( $pageName ) )->text();
567
568        if ( $enabled ) {
569            $query = [ 'page' => $pageName ];
570
571            if ( !in_array( $pageName, $this->pageSequence ) ) {
572                if ( in_array( $currentPageName, $this->pageSequence ) ) {
573                    $query['lastPage'] = $currentPageName;
574                }
575
576                $link = Html::element( 'a',
577                    [
578                        'href' => $this->getUrl( $query )
579                    ],
580                    $name
581                );
582            } else {
583                $link = htmlspecialchars( $name );
584            }
585
586            if ( $pageName == $currentPageName ) {
587                $s .= "<span class=\"config-page-current\">$link</span>";
588            } else {
589                $s .= $link;
590            }
591        } else {
592            $s .= Html::element( 'span',
593                [
594                    'class' => 'config-page-disabled'
595                ],
596                $name
597            );
598        }
599
600        $s .= "</li>\n";
601
602        return $s;
603    }
604
605    /**
606     * Output some stuff after a page is finished.
607     */
608    private function endPageWrapper() {
609        $this->output->addHTMLNoFlush(
610            "<div class=\"visualClear\"></div>\n" .
611            "</div>\n" .
612            "<div class=\"visualClear\"></div>\n" .
613            "</div>" );
614    }
615
616    /**
617     * Get small text indented help for a preceding form field.
618     * Parameters like wfMessage().
619     *
620     * @param string $msg Message key
621     * @param string|int|float ...$params Message parameters
622     * @return string HTML
623     * @return-taint escaped
624     */
625    public function getHelpBox( $msg, ...$params ) {
626        $params = array_map( 'htmlspecialchars', $params );
627        $text = wfMessage( $msg, $params )->useDatabase( false )->plain();
628        $html = $this->parse( $text, true );
629
630        return "<div class=\"config-help-field-container\">\n" .
631            "<a class=\"config-help-field-hint\" title=\"" .
632            wfMessage( 'config-help-tooltip' )->escaped() . "\">ℹ️ " .
633            wfMessage( 'config-help' )->escaped() . "</a>\n" .
634            "<div class=\"config-help-field-content config-help-field-content-hidden " .
635            "cdx-message cdx-message--block cdx-message--notice\" style=\"margin: 10px\">" .
636            "<div class=\"cdx-message__content\">" . $html . "</div></div>\n" .
637            "</div>\n";
638    }
639
640    /**
641     * Get HTML for an information message box.
642     *
643     * @param string|HtmlArmor $text Wikitext to be parsed (from Message::plain) or raw HTML.
644     * @return string HTML
645     */
646    public function getInfoBox( $text ) {
647        $html = ( $text instanceof HtmlArmor ) ?
648            HtmlArmor::getHtml( $text ) :
649            $this->parse( $text, true );
650        return '<div class="cdx-message cdx-message--block cdx-message--notice">' .
651            '<span class="cdx-message__icon"></span><div class="cdx-message__content">' .
652            '<p><strong>' . wfMessage( 'config-information' )->escaped() . '</strong></p>' .
653            $html .
654            "</div></div>\n";
655    }
656
657    /** @inheritDoc */
658    public function showSuccess( $msg, ...$params ) {
659        $html = '<div class="cdx-message cdx-message--block cdx-message--success">' .
660            '<span class="cdx-message__icon"></span><div class="cdx-message__content">' .
661            $this->parse( wfMessage( $msg, $params )->useDatabase( false )->plain() ) .
662            "</div></div>\n";
663        $this->output->addHTML( $html );
664    }
665
666    /** @inheritDoc */
667    public function showMessage( $msg, ...$params ) {
668        $html = '<div class="cdx-message cdx-message--block cdx-message--notice">' .
669            '<span class="cdx-message__icon"></span><div class="cdx-message__content">' .
670            $this->parse( wfMessage( $msg, $params )->useDatabase( false )->plain() ) .
671            "</div></div>\n";
672        $this->output->addHTML( $html );
673    }
674
675    /** @inheritDoc */
676    public function showWarning( $msg, ...$params ) {
677        $html = '<div class="cdx-message cdx-message--block cdx-message--warning">' .
678            '<span class="cdx-message__icon"></span><div class="cdx-message__content">' .
679            $this->parse( wfMessage( $msg, $params )->useDatabase( false )->plain() ) .
680            "</div></div>\n";
681        $this->output->addHTML( $html );
682    }
683
684    /** @inheritDoc */
685    public function showStatusMessage( Status $status ) {
686        // Show errors at the top in web installer to make them easier to notice
687        foreach ( $status->getMessages( 'error' ) as $msg ) {
688            $this->showWarning( $msg );
689        }
690        foreach ( $status->getMessages( 'warning' ) as $msg ) {
691            $this->showWarning( $msg );
692        }
693    }
694
695    /**
696     * Label a control by wrapping a config-input div around it and putting a
697     * label before it.
698     *
699     * @param string $msg
700     * @param string|false $forId
701     * @param string $contents HTML
702     * @param string $helpData
703     * @return string HTML
704     * @return-taint escaped
705     */
706    public function label( $msg, $forId, $contents, $helpData = "" ) {
707        if ( strval( $msg ) == '' ) {
708            $labelText = "\u{00A0}";
709        } else {
710            $labelText = wfMessage( $msg )->escaped();
711        }
712
713        $attributes = [ 'class' => 'config-label' ];
714
715        if ( $forId ) {
716            $attributes['for'] = $forId;
717        }
718
719        return "<div class=\"config-block\">\n" .
720            "  <div class=\"config-block-label\">\n" .
721            Html::rawElement( 'label',
722                $attributes,
723                $labelText
724            ) . "\n" .
725            $helpData .
726            "  </div>\n" .
727            "  <div class=\"config-block-elements\">\n" .
728            $contents .
729            "  </div>\n" .
730            "</div>\n";
731    }
732
733    /**
734     * Get a labelled text box to configure a variable.
735     *
736     * @param mixed[] $params
737     *    Parameters are:
738     *      var:         The variable to be configured (required)
739     *      label:       The message name for the label (required)
740     *      attribs:     Additional attributes for the input element (optional)
741     *      controlName: The name for the input element (optional)
742     *      value:       The current value of the variable (optional)
743     *      help:        The html for the help text (optional)
744     *
745     * @return string HTML
746     * @return-taint escaped
747     */
748    public function getTextBox( $params ) {
749        if ( !isset( $params['controlName'] ) ) {
750            $params['controlName'] = 'config_' . $params['var'];
751        }
752
753        if ( !isset( $params['value'] ) ) {
754            $params['value'] = $this->getVar( $params['var'] );
755        }
756
757        if ( !isset( $params['attribs'] ) ) {
758            $params['attribs'] = [];
759        }
760        if ( !isset( $params['help'] ) ) {
761            $params['help'] = "";
762        }
763
764        return $this->label(
765            $params['label'],
766            $params['controlName'],
767            "<div class=\"cdx-text-input\">" .
768            Html::input(
769                $params['controlName'],
770                $params['value'],
771                $params['attribs']['type'] ?? 'text',
772                $params['attribs'] + [
773                    'id' => $params['controlName'],
774                    'size' => 30, // intended to be overridden by CSS
775                    'class' => 'cdx-text-input__input',
776                    'tabindex' => $this->nextTabIndex()
777                ]
778            ) . "</div>",
779            $params['help']
780        );
781    }
782
783    /**
784     * Get a labelled textarea to configure a variable
785     *
786     * @param mixed[] $params
787     *    Parameters are:
788     *      var:         The variable to be configured (required)
789     *      label:       The message name for the label (required)
790     *      attribs:     Additional attributes for the input element (optional)
791     *      controlName: The name for the input element (optional)
792     *      value:       The current value of the variable (optional)
793     *      help:        The html for the help text (optional)
794     *
795     * @return string
796     */
797    public function getTextArea( $params ) {
798        if ( !isset( $params['controlName'] ) ) {
799            $params['controlName'] = 'config_' . $params['var'];
800        }
801
802        if ( !isset( $params['value'] ) ) {
803            $params['value'] = $this->getVar( $params['var'] );
804        }
805
806        if ( !isset( $params['attribs'] ) ) {
807            $params['attribs'] = [];
808        }
809        if ( !isset( $params['help'] ) ) {
810            $params['help'] = "";
811        }
812
813        return $this->label(
814            $params['label'],
815            $params['controlName'],
816            Html::textarea(
817                $params['controlName'],
818                $params['value'],
819                $params['attribs'] + [
820                    'id' => $params['controlName'],
821                    'cols' => 30,
822                    'rows' => 5,
823                    'class' => 'config-input-text',
824                    'tabindex' => $this->nextTabIndex()
825                ]
826            ),
827            $params['help']
828        );
829    }
830
831    /**
832     * Get a labelled password box to configure a variable.
833     *
834     * Implements password hiding
835     * @param mixed[] $params
836     *    Parameters are:
837     *      var:         The variable to be configured (required)
838     *      label:       The message name for the label (required)
839     *      attribs:     Additional attributes for the input element (optional)
840     *      controlName: The name for the input element (optional)
841     *      value:       The current value of the variable (optional)
842     *      help:        The html for the help text (optional)
843     *
844     * @return string HTML
845     * @return-taint escaped
846     */
847    public function getPasswordBox( $params ) {
848        if ( !isset( $params['value'] ) ) {
849            $params['value'] = $this->getVar( $params['var'] );
850        }
851
852        if ( !isset( $params['attribs'] ) ) {
853            $params['attribs'] = [];
854        }
855
856        $params['value'] = $this->getFakePassword( $params['value'] );
857        $params['attribs']['type'] = 'password';
858
859        return $this->getTextBox( $params );
860    }
861
862    /**
863     * Get a labelled checkbox to configure a boolean variable.
864     *
865     * @param mixed[] $params
866     *    Parameters are:
867     *      var:         The variable to be configured (required)
868     *      label:       The message name for the label (required)
869     *      labelAttribs:Additional attributes for the label element (optional)
870     *      attribs:     Additional attributes for the input element (optional)
871     *      controlName: The name for the input element (optional)
872     *      value:       The current value of the variable (optional)
873     *      help:        The html for the help text (optional)
874     *
875     * @return string HTML
876     * @return-taint escaped
877     */
878    public function getCheckBox( $params ) {
879        if ( !isset( $params['controlName'] ) ) {
880            $params['controlName'] = 'config_' . $params['var'];
881        }
882
883        if ( !isset( $params['value'] ) ) {
884            $params['value'] = $this->getVar( $params['var'] );
885        }
886
887        if ( !isset( $params['attribs'] ) ) {
888            $params['attribs'] = [];
889        }
890        if ( !isset( $params['help'] ) ) {
891            $params['help'] = "";
892        }
893        if ( !isset( $params['labelAttribs'] ) ) {
894            $params['labelAttribs'] = [];
895        }
896        $labelText = $params['rawtext'] ?? $this->parse( wfMessage( $params['label'] )->plain() );
897        $labelText = '<span class="cdx-label__label__text"> ' . $labelText . '</span>';
898        Html::addClass( $params['attribs']['class'], 'cdx-checkbox__input' );
899        Html::addClass( $params['labelAttribs']['class'], 'cdx-label__label' );
900
901        return "<div class=\"cdx-checkbox\" style=\"margin-top: 12px; margin-bottom: 2px;\">" .
902            "<div class=\"cdx-checkbox__wrapper\">\n" .
903            Html::check(
904                $params['controlName'],
905                $params['value'],
906                $params['attribs'] + [
907                    'id' => $params['controlName'],
908                    'tabindex' => $this->nextTabIndex()
909                ]
910            ) .
911            "<span class=\"cdx-checkbox__icon\"></span>" .
912            "<div class=\"cdx-checkbox__label cdx-label\">" .
913            Html::rawElement(
914                'label',
915                $params['labelAttribs'] + [
916                    'for' => $params['controlName']
917                ],
918                $labelText
919                ) .
920            "</div></div></div>\n" . $params['help'];
921    }
922
923    /**
924     * Get a set of labelled radio buttons.
925     *
926     * @param mixed[] $params
927     *    Parameters are:
928     *      var:             The variable to be configured (required)
929     *      label:           The message name for the label (required)
930     *      itemLabelPrefix: The message name prefix for the item labels (required)
931     *      itemLabels:      List of message names to use for the item labels instead
932     *                       of itemLabelPrefix, keyed by values
933     *      values:          List of allowed values (required)
934     *      itemAttribs:     Array of attribute arrays, outer key is the value name (optional)
935     *      commonAttribs:   Attribute array applied to all items
936     *      controlName:     The name for the input element (optional)
937     *      value:           The current value of the variable (optional)
938     *      help:            The html for the help text (optional)
939     *
940     * @return string HTML
941     * @return-taint escaped
942     */
943    public function getRadioSet( $params ) {
944        $items = $this->getRadioElements( $params );
945
946        $label = $params['label'] ?? '';
947
948        if ( !isset( $params['controlName'] ) ) {
949            $params['controlName'] = 'config_' . $params['var'];
950        }
951
952        if ( !isset( $params['help'] ) ) {
953            $params['help'] = "";
954        }
955
956        $s = "";
957        foreach ( $items as $item ) {
958            $s .= "$item\n";
959        }
960
961        return $this->label( $label, $params['controlName'], $s, $params['help'] );
962    }
963
964    /**
965     * Get a set of labelled radio buttons. You probably want to use getRadioSet(), not this.
966     *
967     * @see getRadioSet
968     *
969     * @param mixed[] $params
970     * @return string[] HTML
971     * @return-taint escaped
972     */
973    public function getRadioElements( $params ) {
974        if ( !isset( $params['controlName'] ) ) {
975            $params['controlName'] = 'config_' . $params['var'];
976        }
977
978        if ( !isset( $params['value'] ) ) {
979            $params['value'] = $this->getVar( $params['var'] );
980        }
981
982        $items = [];
983
984        foreach ( $params['values'] as $value ) {
985            $itemAttribs = [];
986
987            if ( isset( $params['commonAttribs'] ) ) {
988                $itemAttribs = $params['commonAttribs'];
989            }
990
991            if ( isset( $params['itemAttribs'][$value] ) ) {
992                $itemAttribs = $params['itemAttribs'][$value] + $itemAttribs;
993            }
994
995            $checked = $value == $params['value'];
996            $id = $params['controlName'] . '_' . $value;
997            $itemAttribs['id'] = $id;
998            $itemAttribs['tabindex'] = $this->nextTabIndex();
999            Html::addClass( $itemAttribs['class'], 'cdx-radio__input' );
1000
1001            $radioText = $this->parse(
1002                isset( $params['itemLabels'] ) ?
1003                    wfMessage( $params['itemLabels'][$value] )->plain() :
1004                    wfMessage( $params['itemLabelPrefix'] . strtolower( $value ) )->plain()
1005            );
1006            $items[$value] =
1007                '<span class="cdx-radio">' .
1008                '<span class="cdx-radio__wrapper">' .
1009                Html::radio( $params['controlName'], $checked, $itemAttribs + [ 'value' => $value ] ) .
1010                '<span class="cdx-radio__icon"></span>' .
1011                '<span class="cdx-radio__label cdx-label">' .
1012                Html::rawElement(
1013                    'label',
1014                    [ 'for' => $id, 'class' => 'cdx-label__label' ],
1015                    '<span class="cdx-label__label__text">' . $radioText . '</span>'
1016                ) . '</span></span></span>';
1017        }
1018
1019        return $items;
1020    }
1021
1022    /**
1023     * Output an error or warning box using a Status object.
1024     *
1025     * @param Status $status
1026     */
1027    public function showStatusBox( $status ) {
1028        if ( !$status->isGood() ) {
1029            $html = $status->getHTML();
1030
1031            if ( $status->isOK() ) {
1032                $box = Html::warningBox( $html, 'config-warning-box' );
1033            } else {
1034                $box = Html::errorBox( $html, '', 'config-error-box' );
1035            }
1036
1037            $this->output->addHTML( $box );
1038        }
1039    }
1040
1041    /**
1042     * Convenience function to set variables based on form data.
1043     * Assumes that variables containing "password" in the name are (potentially
1044     * fake) passwords.
1045     *
1046     * @param string[] $varNames
1047     * @param string $prefix The prefix added to variables to obtain form names
1048     *
1049     * @return string[]
1050     */
1051    public function setVarsFromRequest( $varNames, $prefix = 'config_' ) {
1052        $newValues = [];
1053
1054        foreach ( $varNames as $name ) {
1055            $value = $this->request->getVal( $prefix . $name );
1056            // T32524, do not trim passwords
1057            if ( $value !== null && stripos( $name, 'password' ) === false ) {
1058                $value = trim( $value );
1059            }
1060            $newValues[$name] = $value;
1061
1062            if ( $value === null ) {
1063                // Checkbox?
1064                $this->setVar( $name, false );
1065            } elseif ( stripos( $name, 'password' ) !== false ) {
1066                $this->setPassword( $name, $value );
1067            } else {
1068                $this->setVar( $name, $value );
1069            }
1070        }
1071
1072        return $newValues;
1073    }
1074
1075    /**
1076     * Helper for WebInstallerOutput
1077     *
1078     * @internal For use by WebInstallerOutput
1079     * @param string $page
1080     * @return string
1081     */
1082    public function getDocUrl( $page ) {
1083        $query = [ 'page' => $page ];
1084
1085        if ( in_array( $this->currentPageName, $this->pageSequence ) ) {
1086            $query['lastPage'] = $this->currentPageName;
1087        }
1088
1089        return $this->getUrl( $query );
1090    }
1091
1092    /**
1093     * Helper for sidebar links.
1094     *
1095     * @internal For use in WebInstallerOutput class
1096     * @param string $url
1097     * @param string $linkText
1098     * @return string HTML
1099     */
1100    public function makeLinkItem( $url, $linkText ) {
1101        return Html::rawElement( 'li', [],
1102            Html::element( 'a', [ 'href' => $url ], $linkText )
1103        );
1104    }
1105
1106    /**
1107     * If the software package wants the LocalSettings.php file
1108     * to be placed in a specific location, override this function
1109     * (see mw-config/overrides/README) to return the path of
1110     * where the file should be saved, or false for a generic
1111     * "in the base of your install"
1112     *
1113     * @since 1.27
1114     * @return string|bool
1115     */
1116    public function getLocalSettingsLocation() {
1117        return false;
1118    }
1119
1120    /**
1121     * @return bool
1122     */
1123    public function envCheckPath() {
1124        // PHP_SELF isn't available sometimes, such as when PHP is CGI but
1125        // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
1126        // to get the path to the current script... hopefully it's reliable. SIGH
1127        $path = false;
1128        if ( !empty( $_SERVER['PHP_SELF'] ) ) {
1129            $path = $_SERVER['PHP_SELF'];
1130        } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
1131            $path = $_SERVER['SCRIPT_NAME'];
1132        }
1133        if ( $path === false ) {
1134            $this->showError( 'config-no-uri' );
1135            return false;
1136        }
1137
1138        return parent::envCheckPath();
1139    }
1140
1141    /** @inheritDoc */
1142    protected function detectWebPaths() {
1143        // PHP_SELF isn't available sometimes, such as when PHP is CGI but
1144        // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
1145        // to get the path to the current script... hopefully it's reliable. SIGH
1146        $path = false;
1147        if ( !empty( $_SERVER['PHP_SELF'] ) ) {
1148            $path = $_SERVER['PHP_SELF'];
1149        } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
1150            $path = $_SERVER['SCRIPT_NAME'];
1151        }
1152        if ( $path !== false ) {
1153            $scriptPath = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
1154
1155            return [
1156                'wgScriptPath' => "$scriptPath",
1157                // Update variables set from Setup.php that are derived from wgScriptPath
1158                'wgScript' => "$scriptPath/index.php",
1159                'wgLoadScript' => "$scriptPath/load.php",
1160                'wgStylePath' => "$scriptPath/skins",
1161                'wgLocalStylePath' => "$scriptPath/skins",
1162                'wgExtensionAssetsPath' => "$scriptPath/extensions",
1163                'wgUploadPath' => "$scriptPath/images",
1164                'wgResourceBasePath' => "$scriptPath",
1165            ];
1166        }
1167        return [];
1168    }
1169
1170    /**
1171     * @return string
1172     */
1173    protected function envGetDefaultServer() {
1174        $assumeProxiesUseDefaultProtocolPorts =
1175            $this->getVar( 'wgAssumeProxiesUseDefaultProtocolPorts' );
1176
1177        return WebRequest::detectServer( $assumeProxiesUseDefaultProtocolPorts );
1178    }
1179
1180    /**
1181     * @return string
1182     */
1183    public function getDefaultServer() {
1184        return $this->envGetDefaultServer();
1185    }
1186
1187    /**
1188     * Actually output LocalSettings.php for download
1189     */
1190    private function outputLS() {
1191        ContentSecurityPolicy::sendRestrictiveHeader();
1192        $this->request->response()->header( 'Content-type: application/x-httpd-php' );
1193        $this->request->response()->header(
1194            'Content-Disposition: attachment; filename="LocalSettings.php"'
1195        );
1196
1197        $ls = InstallerOverrides::getLocalSettingsGenerator( $this );
1198        $rightsProfile = $this->rightsProfiles[$this->getVar( '_RightsProfile' )];
1199        foreach ( $rightsProfile as $group => $rightsArr ) {
1200            $ls->setGroupRights( $group, $rightsArr );
1201        }
1202        echo $ls->getText();
1203    }
1204
1205    /**
1206     * Output stylesheet for web installer pages
1207     */
1208    public function outputCss() {
1209        $this->request->response()->header( 'Content-type: text/css' );
1210        ContentSecurityPolicy::sendRestrictiveHeader();
1211        echo $this->output->getCSS();
1212    }
1213
1214    /**
1215     * @return string[]
1216     */
1217    public function getPhpErrors() {
1218        return $this->phpErrors;
1219    }
1220
1221    /**
1222     * Determine whether the current database needs to be upgraded, i.e. whether
1223     * it already has MediaWiki tables.
1224     *
1225     * @return bool
1226     */
1227    public function needsUpgrade() {
1228        return $this->getDBInstaller()->needsUpgrade();
1229    }
1230
1231    /**
1232     * Perform database upgrades
1233     *
1234     * @return bool
1235     */
1236    public function doUpgrade() {
1237        $dbInstaller = $this->getDBInstaller();
1238        $dbInstaller->preUpgrade();
1239
1240        $taskList = new TaskList;
1241        $taskFactory = $this->getTaskFactory();
1242        $taskFactory->registerWebUpgradeTasks( $taskList );
1243        $taskRunner = new TaskRunner( $taskList, $taskFactory, TaskFactory::PROFILE_WEB_UPGRADE );
1244
1245        ob_start( $this->outputHandler( ... ) );
1246        try {
1247            $status = $taskRunner->execute();
1248            $ret = $status->isOK();
1249
1250            $this->setVar( '_UpgradeDone', true );
1251        } catch ( Exception $e ) {
1252            // TODO: Should this use MWExceptionRenderer?
1253            echo "\nAn error occurred:\n";
1254            echo $e->getMessage();
1255            $ret = false;
1256        }
1257        ob_end_flush();
1258
1259        return $ret;
1260    }
1261
1262    public function outputHandler( string $string ): string {
1263        return htmlspecialchars( $string );
1264    }
1265
1266}