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