WebInstaller.php 32 KB

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