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