Installer.php 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815
  1. <?php
  2. /**
  3. * Base code for MediaWiki installer.
  4. *
  5. * DO NOT PATCH THIS FILE IF YOU NEED TO CHANGE INSTALLER BEHAVIOR IN YOUR PACKAGE!
  6. * See mw-config/overrides/README for details.
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program; if not, write to the Free Software Foundation, Inc.,
  20. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. * http://www.gnu.org/copyleft/gpl.html
  22. *
  23. * @file
  24. * @ingroup Deployment
  25. */
  26. use MediaWiki\Interwiki\NullInterwikiLookup;
  27. use MediaWiki\MediaWikiServices;
  28. use MediaWiki\Shell\Shell;
  29. /**
  30. * This documentation group collects source code files with deployment functionality.
  31. *
  32. * @defgroup Deployment Deployment
  33. */
  34. /**
  35. * Base installer class.
  36. *
  37. * This class provides the base for installation and update functionality
  38. * for both MediaWiki core and extensions.
  39. *
  40. * @ingroup Deployment
  41. * @since 1.17
  42. */
  43. abstract class Installer {
  44. /**
  45. * The oldest version of PCRE we can support.
  46. *
  47. * Defining this is necessary because PHP may be linked with a system version
  48. * of PCRE, which may be older than that bundled with the minimum PHP version.
  49. */
  50. const MINIMUM_PCRE_VERSION = '7.2';
  51. /**
  52. * @var array
  53. */
  54. protected $settings;
  55. /**
  56. * List of detected DBs, access using getCompiledDBs().
  57. *
  58. * @var array
  59. */
  60. protected $compiledDBs;
  61. /**
  62. * Cached DB installer instances, access using getDBInstaller().
  63. *
  64. * @var array
  65. */
  66. protected $dbInstallers = [];
  67. /**
  68. * Minimum memory size in MB.
  69. *
  70. * @var int
  71. */
  72. protected $minMemorySize = 50;
  73. /**
  74. * Cached Title, used by parse().
  75. *
  76. * @var Title
  77. */
  78. protected $parserTitle;
  79. /**
  80. * Cached ParserOptions, used by parse().
  81. *
  82. * @var ParserOptions
  83. */
  84. protected $parserOptions;
  85. /**
  86. * Known database types. These correspond to the class names <type>Installer,
  87. * and are also MediaWiki database types valid for $wgDBtype.
  88. *
  89. * To add a new type, create a <type>Installer class and a Database<type>
  90. * class, and add a config-type-<type> message to MessagesEn.php.
  91. *
  92. * @var array
  93. */
  94. protected static $dbTypes = [
  95. 'mysql',
  96. 'postgres',
  97. 'oracle',
  98. 'mssql',
  99. 'sqlite',
  100. ];
  101. /**
  102. * A list of environment check methods called by doEnvironmentChecks().
  103. * These may output warnings using showMessage(), and/or abort the
  104. * installation process by returning false.
  105. *
  106. * For the WebInstaller these are only called on the Welcome page,
  107. * if these methods have side-effects that should affect later page loads
  108. * (as well as the generated stylesheet), use envPreps instead.
  109. *
  110. * @var array
  111. */
  112. protected $envChecks = [
  113. 'envCheckDB',
  114. 'envCheckBrokenXML',
  115. 'envCheckPCRE',
  116. 'envCheckMemory',
  117. 'envCheckCache',
  118. 'envCheckModSecurity',
  119. 'envCheckDiff3',
  120. 'envCheckGraphics',
  121. 'envCheckGit',
  122. 'envCheckServer',
  123. 'envCheckPath',
  124. 'envCheckShellLocale',
  125. 'envCheckUploadsDirectory',
  126. 'envCheckLibicu',
  127. 'envCheckSuhosinMaxValueLength',
  128. 'envCheck64Bit',
  129. ];
  130. /**
  131. * A list of environment preparation methods called by doEnvironmentPreps().
  132. *
  133. * @var array
  134. */
  135. protected $envPreps = [
  136. 'envPrepServer',
  137. 'envPrepPath',
  138. ];
  139. /**
  140. * MediaWiki configuration globals that will eventually be passed through
  141. * to LocalSettings.php. The names only are given here, the defaults
  142. * typically come from DefaultSettings.php.
  143. *
  144. * @var array
  145. */
  146. protected $defaultVarNames = [
  147. 'wgSitename',
  148. 'wgPasswordSender',
  149. 'wgLanguageCode',
  150. 'wgRightsIcon',
  151. 'wgRightsText',
  152. 'wgRightsUrl',
  153. 'wgEnableEmail',
  154. 'wgEnableUserEmail',
  155. 'wgEnotifUserTalk',
  156. 'wgEnotifWatchlist',
  157. 'wgEmailAuthentication',
  158. 'wgDBname',
  159. 'wgDBtype',
  160. 'wgDiff3',
  161. 'wgImageMagickConvertCommand',
  162. 'wgGitBin',
  163. 'IP',
  164. 'wgScriptPath',
  165. 'wgMetaNamespace',
  166. 'wgDeletedDirectory',
  167. 'wgEnableUploads',
  168. 'wgShellLocale',
  169. 'wgSecretKey',
  170. 'wgUseInstantCommons',
  171. 'wgUpgradeKey',
  172. 'wgDefaultSkin',
  173. 'wgPingback',
  174. ];
  175. /**
  176. * Variables that are stored alongside globals, and are used for any
  177. * configuration of the installation process aside from the MediaWiki
  178. * configuration. Map of names to defaults.
  179. *
  180. * @var array
  181. */
  182. protected $internalDefaults = [
  183. '_UserLang' => 'en',
  184. '_Environment' => false,
  185. '_RaiseMemory' => false,
  186. '_UpgradeDone' => false,
  187. '_InstallDone' => false,
  188. '_Caches' => [],
  189. '_InstallPassword' => '',
  190. '_SameAccount' => true,
  191. '_CreateDBAccount' => false,
  192. '_NamespaceType' => 'site-name',
  193. '_AdminName' => '', // will be set later, when the user selects language
  194. '_AdminPassword' => '',
  195. '_AdminPasswordConfirm' => '',
  196. '_AdminEmail' => '',
  197. '_Subscribe' => false,
  198. '_SkipOptional' => 'continue',
  199. '_RightsProfile' => 'wiki',
  200. '_LicenseCode' => 'none',
  201. '_CCDone' => false,
  202. '_Extensions' => [],
  203. '_Skins' => [],
  204. '_MemCachedServers' => '',
  205. '_UpgradeKeySupplied' => false,
  206. '_ExistingDBSettings' => false,
  207. // $wgLogo is probably wrong (T50084); set something that will work.
  208. // Single quotes work fine here, as LocalSettingsGenerator outputs this unescaped.
  209. 'wgLogo' => '$wgResourceBasePath/resources/assets/wiki.png',
  210. 'wgAuthenticationTokenVersion' => 1,
  211. ];
  212. /**
  213. * The actual list of installation steps. This will be initialized by getInstallSteps()
  214. *
  215. * @var array
  216. */
  217. private $installSteps = [];
  218. /**
  219. * Extra steps for installation, for things like DatabaseInstallers to modify
  220. *
  221. * @var array
  222. */
  223. protected $extraInstallSteps = [];
  224. /**
  225. * Known object cache types and the functions used to test for their existence.
  226. *
  227. * @var array
  228. */
  229. protected $objectCaches = [
  230. 'apc' => 'apc_fetch',
  231. 'apcu' => 'apcu_fetch',
  232. 'wincache' => 'wincache_ucache_get'
  233. ];
  234. /**
  235. * User rights profiles.
  236. *
  237. * @var array
  238. */
  239. public $rightsProfiles = [
  240. 'wiki' => [],
  241. 'no-anon' => [
  242. '*' => [ 'edit' => false ]
  243. ],
  244. 'fishbowl' => [
  245. '*' => [
  246. 'createaccount' => false,
  247. 'edit' => false,
  248. ],
  249. ],
  250. 'private' => [
  251. '*' => [
  252. 'createaccount' => false,
  253. 'edit' => false,
  254. 'read' => false,
  255. ],
  256. ],
  257. ];
  258. /**
  259. * License types.
  260. *
  261. * @var array
  262. */
  263. public $licenses = [
  264. 'cc-by' => [
  265. 'url' => 'https://creativecommons.org/licenses/by/4.0/',
  266. 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png',
  267. ],
  268. 'cc-by-sa' => [
  269. 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/',
  270. 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png',
  271. ],
  272. 'cc-by-nc-sa' => [
  273. 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
  274. 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png',
  275. ],
  276. 'cc-0' => [
  277. 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/',
  278. 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png',
  279. ],
  280. 'gfdl' => [
  281. 'url' => 'https://www.gnu.org/copyleft/fdl.html',
  282. 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png',
  283. ],
  284. 'none' => [
  285. 'url' => '',
  286. 'icon' => '',
  287. 'text' => ''
  288. ],
  289. 'cc-choose' => [
  290. // Details will be filled in by the selector.
  291. 'url' => '',
  292. 'icon' => '',
  293. 'text' => '',
  294. ],
  295. ];
  296. /**
  297. * URL to mediawiki-announce subscription
  298. */
  299. protected $mediaWikiAnnounceUrl =
  300. 'https://lists.wikimedia.org/mailman/subscribe/mediawiki-announce';
  301. /**
  302. * Supported language codes for Mailman
  303. */
  304. protected $mediaWikiAnnounceLanguages = [
  305. 'ca', 'cs', 'da', 'de', 'en', 'es', 'et', 'eu', 'fi', 'fr', 'hr', 'hu',
  306. 'it', 'ja', 'ko', 'lt', 'nl', 'no', 'pl', 'pt', 'pt-br', 'ro', 'ru',
  307. 'sl', 'sr', 'sv', 'tr', 'uk'
  308. ];
  309. /**
  310. * UI interface for displaying a short message
  311. * The parameters are like parameters to wfMessage().
  312. * The messages will be in wikitext format, which will be converted to an
  313. * output format such as HTML or text before being sent to the user.
  314. * @param string $msg
  315. */
  316. abstract public function showMessage( $msg /*, ... */ );
  317. /**
  318. * Same as showMessage(), but for displaying errors
  319. * @param string $msg
  320. */
  321. abstract public function showError( $msg /*, ... */ );
  322. /**
  323. * Show a message to the installing user by using a Status object
  324. * @param Status $status
  325. */
  326. abstract public function showStatusMessage( Status $status );
  327. /**
  328. * Constructs a Config object that contains configuration settings that should be
  329. * overwritten for the installation process.
  330. *
  331. * @since 1.27
  332. *
  333. * @param Config $baseConfig
  334. *
  335. * @return Config The config to use during installation.
  336. */
  337. public static function getInstallerConfig( Config $baseConfig ) {
  338. $configOverrides = new HashConfig();
  339. // disable (problematic) object cache types explicitly, preserving all other (working) ones
  340. // bug T113843
  341. $emptyCache = [ 'class' => EmptyBagOStuff::class ];
  342. $objectCaches = [
  343. CACHE_NONE => $emptyCache,
  344. CACHE_DB => $emptyCache,
  345. CACHE_ANYTHING => $emptyCache,
  346. CACHE_MEMCACHED => $emptyCache,
  347. ] + $baseConfig->get( 'ObjectCaches' );
  348. $configOverrides->set( 'ObjectCaches', $objectCaches );
  349. // Load the installer's i18n.
  350. $messageDirs = $baseConfig->get( 'MessagesDirs' );
  351. $messageDirs['MediawikiInstaller'] = __DIR__ . '/i18n';
  352. $configOverrides->set( 'MessagesDirs', $messageDirs );
  353. $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] );
  354. // make sure we use the installer config as the main config
  355. $configRegistry = $baseConfig->get( 'ConfigRegistry' );
  356. $configRegistry['main'] = function () use ( $installerConfig ) {
  357. return $installerConfig;
  358. };
  359. $configOverrides->set( 'ConfigRegistry', $configRegistry );
  360. return $installerConfig;
  361. }
  362. /**
  363. * Constructor, always call this from child classes.
  364. */
  365. public function __construct() {
  366. global $wgMemc, $wgUser, $wgObjectCaches;
  367. $defaultConfig = new GlobalVarConfig(); // all the stuff from DefaultSettings.php
  368. $installerConfig = self::getInstallerConfig( $defaultConfig );
  369. // Reset all services and inject config overrides
  370. MediaWikiServices::resetGlobalInstance( $installerConfig );
  371. // Don't attempt to load user language options (T126177)
  372. // This will be overridden in the web installer with the user-specified language
  373. RequestContext::getMain()->setLanguage( 'en' );
  374. // Disable the i18n cache
  375. // TODO: manage LocalisationCache singleton in MediaWikiServices
  376. Language::getLocalisationCache()->disableBackend();
  377. // Disable all global services, since we don't have any configuration yet!
  378. MediaWikiServices::disableStorageBackend();
  379. $mwServices = MediaWikiServices::getInstance();
  380. // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and
  381. // SqlBagOStuff will then throw since we just disabled wfGetDB)
  382. $wgObjectCaches = $mwServices->getMainConfig()->get( 'ObjectCaches' );
  383. $wgMemc = ObjectCache::getInstance( CACHE_NONE );
  384. // Disable interwiki lookup, to avoid database access during parses
  385. $mwServices->redefineService( 'InterwikiLookup', function () {
  386. return new NullInterwikiLookup();
  387. } );
  388. // Having a user with id = 0 safeguards us from DB access via User::loadOptions().
  389. $wgUser = User::newFromId( 0 );
  390. RequestContext::getMain()->setUser( $wgUser );
  391. $this->settings = $this->internalDefaults;
  392. foreach ( $this->defaultVarNames as $var ) {
  393. $this->settings[$var] = $GLOBALS[$var];
  394. }
  395. $this->doEnvironmentPreps();
  396. $this->compiledDBs = [];
  397. foreach ( self::getDBTypes() as $type ) {
  398. $installer = $this->getDBInstaller( $type );
  399. if ( !$installer->isCompiled() ) {
  400. continue;
  401. }
  402. $this->compiledDBs[] = $type;
  403. }
  404. $this->parserTitle = Title::newFromText( 'Installer' );
  405. $this->parserOptions = new ParserOptions( $wgUser ); // language will be wrong :(
  406. // Don't try to access DB before user language is initialised
  407. $this->setParserLanguage( Language::factory( 'en' ) );
  408. }
  409. /**
  410. * Get a list of known DB types.
  411. *
  412. * @return array
  413. */
  414. public static function getDBTypes() {
  415. return self::$dbTypes;
  416. }
  417. /**
  418. * Do initial checks of the PHP environment. Set variables according to
  419. * the observed environment.
  420. *
  421. * It's possible that this may be called under the CLI SAPI, not the SAPI
  422. * that the wiki will primarily run under. In that case, the subclass should
  423. * initialise variables such as wgScriptPath, before calling this function.
  424. *
  425. * Under the web subclass, it can already be assumed that PHP 5+ is in use
  426. * and that sessions are working.
  427. *
  428. * @return Status
  429. */
  430. public function doEnvironmentChecks() {
  431. // Php version has already been checked by entry scripts
  432. // Show message here for information purposes
  433. if ( wfIsHHVM() ) {
  434. $this->showMessage( 'config-env-hhvm', HHVM_VERSION );
  435. } else {
  436. $this->showMessage( 'config-env-php', PHP_VERSION );
  437. }
  438. $good = true;
  439. // Must go here because an old version of PCRE can prevent other checks from completing
  440. list( $pcreVersion ) = explode( ' ', PCRE_VERSION, 2 );
  441. if ( version_compare( $pcreVersion, self::MINIMUM_PCRE_VERSION, '<' ) ) {
  442. $this->showError( 'config-pcre-old', self::MINIMUM_PCRE_VERSION, $pcreVersion );
  443. $good = false;
  444. } else {
  445. foreach ( $this->envChecks as $check ) {
  446. $status = $this->$check();
  447. if ( $status === false ) {
  448. $good = false;
  449. }
  450. }
  451. }
  452. $this->setVar( '_Environment', $good );
  453. return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' );
  454. }
  455. public function doEnvironmentPreps() {
  456. foreach ( $this->envPreps as $prep ) {
  457. $this->$prep();
  458. }
  459. }
  460. /**
  461. * Set a MW configuration variable, or internal installer configuration variable.
  462. *
  463. * @param string $name
  464. * @param mixed $value
  465. */
  466. public function setVar( $name, $value ) {
  467. $this->settings[$name] = $value;
  468. }
  469. /**
  470. * Get an MW configuration variable, or internal installer configuration variable.
  471. * The defaults come from $GLOBALS (ultimately DefaultSettings.php).
  472. * Installer variables are typically prefixed by an underscore.
  473. *
  474. * @param string $name
  475. * @param mixed $default
  476. *
  477. * @return mixed
  478. */
  479. public function getVar( $name, $default = null ) {
  480. if ( !isset( $this->settings[$name] ) ) {
  481. return $default;
  482. } else {
  483. return $this->settings[$name];
  484. }
  485. }
  486. /**
  487. * Get a list of DBs supported by current PHP setup
  488. *
  489. * @return array
  490. */
  491. public function getCompiledDBs() {
  492. return $this->compiledDBs;
  493. }
  494. /**
  495. * Get the DatabaseInstaller class name for this type
  496. *
  497. * @param string $type database type ($wgDBtype)
  498. * @return string Class name
  499. * @since 1.30
  500. */
  501. public static function getDBInstallerClass( $type ) {
  502. return ucfirst( $type ) . 'Installer';
  503. }
  504. /**
  505. * Get an instance of DatabaseInstaller for the specified DB type.
  506. *
  507. * @param mixed $type DB installer for which is needed, false to use default.
  508. *
  509. * @return DatabaseInstaller
  510. */
  511. public function getDBInstaller( $type = false ) {
  512. if ( !$type ) {
  513. $type = $this->getVar( 'wgDBtype' );
  514. }
  515. $type = strtolower( $type );
  516. if ( !isset( $this->dbInstallers[$type] ) ) {
  517. $class = self::getDBInstallerClass( $type );
  518. $this->dbInstallers[$type] = new $class( $this );
  519. }
  520. return $this->dbInstallers[$type];
  521. }
  522. /**
  523. * Determine if LocalSettings.php exists. If it does, return its variables.
  524. *
  525. * @return array|false
  526. */
  527. public static function getExistingLocalSettings() {
  528. global $IP;
  529. // You might be wondering why this is here. Well if you don't do this
  530. // then some poorly-formed extensions try to call their own classes
  531. // after immediately registering them. We really need to get extension
  532. // registration out of the global scope and into a real format.
  533. // @see https://phabricator.wikimedia.org/T69440
  534. global $wgAutoloadClasses;
  535. $wgAutoloadClasses = [];
  536. // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions
  537. // Define the required globals here, to ensure, the functions can do it work correctly.
  538. // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables
  539. global $wgExtensionDirectory, $wgStyleDirectory;
  540. Wikimedia\suppressWarnings();
  541. $_lsExists = file_exists( "$IP/LocalSettings.php" );
  542. Wikimedia\restoreWarnings();
  543. if ( !$_lsExists ) {
  544. return false;
  545. }
  546. unset( $_lsExists );
  547. require "$IP/includes/DefaultSettings.php";
  548. require "$IP/LocalSettings.php";
  549. return get_defined_vars();
  550. }
  551. /**
  552. * Get a fake password for sending back to the user in HTML.
  553. * This is a security mechanism to avoid compromise of the password in the
  554. * event of session ID compromise.
  555. *
  556. * @param string $realPassword
  557. *
  558. * @return string
  559. */
  560. public function getFakePassword( $realPassword ) {
  561. return str_repeat( '*', strlen( $realPassword ) );
  562. }
  563. /**
  564. * Set a variable which stores a password, except if the new value is a
  565. * fake password in which case leave it as it is.
  566. *
  567. * @param string $name
  568. * @param mixed $value
  569. */
  570. public function setPassword( $name, $value ) {
  571. if ( !preg_match( '/^\*+$/', $value ) ) {
  572. $this->setVar( $name, $value );
  573. }
  574. }
  575. /**
  576. * On POSIX systems return the primary group of the webserver we're running under.
  577. * On other systems just returns null.
  578. *
  579. * This is used to advice the user that he should chgrp his mw-config/data/images directory as the
  580. * webserver user before he can install.
  581. *
  582. * Public because SqliteInstaller needs it, and doesn't subclass Installer.
  583. *
  584. * @return mixed
  585. */
  586. public static function maybeGetWebserverPrimaryGroup() {
  587. if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) {
  588. # I don't know this, this isn't UNIX.
  589. return null;
  590. }
  591. # posix_getegid() *not* getmygid() because we want the group of the webserver,
  592. # not whoever owns the current script.
  593. $gid = posix_getegid();
  594. $group = posix_getpwuid( $gid )['name'];
  595. return $group;
  596. }
  597. /**
  598. * Convert wikitext $text to HTML.
  599. *
  600. * This is potentially error prone since many parser features require a complete
  601. * installed MW database. The solution is to just not use those features when you
  602. * write your messages. This appears to work well enough. Basic formatting and
  603. * external links work just fine.
  604. *
  605. * But in case a translator decides to throw in a "#ifexist" or internal link or
  606. * whatever, this function is guarded to catch the attempted DB access and to present
  607. * some fallback text.
  608. *
  609. * @param string $text
  610. * @param bool $lineStart
  611. * @return string
  612. */
  613. public function parse( $text, $lineStart = false ) {
  614. global $wgParser;
  615. try {
  616. $out = $wgParser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart );
  617. $html = $out->getText( [
  618. 'enableSectionEditLinks' => false,
  619. 'unwrap' => true,
  620. ] );
  621. } catch ( MediaWiki\Services\ServiceDisabledException $e ) {
  622. $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text );
  623. }
  624. return $html;
  625. }
  626. /**
  627. * @return ParserOptions
  628. */
  629. public function getParserOptions() {
  630. return $this->parserOptions;
  631. }
  632. public function disableLinkPopups() {
  633. $this->parserOptions->setExternalLinkTarget( false );
  634. }
  635. public function restoreLinkPopups() {
  636. global $wgExternalLinkTarget;
  637. $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget );
  638. }
  639. /**
  640. * Install step which adds a row to the site_stats table with appropriate
  641. * initial values.
  642. *
  643. * @param DatabaseInstaller $installer
  644. *
  645. * @return Status
  646. */
  647. public function populateSiteStats( DatabaseInstaller $installer ) {
  648. $status = $installer->getConnection();
  649. if ( !$status->isOK() ) {
  650. return $status;
  651. }
  652. $status->value->insert(
  653. 'site_stats',
  654. [
  655. 'ss_row_id' => 1,
  656. 'ss_total_edits' => 0,
  657. 'ss_good_articles' => 0,
  658. 'ss_total_pages' => 0,
  659. 'ss_users' => 0,
  660. 'ss_active_users' => 0,
  661. 'ss_images' => 0
  662. ],
  663. __METHOD__, 'IGNORE'
  664. );
  665. return Status::newGood();
  666. }
  667. /**
  668. * Environment check for DB types.
  669. * @return bool
  670. */
  671. protected function envCheckDB() {
  672. global $wgLang;
  673. $allNames = [];
  674. // Messages: config-type-mysql, config-type-postgres, config-type-oracle,
  675. // config-type-sqlite
  676. foreach ( self::getDBTypes() as $name ) {
  677. $allNames[] = wfMessage( "config-type-$name" )->text();
  678. }
  679. $databases = $this->getCompiledDBs();
  680. $databases = array_flip( $databases );
  681. foreach ( array_keys( $databases ) as $db ) {
  682. $installer = $this->getDBInstaller( $db );
  683. $status = $installer->checkPrerequisites();
  684. if ( !$status->isGood() ) {
  685. $this->showStatusMessage( $status );
  686. }
  687. if ( !$status->isOK() ) {
  688. unset( $databases[$db] );
  689. }
  690. }
  691. $databases = array_flip( $databases );
  692. if ( !$databases ) {
  693. $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) );
  694. // @todo FIXME: This only works for the web installer!
  695. return false;
  696. }
  697. return true;
  698. }
  699. /**
  700. * Some versions of libxml+PHP break < and > encoding horribly
  701. * @return bool
  702. */
  703. protected function envCheckBrokenXML() {
  704. $test = new PhpXmlBugTester();
  705. if ( !$test->ok ) {
  706. $this->showError( 'config-brokenlibxml' );
  707. return false;
  708. }
  709. return true;
  710. }
  711. /**
  712. * Environment check for the PCRE module.
  713. *
  714. * @note If this check were to fail, the parser would
  715. * probably throw an exception before the result
  716. * of this check is shown to the user.
  717. * @return bool
  718. */
  719. protected function envCheckPCRE() {
  720. Wikimedia\suppressWarnings();
  721. $regexd = preg_replace( '/[\x{0430}-\x{04FF}]/iu', '', '-АБВГД-' );
  722. // Need to check for \p support too, as PCRE can be compiled
  723. // with utf8 support, but not unicode property support.
  724. // check that \p{Zs} (space separators) matches
  725. // U+3000 (Ideographic space)
  726. $regexprop = preg_replace( '/\p{Zs}/u', '', "-\xE3\x80\x80-" );
  727. Wikimedia\restoreWarnings();
  728. if ( $regexd != '--' || $regexprop != '--' ) {
  729. $this->showError( 'config-pcre-no-utf8' );
  730. return false;
  731. }
  732. return true;
  733. }
  734. /**
  735. * Environment check for available memory.
  736. * @return bool
  737. */
  738. protected function envCheckMemory() {
  739. $limit = ini_get( 'memory_limit' );
  740. if ( !$limit || $limit == -1 ) {
  741. return true;
  742. }
  743. $n = wfShorthandToInteger( $limit );
  744. if ( $n < $this->minMemorySize * 1024 * 1024 ) {
  745. $newLimit = "{$this->minMemorySize}M";
  746. if ( ini_set( "memory_limit", $newLimit ) === false ) {
  747. $this->showMessage( 'config-memory-bad', $limit );
  748. } else {
  749. $this->showMessage( 'config-memory-raised', $limit, $newLimit );
  750. $this->setVar( '_RaiseMemory', true );
  751. }
  752. }
  753. return true;
  754. }
  755. /**
  756. * Environment check for compiled object cache types.
  757. */
  758. protected function envCheckCache() {
  759. $caches = [];
  760. foreach ( $this->objectCaches as $name => $function ) {
  761. if ( function_exists( $function ) ) {
  762. $caches[$name] = true;
  763. }
  764. }
  765. if ( !$caches ) {
  766. $key = 'config-no-cache-apcu';
  767. $this->showMessage( $key );
  768. }
  769. $this->setVar( '_Caches', $caches );
  770. }
  771. /**
  772. * Scare user to death if they have mod_security or mod_security2
  773. * @return bool
  774. */
  775. protected function envCheckModSecurity() {
  776. if ( self::apacheModulePresent( 'mod_security' )
  777. || self::apacheModulePresent( 'mod_security2' ) ) {
  778. $this->showMessage( 'config-mod-security' );
  779. }
  780. return true;
  781. }
  782. /**
  783. * Search for GNU diff3.
  784. * @return bool
  785. */
  786. protected function envCheckDiff3() {
  787. $names = [ "gdiff3", "diff3" ];
  788. if ( wfIsWindows() ) {
  789. $names[] = 'diff3.exe';
  790. }
  791. $versionInfo = [ '--version', 'GNU diffutils' ];
  792. $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
  793. if ( $diff3 ) {
  794. $this->setVar( 'wgDiff3', $diff3 );
  795. } else {
  796. $this->setVar( 'wgDiff3', false );
  797. $this->showMessage( 'config-diff3-bad' );
  798. }
  799. return true;
  800. }
  801. /**
  802. * Environment check for ImageMagick and GD.
  803. * @return bool
  804. */
  805. protected function envCheckGraphics() {
  806. $names = wfIsWindows() ? 'convert.exe' : 'convert';
  807. $versionInfo = [ '-version', 'ImageMagick' ];
  808. $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
  809. $this->setVar( 'wgImageMagickConvertCommand', '' );
  810. if ( $convert ) {
  811. $this->setVar( 'wgImageMagickConvertCommand', $convert );
  812. $this->showMessage( 'config-imagemagick', $convert );
  813. return true;
  814. } elseif ( function_exists( 'imagejpeg' ) ) {
  815. $this->showMessage( 'config-gd' );
  816. } else {
  817. $this->showMessage( 'config-no-scaling' );
  818. }
  819. return true;
  820. }
  821. /**
  822. * Search for git.
  823. *
  824. * @since 1.22
  825. * @return bool
  826. */
  827. protected function envCheckGit() {
  828. $names = wfIsWindows() ? 'git.exe' : 'git';
  829. $versionInfo = [ '--version', 'git version' ];
  830. $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo );
  831. if ( $git ) {
  832. $this->setVar( 'wgGitBin', $git );
  833. $this->showMessage( 'config-git', $git );
  834. } else {
  835. $this->setVar( 'wgGitBin', false );
  836. $this->showMessage( 'config-git-bad' );
  837. }
  838. return true;
  839. }
  840. /**
  841. * Environment check to inform user which server we've assumed.
  842. *
  843. * @return bool
  844. */
  845. protected function envCheckServer() {
  846. $server = $this->envGetDefaultServer();
  847. if ( $server !== null ) {
  848. $this->showMessage( 'config-using-server', $server );
  849. }
  850. return true;
  851. }
  852. /**
  853. * Environment check to inform user which paths we've assumed.
  854. *
  855. * @return bool
  856. */
  857. protected function envCheckPath() {
  858. $this->showMessage(
  859. 'config-using-uri',
  860. $this->getVar( 'wgServer' ),
  861. $this->getVar( 'wgScriptPath' )
  862. );
  863. return true;
  864. }
  865. /**
  866. * Environment check for preferred locale in shell
  867. * @return bool
  868. */
  869. protected function envCheckShellLocale() {
  870. $os = php_uname( 's' );
  871. $supported = [ 'Linux', 'SunOS', 'HP-UX', 'Darwin' ]; # Tested these
  872. if ( !in_array( $os, $supported ) ) {
  873. return true;
  874. }
  875. if ( Shell::isDisabled() ) {
  876. return true;
  877. }
  878. # Get a list of available locales.
  879. $result = Shell::command( '/usr/bin/locale', '-a' )
  880. ->execute();
  881. if ( $result->getExitCode() != 0 ) {
  882. return true;
  883. }
  884. $lines = $result->getStdout();
  885. $lines = array_map( 'trim', explode( "\n", $lines ) );
  886. $candidatesByLocale = [];
  887. $candidatesByLang = [];
  888. foreach ( $lines as $line ) {
  889. if ( $line === '' ) {
  890. continue;
  891. }
  892. if ( !preg_match( '/^([a-zA-Z]+)(_[a-zA-Z]+|)\.(utf8|UTF-8)(@[a-zA-Z_]*|)$/i', $line, $m ) ) {
  893. continue;
  894. }
  895. list( , $lang, , , ) = $m;
  896. $candidatesByLocale[$m[0]] = $m;
  897. $candidatesByLang[$lang][] = $m;
  898. }
  899. # Try the current value of LANG.
  900. if ( isset( $candidatesByLocale[getenv( 'LANG' )] ) ) {
  901. $this->setVar( 'wgShellLocale', getenv( 'LANG' ) );
  902. return true;
  903. }
  904. # Try the most common ones.
  905. $commonLocales = [ 'C.UTF-8', 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ];
  906. foreach ( $commonLocales as $commonLocale ) {
  907. if ( isset( $candidatesByLocale[$commonLocale] ) ) {
  908. $this->setVar( 'wgShellLocale', $commonLocale );
  909. return true;
  910. }
  911. }
  912. # Is there an available locale in the Wiki's language?
  913. $wikiLang = $this->getVar( 'wgLanguageCode' );
  914. if ( isset( $candidatesByLang[$wikiLang] ) ) {
  915. $m = reset( $candidatesByLang[$wikiLang] );
  916. $this->setVar( 'wgShellLocale', $m[0] );
  917. return true;
  918. }
  919. # Are there any at all?
  920. if ( count( $candidatesByLocale ) ) {
  921. $m = reset( $candidatesByLocale );
  922. $this->setVar( 'wgShellLocale', $m[0] );
  923. return true;
  924. }
  925. # Give up.
  926. return true;
  927. }
  928. /**
  929. * Environment check for the permissions of the uploads directory
  930. * @return bool
  931. */
  932. protected function envCheckUploadsDirectory() {
  933. global $IP;
  934. $dir = $IP . '/images/';
  935. $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/';
  936. $safe = !$this->dirIsExecutable( $dir, $url );
  937. if ( !$safe ) {
  938. $this->showMessage( 'config-uploads-not-safe', $dir );
  939. }
  940. return true;
  941. }
  942. /**
  943. * Checks if suhosin.get.max_value_length is set, and if so generate
  944. * a warning because it decreases ResourceLoader performance.
  945. * @return bool
  946. */
  947. protected function envCheckSuhosinMaxValueLength() {
  948. $maxValueLength = ini_get( 'suhosin.get.max_value_length' );
  949. if ( $maxValueLength > 0 && $maxValueLength < 1024 ) {
  950. // Only warn if the value is below the sane 1024
  951. $this->showMessage( 'config-suhosin-max-value-length', $maxValueLength );
  952. }
  953. return true;
  954. }
  955. /**
  956. * Checks if we're running on 64 bit or not. 32 bit is becoming increasingly
  957. * hard to support, so let's at least warn people.
  958. *
  959. * @return bool
  960. */
  961. protected function envCheck64Bit() {
  962. if ( PHP_INT_SIZE == 4 ) {
  963. $this->showMessage( 'config-using-32bit' );
  964. }
  965. return true;
  966. }
  967. /**
  968. * Convert a hex string representing a Unicode code point to that code point.
  969. * @param string $c
  970. * @return string|false
  971. */
  972. protected function unicodeChar( $c ) {
  973. $c = hexdec( $c );
  974. if ( $c <= 0x7F ) {
  975. return chr( $c );
  976. } elseif ( $c <= 0x7FF ) {
  977. return chr( 0xC0 | $c >> 6 ) . chr( 0x80 | $c & 0x3F );
  978. } elseif ( $c <= 0xFFFF ) {
  979. return chr( 0xE0 | $c >> 12 ) . chr( 0x80 | $c >> 6 & 0x3F ) .
  980. chr( 0x80 | $c & 0x3F );
  981. } elseif ( $c <= 0x10FFFF ) {
  982. return chr( 0xF0 | $c >> 18 ) . chr( 0x80 | $c >> 12 & 0x3F ) .
  983. chr( 0x80 | $c >> 6 & 0x3F ) .
  984. chr( 0x80 | $c & 0x3F );
  985. } else {
  986. return false;
  987. }
  988. }
  989. /**
  990. * Check the libicu version
  991. */
  992. protected function envCheckLibicu() {
  993. /**
  994. * This needs to be updated something that the latest libicu
  995. * will properly normalize. This normalization was found at
  996. * http://www.unicode.org/versions/Unicode5.2.0/#Character_Additions
  997. * Note that we use the hex representation to create the code
  998. * points in order to avoid any Unicode-destroying during transit.
  999. */
  1000. $not_normal_c = $this->unicodeChar( "FA6C" );
  1001. $normal_c = $this->unicodeChar( "242EE" );
  1002. $useNormalizer = 'php';
  1003. $needsUpdate = false;
  1004. if ( function_exists( 'normalizer_normalize' ) ) {
  1005. $useNormalizer = 'intl';
  1006. $intl = normalizer_normalize( $not_normal_c, Normalizer::FORM_C );
  1007. if ( $intl !== $normal_c ) {
  1008. $needsUpdate = true;
  1009. }
  1010. }
  1011. // Uses messages 'config-unicode-using-php' and 'config-unicode-using-intl'
  1012. if ( $useNormalizer === 'php' ) {
  1013. $this->showMessage( 'config-unicode-pure-php-warning' );
  1014. } else {
  1015. $this->showMessage( 'config-unicode-using-' . $useNormalizer );
  1016. if ( $needsUpdate ) {
  1017. $this->showMessage( 'config-unicode-update-warning' );
  1018. }
  1019. }
  1020. }
  1021. /**
  1022. * Environment prep for the server hostname.
  1023. */
  1024. protected function envPrepServer() {
  1025. $server = $this->envGetDefaultServer();
  1026. if ( $server !== null ) {
  1027. $this->setVar( 'wgServer', $server );
  1028. }
  1029. }
  1030. /**
  1031. * Helper function to be called from envPrepServer()
  1032. * @return string
  1033. */
  1034. abstract protected function envGetDefaultServer();
  1035. /**
  1036. * Environment prep for setting $IP and $wgScriptPath.
  1037. */
  1038. protected function envPrepPath() {
  1039. global $IP;
  1040. $IP = dirname( dirname( __DIR__ ) );
  1041. $this->setVar( 'IP', $IP );
  1042. }
  1043. /**
  1044. * Checks if scripts located in the given directory can be executed via the given URL.
  1045. *
  1046. * Used only by environment checks.
  1047. * @param string $dir
  1048. * @param string $url
  1049. * @return bool|int|string
  1050. */
  1051. public function dirIsExecutable( $dir, $url ) {
  1052. $scriptTypes = [
  1053. 'php' => [
  1054. "<?php echo 'ex' . 'ec';",
  1055. "#!/var/env php\n<?php echo 'ex' . 'ec';",
  1056. ],
  1057. ];
  1058. // it would be good to check other popular languages here, but it'll be slow.
  1059. Wikimedia\suppressWarnings();
  1060. foreach ( $scriptTypes as $ext => $contents ) {
  1061. foreach ( $contents as $source ) {
  1062. $file = 'exectest.' . $ext;
  1063. if ( !file_put_contents( $dir . $file, $source ) ) {
  1064. break;
  1065. }
  1066. try {
  1067. $text = Http::get( $url . $file, [ 'timeout' => 3 ], __METHOD__ );
  1068. } catch ( Exception $e ) {
  1069. // Http::get throws with allow_url_fopen = false and no curl extension.
  1070. $text = null;
  1071. }
  1072. unlink( $dir . $file );
  1073. if ( $text == 'exec' ) {
  1074. Wikimedia\restoreWarnings();
  1075. return $ext;
  1076. }
  1077. }
  1078. }
  1079. Wikimedia\restoreWarnings();
  1080. return false;
  1081. }
  1082. /**
  1083. * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too.
  1084. *
  1085. * @param string $moduleName Name of module to check.
  1086. * @return bool
  1087. */
  1088. public static function apacheModulePresent( $moduleName ) {
  1089. if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) {
  1090. return true;
  1091. }
  1092. // try it the hard way
  1093. ob_start();
  1094. phpinfo( INFO_MODULES );
  1095. $info = ob_get_clean();
  1096. return strpos( $info, $moduleName ) !== false;
  1097. }
  1098. /**
  1099. * ParserOptions are constructed before we determined the language, so fix it
  1100. *
  1101. * @param Language $lang
  1102. */
  1103. public function setParserLanguage( $lang ) {
  1104. $this->parserOptions->setTargetLanguage( $lang );
  1105. $this->parserOptions->setUserLang( $lang );
  1106. }
  1107. /**
  1108. * Overridden by WebInstaller to provide lastPage parameters.
  1109. * @param string $page
  1110. * @return string
  1111. */
  1112. protected function getDocUrl( $page ) {
  1113. return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page );
  1114. }
  1115. /**
  1116. * Finds extensions that follow the format /$directory/Name/Name.php,
  1117. * and returns an array containing the value for 'Name' for each found extension.
  1118. *
  1119. * Reasonable values for $directory include 'extensions' (the default) and 'skins'.
  1120. *
  1121. * @param string $directory Directory to search in
  1122. * @return array [ $extName => [ 'screenshots' => [ '...' ] ]
  1123. */
  1124. public function findExtensions( $directory = 'extensions' ) {
  1125. if ( $this->getVar( 'IP' ) === null ) {
  1126. return [];
  1127. }
  1128. $extDir = $this->getVar( 'IP' ) . '/' . $directory;
  1129. if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) {
  1130. return [];
  1131. }
  1132. // extensions -> extension.json, skins -> skin.json
  1133. $jsonFile = substr( $directory, 0, strlen( $directory ) - 1 ) . '.json';
  1134. $dh = opendir( $extDir );
  1135. $exts = [];
  1136. while ( ( $file = readdir( $dh ) ) !== false ) {
  1137. if ( !is_dir( "$extDir/$file" ) ) {
  1138. continue;
  1139. }
  1140. $fullJsonFile = "$extDir/$file/$jsonFile";
  1141. $isJson = file_exists( $fullJsonFile );
  1142. $isPhp = false;
  1143. if ( !$isJson ) {
  1144. // Only fallback to PHP file if JSON doesn't exist
  1145. $fullPhpFile = "$extDir/$file/$file.php";
  1146. $isPhp = file_exists( $fullPhpFile );
  1147. }
  1148. if ( $isJson || $isPhp ) {
  1149. // Extension exists. Now see if there are screenshots
  1150. $exts[$file] = [];
  1151. if ( is_dir( "$extDir/$file/screenshots" ) ) {
  1152. $paths = glob( "$extDir/$file/screenshots/*.png" );
  1153. foreach ( $paths as $path ) {
  1154. $exts[$file]['screenshots'][] = str_replace( $extDir, "../$directory", $path );
  1155. }
  1156. }
  1157. }
  1158. if ( $isJson ) {
  1159. $info = $this->readExtension( $fullJsonFile );
  1160. if ( $info === false ) {
  1161. continue;
  1162. }
  1163. $exts[$file] += $info;
  1164. }
  1165. }
  1166. closedir( $dh );
  1167. uksort( $exts, 'strnatcasecmp' );
  1168. return $exts;
  1169. }
  1170. /**
  1171. * @param string $fullJsonFile
  1172. * @param array $extDeps
  1173. * @param array $skinDeps
  1174. *
  1175. * @return array|bool False if this extension can't be loaded
  1176. */
  1177. private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) {
  1178. $load = [
  1179. $fullJsonFile => 1
  1180. ];
  1181. if ( $extDeps ) {
  1182. $extDir = $this->getVar( 'IP' ) . '/extensions';
  1183. foreach ( $extDeps as $dep ) {
  1184. $fname = "$extDir/$dep/extension.json";
  1185. if ( !file_exists( $fname ) ) {
  1186. return false;
  1187. }
  1188. $load[$fname] = 1;
  1189. }
  1190. }
  1191. if ( $skinDeps ) {
  1192. $skinDir = $this->getVar( 'IP' ) . '/skins';
  1193. foreach ( $skinDeps as $dep ) {
  1194. $fname = "$skinDir/$dep/skin.json";
  1195. if ( !file_exists( $fname ) ) {
  1196. return false;
  1197. }
  1198. $load[$fname] = 1;
  1199. }
  1200. }
  1201. $registry = new ExtensionRegistry();
  1202. try {
  1203. $info = $registry->readFromQueue( $load );
  1204. } catch ( ExtensionDependencyError $e ) {
  1205. if ( $e->incompatibleCore || $e->incompatibleSkins
  1206. || $e->incompatibleExtensions
  1207. ) {
  1208. // If something is incompatible with a dependency, we have no real
  1209. // option besides skipping it
  1210. return false;
  1211. } elseif ( $e->missingExtensions || $e->missingSkins ) {
  1212. // There's an extension missing in the dependency tree,
  1213. // so add those to the dependency list and try again
  1214. return $this->readExtension(
  1215. $fullJsonFile,
  1216. array_merge( $extDeps, $e->missingExtensions ),
  1217. array_merge( $skinDeps, $e->missingSkins )
  1218. );
  1219. }
  1220. // Some other kind of dependency error?
  1221. return false;
  1222. }
  1223. $ret = [];
  1224. // The order of credits will be the order of $load,
  1225. // so the first extension is the one we want to load,
  1226. // everything else is a dependency
  1227. $i = 0;
  1228. foreach ( $info['credits'] as $name => $credit ) {
  1229. $i++;
  1230. if ( $i == 1 ) {
  1231. // Extension we want to load
  1232. continue;
  1233. }
  1234. $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions';
  1235. $ret['requires'][$type][] = $credit['name'];
  1236. }
  1237. $credits = array_values( $info['credits'] )[0];
  1238. if ( isset( $credits['url'] ) ) {
  1239. $ret['url'] = $credits['url'];
  1240. }
  1241. $ret['type'] = $credits['type'];
  1242. return $ret;
  1243. }
  1244. /**
  1245. * Returns a default value to be used for $wgDefaultSkin: normally the one set in DefaultSettings,
  1246. * but will fall back to another if the default skin is missing and some other one is present
  1247. * instead.
  1248. *
  1249. * @param string[] $skinNames Names of installed skins.
  1250. * @return string
  1251. */
  1252. public function getDefaultSkin( array $skinNames ) {
  1253. $defaultSkin = $GLOBALS['wgDefaultSkin'];
  1254. if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) {
  1255. return $defaultSkin;
  1256. } else {
  1257. return $skinNames[0];
  1258. }
  1259. }
  1260. /**
  1261. * Installs the auto-detected extensions.
  1262. *
  1263. * @return Status
  1264. */
  1265. protected function includeExtensions() {
  1266. global $IP;
  1267. $exts = $this->getVar( '_Extensions' );
  1268. $IP = $this->getVar( 'IP' );
  1269. // Marker for DatabaseUpdater::loadExtensions so we don't
  1270. // double load extensions
  1271. define( 'MW_EXTENSIONS_LOADED', true );
  1272. /**
  1273. * We need to include DefaultSettings before including extensions to avoid
  1274. * warnings about unset variables. However, the only thing we really
  1275. * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work
  1276. * if the extension has hidden hook registration in $wgExtensionFunctions,
  1277. * but we're not opening that can of worms
  1278. * @see https://phabricator.wikimedia.org/T28857
  1279. */
  1280. global $wgAutoloadClasses;
  1281. $wgAutoloadClasses = [];
  1282. $queue = [];
  1283. require "$IP/includes/DefaultSettings.php";
  1284. foreach ( $exts as $e ) {
  1285. if ( file_exists( "$IP/extensions/$e/extension.json" ) ) {
  1286. $queue["$IP/extensions/$e/extension.json"] = 1;
  1287. } else {
  1288. require_once "$IP/extensions/$e/$e.php";
  1289. }
  1290. }
  1291. $registry = new ExtensionRegistry();
  1292. $data = $registry->readFromQueue( $queue );
  1293. $wgAutoloadClasses += $data['autoload'];
  1294. $hooksWeWant = isset( $wgHooks['LoadExtensionSchemaUpdates'] ) ?
  1295. /** @suppress PhanUndeclaredVariable $wgHooks is set by DefaultSettings */
  1296. $wgHooks['LoadExtensionSchemaUpdates'] : [];
  1297. if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) {
  1298. $hooksWeWant = array_merge_recursive(
  1299. $hooksWeWant,
  1300. $data['globals']['wgHooks']['LoadExtensionSchemaUpdates']
  1301. );
  1302. }
  1303. // Unset everyone else's hooks. Lord knows what someone might be doing
  1304. // in ParserFirstCallInit (see T29171)
  1305. $GLOBALS['wgHooks'] = [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ];
  1306. return Status::newGood();
  1307. }
  1308. /**
  1309. * Get an array of install steps. Should always be in the format of
  1310. * [
  1311. * 'name' => 'someuniquename',
  1312. * 'callback' => [ $obj, 'method' ],
  1313. * ]
  1314. * There must be a config-install-$name message defined per step, which will
  1315. * be shown on install.
  1316. *
  1317. * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks
  1318. * @return array
  1319. */
  1320. protected function getInstallSteps( DatabaseInstaller $installer ) {
  1321. $coreInstallSteps = [
  1322. [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ],
  1323. [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ],
  1324. [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ],
  1325. [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ],
  1326. [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ],
  1327. [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ],
  1328. [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ],
  1329. [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ],
  1330. ];
  1331. // Build the array of install steps starting from the core install list,
  1332. // then adding any callbacks that wanted to attach after a given step
  1333. foreach ( $coreInstallSteps as $step ) {
  1334. $this->installSteps[] = $step;
  1335. if ( isset( $this->extraInstallSteps[$step['name']] ) ) {
  1336. $this->installSteps = array_merge(
  1337. $this->installSteps,
  1338. $this->extraInstallSteps[$step['name']]
  1339. );
  1340. }
  1341. }
  1342. // Prepend any steps that want to be at the beginning
  1343. if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) {
  1344. $this->installSteps = array_merge(
  1345. $this->extraInstallSteps['BEGINNING'],
  1346. $this->installSteps
  1347. );
  1348. }
  1349. // Extensions should always go first, chance to tie into hooks and such
  1350. if ( count( $this->getVar( '_Extensions' ) ) ) {
  1351. array_unshift( $this->installSteps,
  1352. [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ]
  1353. );
  1354. $this->installSteps[] = [
  1355. 'name' => 'extension-tables',
  1356. 'callback' => [ $installer, 'createExtensionTables' ]
  1357. ];
  1358. }
  1359. return $this->installSteps;
  1360. }
  1361. /**
  1362. * Actually perform the installation.
  1363. *
  1364. * @param callable $startCB A callback array for the beginning of each step
  1365. * @param callable $endCB A callback array for the end of each step
  1366. *
  1367. * @return array Array of Status objects
  1368. */
  1369. public function performInstallation( $startCB, $endCB ) {
  1370. $installResults = [];
  1371. $installer = $this->getDBInstaller();
  1372. $installer->preInstall();
  1373. $steps = $this->getInstallSteps( $installer );
  1374. foreach ( $steps as $stepObj ) {
  1375. $name = $stepObj['name'];
  1376. call_user_func_array( $startCB, [ $name ] );
  1377. // Perform the callback step
  1378. $status = call_user_func( $stepObj['callback'], $installer );
  1379. // Output and save the results
  1380. call_user_func( $endCB, $name, $status );
  1381. $installResults[$name] = $status;
  1382. // If we've hit some sort of fatal, we need to bail.
  1383. // Callback already had a chance to do output above.
  1384. if ( !$status->isOk() ) {
  1385. break;
  1386. }
  1387. }
  1388. if ( $status->isOk() ) {
  1389. $this->showMessage(
  1390. 'config-install-success',
  1391. $this->getVar( 'wgServer' ),
  1392. $this->getVar( 'wgScriptPath' )
  1393. );
  1394. $this->setVar( '_InstallDone', true );
  1395. }
  1396. return $installResults;
  1397. }
  1398. /**
  1399. * Generate $wgSecretKey. Will warn if we had to use an insecure random source.
  1400. *
  1401. * @return Status
  1402. */
  1403. public function generateKeys() {
  1404. $keys = [ 'wgSecretKey' => 64 ];
  1405. if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) {
  1406. $keys['wgUpgradeKey'] = 16;
  1407. }
  1408. return $this->doGenerateKeys( $keys );
  1409. }
  1410. /**
  1411. * Generate a secret value for variables using our CryptRand generator.
  1412. * Produce a warning if the random source was insecure.
  1413. *
  1414. * @param array $keys
  1415. * @return Status
  1416. */
  1417. protected function doGenerateKeys( $keys ) {
  1418. $status = Status::newGood();
  1419. $strong = true;
  1420. foreach ( $keys as $name => $length ) {
  1421. $secretKey = MWCryptRand::generateHex( $length, true );
  1422. if ( !MWCryptRand::wasStrong() ) {
  1423. $strong = false;
  1424. }
  1425. $this->setVar( $name, $secretKey );
  1426. }
  1427. if ( !$strong ) {
  1428. $names = array_keys( $keys );
  1429. $names = preg_replace( '/^(.*)$/', '\$$1', $names );
  1430. global $wgLang;
  1431. $status->warning( 'config-insecure-keys', $wgLang->listToText( $names ), count( $names ) );
  1432. }
  1433. return $status;
  1434. }
  1435. /**
  1436. * Create the first user account, grant it sysop and bureaucrat rights
  1437. *
  1438. * @return Status
  1439. */
  1440. protected function createSysop() {
  1441. $name = $this->getVar( '_AdminName' );
  1442. $user = User::newFromName( $name );
  1443. if ( !$user ) {
  1444. // We should've validated this earlier anyway!
  1445. return Status::newFatal( 'config-admin-error-user', $name );
  1446. }
  1447. if ( $user->idForName() == 0 ) {
  1448. $user->addToDatabase();
  1449. try {
  1450. $user->setPassword( $this->getVar( '_AdminPassword' ) );
  1451. } catch ( PasswordError $pwe ) {
  1452. return Status::newFatal( 'config-admin-error-password', $name, $pwe->getMessage() );
  1453. }
  1454. $user->addGroup( 'sysop' );
  1455. $user->addGroup( 'bureaucrat' );
  1456. if ( $this->getVar( '_AdminEmail' ) ) {
  1457. $user->setEmail( $this->getVar( '_AdminEmail' ) );
  1458. }
  1459. $user->saveSettings();
  1460. // Update user count
  1461. $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] );
  1462. $ssUpdate->doUpdate();
  1463. }
  1464. $status = Status::newGood();
  1465. if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) {
  1466. $this->subscribeToMediaWikiAnnounce( $status );
  1467. }
  1468. return $status;
  1469. }
  1470. /**
  1471. * @param Status $s
  1472. */
  1473. private function subscribeToMediaWikiAnnounce( Status $s ) {
  1474. $params = [
  1475. 'email' => $this->getVar( '_AdminEmail' ),
  1476. 'language' => 'en',
  1477. 'digest' => 0
  1478. ];
  1479. // Mailman doesn't support as many languages as we do, so check to make
  1480. // sure their selected language is available
  1481. $myLang = $this->getVar( '_UserLang' );
  1482. if ( in_array( $myLang, $this->mediaWikiAnnounceLanguages ) ) {
  1483. $myLang = $myLang == 'pt-br' ? 'pt_BR' : $myLang; // rewrite to Mailman's pt_BR
  1484. $params['language'] = $myLang;
  1485. }
  1486. if ( MWHttpRequest::canMakeRequests() ) {
  1487. $res = MWHttpRequest::factory( $this->mediaWikiAnnounceUrl,
  1488. [ 'method' => 'POST', 'postData' => $params ], __METHOD__ )->execute();
  1489. if ( !$res->isOK() ) {
  1490. $s->warning( 'config-install-subscribe-fail', $res->getMessage() );
  1491. }
  1492. } else {
  1493. $s->warning( 'config-install-subscribe-notpossible' );
  1494. }
  1495. }
  1496. /**
  1497. * Insert Main Page with default content.
  1498. *
  1499. * @param DatabaseInstaller $installer
  1500. * @return Status
  1501. */
  1502. protected function createMainpage( DatabaseInstaller $installer ) {
  1503. $status = Status::newGood();
  1504. $title = Title::newMainPage();
  1505. if ( $title->exists() ) {
  1506. $status->warning( 'config-install-mainpage-exists' );
  1507. return $status;
  1508. }
  1509. try {
  1510. $page = WikiPage::factory( $title );
  1511. $content = new WikitextContent(
  1512. wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
  1513. wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
  1514. );
  1515. $status = $page->doEditContent( $content,
  1516. '',
  1517. EDIT_NEW,
  1518. false,
  1519. User::newFromName( 'MediaWiki default' )
  1520. );
  1521. } catch ( Exception $e ) {
  1522. // using raw, because $wgShowExceptionDetails can not be set yet
  1523. $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
  1524. }
  1525. return $status;
  1526. }
  1527. /**
  1528. * Override the necessary bits of the config to run an installation.
  1529. */
  1530. public static function overrideConfig() {
  1531. // Use PHP's built-in session handling, since MediaWiki's
  1532. // SessionHandler can't work before we have an object cache set up.
  1533. define( 'MW_NO_SESSION_HANDLER', 1 );
  1534. // Don't access the database
  1535. $GLOBALS['wgUseDatabaseMessages'] = false;
  1536. // Don't cache langconv tables
  1537. $GLOBALS['wgLanguageConverterCacheType'] = CACHE_NONE;
  1538. // Debug-friendly
  1539. $GLOBALS['wgShowExceptionDetails'] = true;
  1540. // Don't break forms
  1541. $GLOBALS['wgExternalLinkTarget'] = '_blank';
  1542. // Extended debugging
  1543. $GLOBALS['wgShowSQLErrors'] = true;
  1544. $GLOBALS['wgShowDBErrorBacktrace'] = true;
  1545. // Allow multiple ob_flush() calls
  1546. $GLOBALS['wgDisableOutputCompression'] = true;
  1547. // Use a sensible cookie prefix (not my_wiki)
  1548. $GLOBALS['wgCookiePrefix'] = 'mw_installer';
  1549. // Some of the environment checks make shell requests, remove limits
  1550. $GLOBALS['wgMaxShellMemory'] = 0;
  1551. // Override the default CookieSessionProvider with a dummy
  1552. // implementation that won't stomp on PHP's cookies.
  1553. $GLOBALS['wgSessionProviders'] = [
  1554. [
  1555. 'class' => InstallerSessionProvider::class,
  1556. 'args' => [ [
  1557. 'priority' => 1,
  1558. ] ]
  1559. ]
  1560. ];
  1561. // Don't try to use any object cache for SessionManager either.
  1562. $GLOBALS['wgSessionCacheType'] = CACHE_NONE;
  1563. }
  1564. /**
  1565. * Add an installation step following the given step.
  1566. *
  1567. * @param callable $callback A valid installation callback array, in this form:
  1568. * [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ];
  1569. * @param string $findStep The step to find. Omit to put the step at the beginning
  1570. */
  1571. public function addInstallStep( $callback, $findStep = 'BEGINNING' ) {
  1572. $this->extraInstallSteps[$findStep][] = $callback;
  1573. }
  1574. /**
  1575. * Disable the time limit for execution.
  1576. * Some long-running pages (Install, Upgrade) will want to do this
  1577. */
  1578. protected function disableTimeLimit() {
  1579. Wikimedia\suppressWarnings();
  1580. set_time_limit( 0 );
  1581. Wikimedia\restoreWarnings();
  1582. }
  1583. }