Fix typo of Minimum in variable name
[lhc/web/wiklou.git] / includes / installer / WebInstaller.php
1 <?php
2 /**
3 * Core installer web interface.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Deployment
22 */
23
24 use MediaWiki\MediaWikiServices;
25
26 /**
27 * Class for the core installer web interface.
28 *
29 * @ingroup Deployment
30 * @since 1.17
31 */
32 class 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 'Readme',
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 * Name of the page we're on
129 *
130 * @var string
131 */
132 protected $currentPageName;
133
134 /**
135 * @param WebRequest $request
136 */
137 public function __construct( WebRequest $request ) {
138 parent::__construct();
139 $this->output = new WebInstallerOutput( $this );
140 $this->request = $request;
141
142 // Add parser hooks
143 $parser = MediaWikiServices::getInstance()->getParser();
144 $parser->setHook( 'downloadlink', [ $this, 'downloadLinkHook' ] );
145 $parser->setHook( 'doclink', [ $this, 'docLink' ] );
146 }
147
148 /**
149 * Main entry point.
150 *
151 * @param array[] $session Initial session array
152 *
153 * @return array[] New session array
154 */
155 public function execute( array $session ) {
156 $this->session = $session;
157
158 if ( isset( $session['settings'] ) ) {
159 $this->settings = $session['settings'] + $this->settings;
160 // T187586 MediaWikiServices works with globals
161 foreach ( $this->settings as $key => $val ) {
162 $GLOBALS[$key] = $val;
163 }
164 }
165
166 $this->setupLanguage();
167
168 if ( ( $this->getVar( '_InstallDone' ) || $this->getVar( '_UpgradeDone' ) )
169 && $this->request->getVal( 'localsettings' )
170 ) {
171 $this->outputLS();
172 return $this->session;
173 }
174
175 $isCSS = $this->request->getVal( 'css' );
176 if ( $isCSS ) {
177 $this->outputCss();
178 return $this->session;
179 }
180
181 $this->happyPages = $session['happyPages'] ?? [];
182
183 $this->skippedPages = $session['skippedPages'] ?? [];
184
185 $lowestUnhappy = $this->getLowestUnhappy();
186
187 # Special case for Creative Commons partner chooser box.
188 if ( $this->request->getVal( 'SubmitCC' ) ) {
189 $page = $this->getPageByName( 'Options' );
190 $this->output->useShortHeader();
191 $this->output->allowFrames();
192 $page->submitCC();
193
194 return $this->finish();
195 }
196
197 if ( $this->request->getVal( 'ShowCC' ) ) {
198 $page = $this->getPageByName( 'Options' );
199 $this->output->useShortHeader();
200 $this->output->allowFrames();
201 $this->output->addHTML( $page->getCCDoneBox() );
202
203 return $this->finish();
204 }
205
206 # Get the page name.
207 $pageName = $this->request->getVal( 'page' );
208
209 if ( in_array( $pageName, $this->otherPages ) ) {
210 # Out of sequence
211 $pageId = false;
212 $page = $this->getPageByName( $pageName );
213 } else {
214 # Main sequence
215 if ( !$pageName || !in_array( $pageName, $this->pageSequence ) ) {
216 $pageId = $lowestUnhappy;
217 } else {
218 $pageId = array_search( $pageName, $this->pageSequence );
219 }
220
221 # If necessary, move back to the lowest-numbered unhappy page
222 if ( $pageId > $lowestUnhappy ) {
223 $pageId = $lowestUnhappy;
224 if ( $lowestUnhappy == 0 ) {
225 # Knocked back to start, possible loss of session data.
226 $this->showSessionWarning = true;
227 }
228 }
229
230 $pageName = $this->pageSequence[$pageId];
231 $page = $this->getPageByName( $pageName );
232 }
233
234 # If a back button was submitted, go back without submitting the form data.
235 if ( $this->request->wasPosted() && $this->request->getBool( 'submit-back' ) ) {
236 if ( $this->request->getVal( 'lastPage' ) ) {
237 $nextPage = $this->request->getVal( 'lastPage' );
238 } elseif ( $pageId !== false ) {
239 # Main sequence page
240 # Skip the skipped pages
241 $nextPageId = $pageId;
242
243 do {
244 $nextPageId--;
245 $nextPage = $this->pageSequence[$nextPageId];
246 } while ( isset( $this->skippedPages[$nextPage] ) );
247 } else {
248 $nextPage = $this->pageSequence[$lowestUnhappy];
249 }
250
251 $this->output->redirect( $this->getUrl( [ 'page' => $nextPage ] ) );
252
253 return $this->finish();
254 }
255
256 # Execute the page.
257 $this->currentPageName = $page->getName();
258 $this->startPageWrapper( $pageName );
259
260 if ( $page->isSlow() ) {
261 $this->disableTimeLimit();
262 }
263
264 $result = $page->execute();
265
266 $this->endPageWrapper();
267
268 if ( $result == 'skip' ) {
269 # Page skipped without explicit submission.
270 # Skip it when we click "back" so that we don't just go forward again.
271 $this->skippedPages[$pageName] = true;
272 $result = 'continue';
273 } else {
274 unset( $this->skippedPages[$pageName] );
275 }
276
277 # If it was posted, the page can request a continue to the next page.
278 if ( $result === 'continue' && !$this->output->headerDone() ) {
279 if ( $pageId !== false ) {
280 $this->happyPages[$pageId] = true;
281 }
282
283 $lowestUnhappy = $this->getLowestUnhappy();
284
285 if ( $this->request->getVal( 'lastPage' ) ) {
286 $nextPage = $this->request->getVal( 'lastPage' );
287 } elseif ( $pageId !== false ) {
288 $nextPage = $this->pageSequence[$pageId + 1];
289 } else {
290 $nextPage = $this->pageSequence[$lowestUnhappy];
291 }
292
293 if ( array_search( $nextPage, $this->pageSequence ) > $lowestUnhappy ) {
294 $nextPage = $this->pageSequence[$lowestUnhappy];
295 }
296
297 $this->output->redirect( $this->getUrl( [ 'page' => $nextPage ] ) );
298 }
299
300 return $this->finish();
301 }
302
303 /**
304 * Find the next page in sequence that hasn't been completed
305 * @return int
306 */
307 public function getLowestUnhappy() {
308 if ( count( $this->happyPages ) == 0 ) {
309 return 0;
310 } else {
311 return max( array_keys( $this->happyPages ) ) + 1;
312 }
313 }
314
315 /**
316 * Start the PHP session. This may be called before execute() to start the PHP session.
317 *
318 * @throws Exception
319 * @return bool
320 */
321 public function startSession() {
322 if ( wfIniGetBool( 'session.auto_start' ) || session_id() ) {
323 // Done already
324 return true;
325 }
326
327 $this->phpErrors = [];
328 set_error_handler( [ $this, 'errorHandler' ] );
329 try {
330 session_name( 'mw_installer_session' );
331 session_start();
332 } catch ( Exception $e ) {
333 restore_error_handler();
334 throw $e;
335 }
336 restore_error_handler();
337
338 if ( $this->phpErrors ) {
339 return false;
340 }
341
342 return true;
343 }
344
345 /**
346 * Get a hash of data identifying this MW installation.
347 *
348 * This is used by mw-config/index.php to prevent multiple installations of MW
349 * on the same cookie domain from interfering with each other.
350 *
351 * @return string
352 */
353 public function getFingerprint() {
354 // Get the base URL of the installation
355 $url = $this->request->getFullRequestURL();
356 if ( preg_match( '!^(.*\?)!', $url, $m ) ) {
357 // Trim query string
358 $url = $m[1];
359 }
360 if ( preg_match( '!^(.*)/[^/]*/[^/]*$!', $url, $m ) ) {
361 // This... seems to try to get the base path from
362 // the /mw-config/index.php. Kinda scary though?
363 $url = $m[1];
364 }
365
366 return md5( serialize( [
367 'local path' => dirname( __DIR__ ),
368 'url' => $url,
369 'version' => $GLOBALS['wgVersion']
370 ] ) );
371 }
372
373 /**
374 * Show an error message in a box. Parameters are like wfMessage(), or
375 * alternatively, pass a Message object in.
376 * @param string|Message $msg
377 * @param mixed ...$params
378 */
379 public function showError( $msg, ...$params ) {
380 if ( !( $msg instanceof Message ) ) {
381 $msg = wfMessage(
382 $msg,
383 array_map( 'htmlspecialchars', $params )
384 );
385 }
386 $text = $msg->useDatabase( false )->plain();
387 $this->output->addHTML( $this->getErrorBox( $text ) );
388 }
389
390 /**
391 * Temporary error handler for session start debugging.
392 *
393 * @param int $errno Unused
394 * @param string $errstr
395 */
396 public function errorHandler( $errno, $errstr ) {
397 $this->phpErrors[] = $errstr;
398 }
399
400 /**
401 * Clean up from execute()
402 *
403 * @return array[]
404 */
405 public function finish() {
406 $this->output->output();
407
408 $this->session['happyPages'] = $this->happyPages;
409 $this->session['skippedPages'] = $this->skippedPages;
410 $this->session['settings'] = $this->settings;
411
412 return $this->session;
413 }
414
415 /**
416 * We're restarting the installation, reset the session, happyPages, etc
417 */
418 public function reset() {
419 $this->session = [];
420 $this->happyPages = [];
421 $this->settings = [];
422 }
423
424 /**
425 * Get a URL for submission back to the same script.
426 *
427 * @param string[] $query
428 *
429 * @return string
430 */
431 public function getUrl( $query = [] ) {
432 $url = $this->request->getRequestURL();
433 # Remove existing query
434 $url = preg_replace( '/\?.*$/', '', $url );
435
436 if ( $query ) {
437 $url .= '?' . wfArrayToCgi( $query );
438 }
439
440 return $url;
441 }
442
443 /**
444 * Get a WebInstallerPage by name.
445 *
446 * @param string $pageName
447 * @return WebInstallerPage
448 */
449 public function getPageByName( $pageName ) {
450 $pageClass = 'WebInstaller' . $pageName;
451
452 return new $pageClass( $this );
453 }
454
455 /**
456 * Get a session variable.
457 *
458 * @param string $name
459 * @param array|null $default
460 *
461 * @return array
462 */
463 public function getSession( $name, $default = null ) {
464 return $this->session[$name] ?? $default;
465 }
466
467 /**
468 * Set a session variable.
469 *
470 * @param string $name Key for the variable
471 * @param mixed $value
472 */
473 public function setSession( $name, $value ) {
474 $this->session[$name] = $value;
475 }
476
477 /**
478 * Get the next tabindex attribute value.
479 *
480 * @return int
481 */
482 public function nextTabIndex() {
483 return $this->tabIndex++;
484 }
485
486 /**
487 * Initializes language-related variables.
488 */
489 public function setupLanguage() {
490 global $wgLang, $wgContLang, $wgLanguageCode;
491
492 if ( $this->getSession( 'test' ) === null && !$this->request->wasPosted() ) {
493 $wgLanguageCode = $this->getAcceptLanguage();
494 $wgLang = Language::factory( $wgLanguageCode );
495 RequestContext::getMain()->setLanguage( $wgLang );
496 $this->setVar( 'wgLanguageCode', $wgLanguageCode );
497 $this->setVar( '_UserLang', $wgLanguageCode );
498 } else {
499 $wgLanguageCode = $this->getVar( 'wgLanguageCode' );
500 }
501 $wgContLang = MediaWikiServices::getInstance()->getContentLanguage();
502 }
503
504 /**
505 * Retrieves MediaWiki language from Accept-Language HTTP header.
506 *
507 * @return string
508 */
509 public function getAcceptLanguage() {
510 global $wgLanguageCode, $wgRequest;
511
512 $mwLanguages = Language::fetchLanguageNames( null, 'mwfile' );
513 $headerLanguages = array_keys( $wgRequest->getAcceptLang() );
514
515 foreach ( $headerLanguages as $lang ) {
516 if ( isset( $mwLanguages[$lang] ) ) {
517 return $lang;
518 }
519 }
520
521 return $wgLanguageCode;
522 }
523
524 /**
525 * Called by execute() before page output starts, to show a page list.
526 *
527 * @param string $currentPageName
528 */
529 private function startPageWrapper( $currentPageName ) {
530 $s = "<div class=\"config-page-wrapper\">\n";
531 $s .= "<div class=\"config-page\">\n";
532 $s .= "<div class=\"config-page-list\"><ul>\n";
533 $lastHappy = -1;
534
535 foreach ( $this->pageSequence as $id => $pageName ) {
536 $happy = !empty( $this->happyPages[$id] );
537 $s .= $this->getPageListItem(
538 $pageName,
539 $happy || $lastHappy == $id - 1,
540 $currentPageName
541 );
542
543 if ( $happy ) {
544 $lastHappy = $id;
545 }
546 }
547
548 $s .= "</ul><br/><ul>\n";
549 $s .= $this->getPageListItem( 'Restart', true, $currentPageName );
550 // End list pane
551 $s .= "</ul></div>\n";
552
553 // Messages:
554 // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
555 // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
556 // config-page-complete, config-page-restart, config-page-readme, config-page-releasenotes,
557 // config-page-copying, config-page-upgradedoc, config-page-existingwiki
558 $s .= Html::element( 'h2', [],
559 wfMessage( 'config-page-' . strtolower( $currentPageName ) )->text() );
560
561 $this->output->addHTMLNoFlush( $s );
562 }
563
564 /**
565 * Get a list item for the page list.
566 *
567 * @param string $pageName
568 * @param bool $enabled
569 * @param string $currentPageName
570 *
571 * @return string
572 */
573 private function getPageListItem( $pageName, $enabled, $currentPageName ) {
574 $s = "<li class=\"config-page-list-item\">";
575
576 // Messages:
577 // config-page-language, config-page-welcome, config-page-dbconnect, config-page-upgrade,
578 // config-page-dbsettings, config-page-name, config-page-options, config-page-install,
579 // config-page-complete, config-page-restart, config-page-readme, config-page-releasenotes,
580 // config-page-copying, config-page-upgradedoc, config-page-existingwiki
581 $name = wfMessage( 'config-page-' . strtolower( $pageName ) )->text();
582
583 if ( $enabled ) {
584 $query = [ 'page' => $pageName ];
585
586 if ( !in_array( $pageName, $this->pageSequence ) ) {
587 if ( in_array( $currentPageName, $this->pageSequence ) ) {
588 $query['lastPage'] = $currentPageName;
589 }
590
591 $link = Html::element( 'a',
592 [
593 'href' => $this->getUrl( $query )
594 ],
595 $name
596 );
597 } else {
598 $link = htmlspecialchars( $name );
599 }
600
601 if ( $pageName == $currentPageName ) {
602 $s .= "<span class=\"config-page-current\">$link</span>";
603 } else {
604 $s .= $link;
605 }
606 } else {
607 $s .= Html::element( 'span',
608 [
609 'class' => 'config-page-disabled'
610 ],
611 $name
612 );
613 }
614
615 $s .= "</li>\n";
616
617 return $s;
618 }
619
620 /**
621 * Output some stuff after a page is finished.
622 */
623 private function endPageWrapper() {
624 $this->output->addHTMLNoFlush(
625 "<div class=\"visualClear\"></div>\n" .
626 "</div>\n" .
627 "<div class=\"visualClear\"></div>\n" .
628 "</div>" );
629 }
630
631 /**
632 * Get HTML for an error box with an icon.
633 *
634 * @param string $text Wikitext, get this with wfMessage()->plain()
635 *
636 * @return string
637 */
638 public function getErrorBox( $text ) {
639 return $this->getInfoBox( $text, 'critical-32.png', 'config-error-box' );
640 }
641
642 /**
643 * Get HTML for a warning box with an icon.
644 *
645 * @param string $text Wikitext, get this with wfMessage()->plain()
646 *
647 * @return string
648 */
649 public function getWarningBox( $text ) {
650 return $this->getInfoBox( $text, 'warning-32.png', 'config-warning-box' );
651 }
652
653 /**
654 * Get HTML for an info box with an icon.
655 *
656 * @param string $text Wikitext, get this with wfMessage()->plain()
657 * @param string|bool $icon Icon name, file in mw-config/images. Default: false
658 * @param string|bool $class Additional class name to add to the wrapper div. Default: false.
659 *
660 * @return string
661 */
662 public function getInfoBox( $text, $icon = false, $class = false ) {
663 $text = $this->parse( $text, true );
664 $icon = ( $icon == false ) ?
665 'images/info-32.png' :
666 'images/' . $icon;
667 $alt = wfMessage( 'config-information' )->text();
668
669 return Html::infoBox( $text, $icon, $alt, $class );
670 }
671
672 /**
673 * Get small text indented help for a preceding form field.
674 * Parameters like wfMessage().
675 *
676 * @param string $msg
677 * @return string
678 */
679 public function getHelpBox( $msg, ...$args ) {
680 $args = array_map( 'htmlspecialchars', $args );
681 $text = wfMessage( $msg, $args )->useDatabase( false )->plain();
682 $html = $this->parse( $text, true );
683
684 return "<div class=\"config-help-field-container\">\n" .
685 "<span class=\"config-help-field-hint\" title=\"" .
686 wfMessage( 'config-help-tooltip' )->escaped() . "\">" .
687 wfMessage( 'config-help' )->escaped() . "</span>\n" .
688 "<div class=\"config-help-field-data\">" . $html . "</div>\n" .
689 "</div>\n";
690 }
691
692 /**
693 * Output a help box.
694 * @param string $msg Key for wfMessage()
695 * @param mixed ...$params
696 */
697 public function showHelpBox( $msg, ...$params ) {
698 $html = $this->getHelpBox( $msg, ...$params );
699 $this->output->addHTML( $html );
700 }
701
702 /**
703 * Show a short informational message.
704 * Output looks like a list.
705 *
706 * @param string $msg
707 * @param mixed ...$params
708 */
709 public function showMessage( $msg, ...$params ) {
710 $html = '<div class="config-message">' .
711 $this->parse( wfMessage( $msg, $params )->useDatabase( false )->plain() ) .
712 "</div>\n";
713 $this->output->addHTML( $html );
714 }
715
716 /**
717 * @param Status $status
718 */
719 public function showStatusMessage( Status $status ) {
720 $errors = array_merge( $status->getErrorsArray(), $status->getWarningsArray() );
721 foreach ( $errors as $error ) {
722 $this->showMessage( ...$error );
723 }
724 }
725
726 /**
727 * Label a control by wrapping a config-input div around it and putting a
728 * label before it.
729 *
730 * @param string $msg
731 * @param string $forId
732 * @param string $contents
733 * @param string $helpData
734 * @return string
735 */
736 public function label( $msg, $forId, $contents, $helpData = "" ) {
737 if ( strval( $msg ) == '' ) {
738 $labelText = "\u{00A0}";
739 } else {
740 $labelText = wfMessage( $msg )->escaped();
741 }
742
743 $attributes = [ 'class' => 'config-label' ];
744
745 if ( $forId ) {
746 $attributes['for'] = $forId;
747 }
748
749 return "<div class=\"config-block\">\n" .
750 " <div class=\"config-block-label\">\n" .
751 Xml::tags( 'label',
752 $attributes,
753 $labelText
754 ) . "\n" .
755 $helpData .
756 " </div>\n" .
757 " <div class=\"config-block-elements\">\n" .
758 $contents .
759 " </div>\n" .
760 "</div>\n";
761 }
762
763 /**
764 * Get a labelled text box to configure a variable.
765 *
766 * @param mixed[] $params
767 * Parameters are:
768 * var: The variable to be configured (required)
769 * label: The message name for the label (required)
770 * attribs: Additional attributes for the input element (optional)
771 * controlName: The name for the input element (optional)
772 * value: The current value of the variable (optional)
773 * help: The html for the help text (optional)
774 *
775 * @return string
776 */
777 public function getTextBox( $params ) {
778 if ( !isset( $params['controlName'] ) ) {
779 $params['controlName'] = 'config_' . $params['var'];
780 }
781
782 if ( !isset( $params['value'] ) ) {
783 $params['value'] = $this->getVar( $params['var'] );
784 }
785
786 if ( !isset( $params['attribs'] ) ) {
787 $params['attribs'] = [];
788 }
789 if ( !isset( $params['help'] ) ) {
790 $params['help'] = "";
791 }
792
793 return $this->label(
794 $params['label'],
795 $params['controlName'],
796 Xml::input(
797 $params['controlName'],
798 30, // intended to be overridden by CSS
799 $params['value'],
800 $params['attribs'] + [
801 'id' => $params['controlName'],
802 'class' => 'config-input-text',
803 'tabindex' => $this->nextTabIndex()
804 ]
805 ),
806 $params['help']
807 );
808 }
809
810 /**
811 * Get a labelled textarea to configure a variable
812 *
813 * @param mixed[] $params
814 * Parameters are:
815 * var: The variable to be configured (required)
816 * label: The message name for the label (required)
817 * attribs: Additional attributes for the input element (optional)
818 * controlName: The name for the input element (optional)
819 * value: The current value of the variable (optional)
820 * help: The html for the help text (optional)
821 *
822 * @return string
823 */
824 public function getTextArea( $params ) {
825 if ( !isset( $params['controlName'] ) ) {
826 $params['controlName'] = 'config_' . $params['var'];
827 }
828
829 if ( !isset( $params['value'] ) ) {
830 $params['value'] = $this->getVar( $params['var'] );
831 }
832
833 if ( !isset( $params['attribs'] ) ) {
834 $params['attribs'] = [];
835 }
836 if ( !isset( $params['help'] ) ) {
837 $params['help'] = "";
838 }
839
840 return $this->label(
841 $params['label'],
842 $params['controlName'],
843 Xml::textarea(
844 $params['controlName'],
845 $params['value'],
846 30,
847 5,
848 $params['attribs'] + [
849 'id' => $params['controlName'],
850 'class' => 'config-input-text',
851 'tabindex' => $this->nextTabIndex()
852 ]
853 ),
854 $params['help']
855 );
856 }
857
858 /**
859 * Get a labelled password box to configure a variable.
860 *
861 * Implements password hiding
862 * @param mixed[] $params
863 * Parameters are:
864 * var: The variable to be configured (required)
865 * label: The message name for the label (required)
866 * attribs: Additional attributes for the input element (optional)
867 * controlName: The name for the input element (optional)
868 * value: The current value of the variable (optional)
869 * help: The html for the help text (optional)
870 *
871 * @return string
872 */
873 public function getPasswordBox( $params ) {
874 if ( !isset( $params['value'] ) ) {
875 $params['value'] = $this->getVar( $params['var'] );
876 }
877
878 if ( !isset( $params['attribs'] ) ) {
879 $params['attribs'] = [];
880 }
881
882 $params['value'] = $this->getFakePassword( $params['value'] );
883 $params['attribs']['type'] = 'password';
884
885 return $this->getTextBox( $params );
886 }
887
888 /**
889 * Get a labelled checkbox to configure a boolean variable.
890 *
891 * @param mixed[] $params
892 * Parameters are:
893 * var: The variable to be configured (required)
894 * label: The message name for the label (required)
895 * labelAttribs:Additional attributes for the label element (optional)
896 * attribs: Additional attributes for the input element (optional)
897 * controlName: The name for the input element (optional)
898 * value: The current value of the variable (optional)
899 * help: The html for the help text (optional)
900 *
901 * @return string
902 */
903 public function getCheckBox( $params ) {
904 if ( !isset( $params['controlName'] ) ) {
905 $params['controlName'] = 'config_' . $params['var'];
906 }
907
908 if ( !isset( $params['value'] ) ) {
909 $params['value'] = $this->getVar( $params['var'] );
910 }
911
912 if ( !isset( $params['attribs'] ) ) {
913 $params['attribs'] = [];
914 }
915 if ( !isset( $params['help'] ) ) {
916 $params['help'] = "";
917 }
918 if ( !isset( $params['labelAttribs'] ) ) {
919 $params['labelAttribs'] = [];
920 }
921 $labelText = $params['rawtext'] ?? $this->parse( wfMessage( $params['label'] )->plain() );
922
923 return "<div class=\"config-input-check\">\n" .
924 $params['help'] .
925 Html::rawElement(
926 'label',
927 $params['labelAttribs'],
928 Xml::check(
929 $params['controlName'],
930 $params['value'],
931 $params['attribs'] + [
932 'id' => $params['controlName'],
933 'tabindex' => $this->nextTabIndex(),
934 ]
935 ) .
936 $labelText . "\n"
937 ) .
938 "</div>\n";
939 }
940
941 /**
942 * Get a set of labelled radio buttons.
943 *
944 * @param mixed[] $params
945 * Parameters are:
946 * var: The variable to be configured (required)
947 * label: The message name for the label (required)
948 * itemLabelPrefix: The message name prefix for the item labels (required)
949 * itemLabels: List of message names to use for the item labels instead
950 * of itemLabelPrefix, keyed by values
951 * values: List of allowed values (required)
952 * itemAttribs: Array of attribute arrays, outer key is the value name (optional)
953 * commonAttribs: Attribute array applied to all items
954 * controlName: The name for the input element (optional)
955 * value: The current value of the variable (optional)
956 * help: The html for the help text (optional)
957 *
958 * @return string
959 */
960 public function getRadioSet( $params ) {
961 $items = $this->getRadioElements( $params );
962
963 $label = $params['label'] ?? '';
964
965 if ( !isset( $params['controlName'] ) ) {
966 $params['controlName'] = 'config_' . $params['var'];
967 }
968
969 if ( !isset( $params['help'] ) ) {
970 $params['help'] = "";
971 }
972
973 $s = "<ul>\n";
974 foreach ( $items as $value => $item ) {
975 $s .= "<li>$item</li>\n";
976 }
977 $s .= "</ul>\n";
978
979 return $this->label( $label, $params['controlName'], $s, $params['help'] );
980 }
981
982 /**
983 * Get a set of labelled radio buttons. You probably want to use getRadioSet(), not this.
984 *
985 * @see getRadioSet
986 *
987 * @param mixed[] $params
988 * @return array
989 */
990 public function getRadioElements( $params ) {
991 if ( !isset( $params['controlName'] ) ) {
992 $params['controlName'] = 'config_' . $params['var'];
993 }
994
995 if ( !isset( $params['value'] ) ) {
996 $params['value'] = $this->getVar( $params['var'] );
997 }
998
999 $items = [];
1000
1001 foreach ( $params['values'] as $value ) {
1002 $itemAttribs = [];
1003
1004 if ( isset( $params['commonAttribs'] ) ) {
1005 $itemAttribs = $params['commonAttribs'];
1006 }
1007
1008 if ( isset( $params['itemAttribs'][$value] ) ) {
1009 $itemAttribs = $params['itemAttribs'][$value] + $itemAttribs;
1010 }
1011
1012 $checked = $value == $params['value'];
1013 $id = $params['controlName'] . '_' . $value;
1014 $itemAttribs['id'] = $id;
1015 $itemAttribs['tabindex'] = $this->nextTabIndex();
1016
1017 $items[$value] =
1018 Xml::radio( $params['controlName'], $value, $checked, $itemAttribs ) .
1019 "\u{00A0}" .
1020 Xml::tags( 'label', [ 'for' => $id ], $this->parse(
1021 isset( $params['itemLabels'] ) ?
1022 wfMessage( $params['itemLabels'][$value] )->plain() :
1023 wfMessage( $params['itemLabelPrefix'] . strtolower( $value ) )->plain()
1024 ) );
1025 }
1026
1027 return $items;
1028 }
1029
1030 /**
1031 * Output an error or warning box using a Status object.
1032 *
1033 * @param Status $status
1034 */
1035 public function showStatusBox( $status ) {
1036 if ( !$status->isGood() ) {
1037 $text = $status->getWikiText();
1038
1039 if ( $status->isOK() ) {
1040 $box = $this->getWarningBox( $text );
1041 } else {
1042 $box = $this->getErrorBox( $text );
1043 }
1044
1045 $this->output->addHTML( $box );
1046 }
1047 }
1048
1049 /**
1050 * Convenience function to set variables based on form data.
1051 * Assumes that variables containing "password" in the name are (potentially
1052 * fake) passwords.
1053 *
1054 * @param string[] $varNames
1055 * @param string $prefix The prefix added to variables to obtain form names
1056 *
1057 * @return string[]
1058 */
1059 public function setVarsFromRequest( $varNames, $prefix = 'config_' ) {
1060 $newValues = [];
1061
1062 foreach ( $varNames as $name ) {
1063 $value = $this->request->getVal( $prefix . $name );
1064 // T32524, do not trim passwords
1065 if ( stripos( $name, 'password' ) === false ) {
1066 $value = trim( $value );
1067 }
1068 $newValues[$name] = $value;
1069
1070 if ( $value === null ) {
1071 // Checkbox?
1072 $this->setVar( $name, false );
1073 } elseif ( stripos( $name, 'password' ) !== false ) {
1074 $this->setPassword( $name, $value );
1075 } else {
1076 $this->setVar( $name, $value );
1077 }
1078 }
1079
1080 return $newValues;
1081 }
1082
1083 /**
1084 * Helper for Installer::docLink()
1085 *
1086 * @param string $page
1087 *
1088 * @return string
1089 */
1090 protected function getDocUrl( $page ) {
1091 $query = [ 'page' => $page ];
1092
1093 if ( in_array( $this->currentPageName, $this->pageSequence ) ) {
1094 $query['lastPage'] = $this->currentPageName;
1095 }
1096
1097 return $this->getUrl( $query );
1098 }
1099
1100 /**
1101 * Extension tag hook for a documentation link.
1102 *
1103 * @param string $linkText
1104 * @param string[] $attribs
1105 * @param Parser $parser Unused
1106 *
1107 * @return string
1108 */
1109 public function docLink( $linkText, $attribs, $parser ) {
1110 $url = $this->getDocUrl( $attribs['href'] );
1111
1112 return Html::element( 'a', [ 'href' => $url ], $linkText );
1113 }
1114
1115 /**
1116 * Helper for "Download LocalSettings" link on WebInstall_Complete
1117 *
1118 * @param string $text Unused
1119 * @param string[] $attribs Unused
1120 * @param Parser $parser Unused
1121 *
1122 * @return string Html for download link
1123 */
1124 public function downloadLinkHook( $text, $attribs, $parser ) {
1125 $anchor = Html::rawElement( 'a',
1126 [ 'href' => $this->getUrl( [ 'localsettings' => 1 ] ) ],
1127 wfMessage( 'config-download-localsettings' )->parse()
1128 );
1129
1130 return Html::rawElement( 'div', [ 'class' => 'config-download-link' ], $anchor );
1131 }
1132
1133 /**
1134 * If the software package wants the LocalSettings.php file
1135 * to be placed in a specific location, override this function
1136 * (see mw-config/overrides/README) to return the path of
1137 * where the file should be saved, or false for a generic
1138 * "in the base of your install"
1139 *
1140 * @since 1.27
1141 * @return string|bool
1142 */
1143 public function getLocalSettingsLocation() {
1144 return false;
1145 }
1146
1147 /**
1148 * @return bool
1149 */
1150 public function envCheckPath() {
1151 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
1152 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
1153 // to get the path to the current script... hopefully it's reliable. SIGH
1154 $path = false;
1155 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
1156 $path = $_SERVER['PHP_SELF'];
1157 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
1158 $path = $_SERVER['SCRIPT_NAME'];
1159 }
1160 if ( $path === false ) {
1161 $this->showError( 'config-no-uri' );
1162 return false;
1163 }
1164
1165 return parent::envCheckPath();
1166 }
1167
1168 public function envPrepPath() {
1169 parent::envPrepPath();
1170 // PHP_SELF isn't available sometimes, such as when PHP is CGI but
1171 // cgi.fix_pathinfo is disabled. In that case, fall back to SCRIPT_NAME
1172 // to get the path to the current script... hopefully it's reliable. SIGH
1173 $path = false;
1174 if ( !empty( $_SERVER['PHP_SELF'] ) ) {
1175 $path = $_SERVER['PHP_SELF'];
1176 } elseif ( !empty( $_SERVER['SCRIPT_NAME'] ) ) {
1177 $path = $_SERVER['SCRIPT_NAME'];
1178 }
1179 if ( $path !== false ) {
1180 $scriptPath = preg_replace( '{^(.*)/(mw-)?config.*$}', '$1', $path );
1181
1182 $this->setVar( 'wgScriptPath', "$scriptPath" );
1183 // Update variables set from Setup.php that are derived from wgScriptPath
1184 $this->setVar( 'wgScript', "$scriptPath/index.php" );
1185 $this->setVar( 'wgLoadScript', "$scriptPath/load.php" );
1186 $this->setVar( 'wgStylePath', "$scriptPath/skins" );
1187 $this->setVar( 'wgLocalStylePath', "$scriptPath/skins" );
1188 $this->setVar( 'wgExtensionAssetsPath', "$scriptPath/extensions" );
1189 $this->setVar( 'wgUploadPath', "$scriptPath/images" );
1190 $this->setVar( 'wgResourceBasePath', "$scriptPath" );
1191 }
1192 }
1193
1194 /**
1195 * @return string
1196 */
1197 protected function envGetDefaultServer() {
1198 return WebRequest::detectServer();
1199 }
1200
1201 /**
1202 * Actually output LocalSettings.php for download
1203 *
1204 * @suppress SecurityCheck-XSS
1205 */
1206 private function outputLS() {
1207 $this->request->response()->header( 'Content-type: application/x-httpd-php' );
1208 $this->request->response()->header(
1209 'Content-Disposition: attachment; filename="LocalSettings.php"'
1210 );
1211
1212 $ls = InstallerOverrides::getLocalSettingsGenerator( $this );
1213 $rightsProfile = $this->rightsProfiles[$this->getVar( '_RightsProfile' )];
1214 foreach ( $rightsProfile as $group => $rightsArr ) {
1215 $ls->setGroupRights( $group, $rightsArr );
1216 }
1217 echo $ls->getText();
1218 }
1219
1220 /**
1221 * Output stylesheet for web installer pages
1222 */
1223 public function outputCss() {
1224 $this->request->response()->header( 'Content-type: text/css' );
1225 echo $this->output->getCSS();
1226 }
1227
1228 /**
1229 * @return string[]
1230 */
1231 public function getPhpErrors() {
1232 return $this->phpErrors;
1233 }
1234
1235 }