Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 640 |
|
0.00% |
0 / 63 |
CRAP | |
0.00% |
0 / 1 |
Installer | |
0.00% |
0 / 640 |
|
0.00% |
0 / 63 |
39402 | |
0.00% |
0 / 1 |
showMessage | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
showError | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
showStatusMessage | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getInstallerConfig | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getDefaultSettings | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
resetMediaWikiServices | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
20 | |||
getDBTypes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doEnvironmentChecks | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
doEnvironmentPreps | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
setVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getVar | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCompiledDBs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDBInstallerClass | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDBInstaller | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getExistingLocalSettings | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
getFakePassword | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setPassword | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
maybeGetWebserverPrimaryGroup | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
parse | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getParserOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableLinkPopups | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
restoreLinkPopups | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
populateSiteStats | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
envCheckDB | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
envCheckPCRE | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
envCheckMemory | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
envCheckCache | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
envCheckModSecurity | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
envCheckDiff3 | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
envCheckGraphics | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
envCheckGit | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
envCheckServer | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
envCheckPath | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
envCheckUploadsDirectory | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
envCheckUploadsServerResponse | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
envCheck64Bit | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
envCheckLibicu | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
envPrepServer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
envGetDefaultServer | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
envPrepPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
dirIsExecutable | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
apacheModulePresent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
setParserLanguage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDocUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findExtensions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
findExtensionsByType | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
90 | |||
getExtensionInfo | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
156 | |||
readExtension | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
380 | |||
getDefaultSkin | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
includeExtensions | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 | |||
getAutoExtensionLegacyHooks | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
includeExtensionFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getAutoExtensionData | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getAutoExtensionHookContainer | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getVirtualDomains | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInstallSteps | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
30 | |||
performInstallation | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
generateKeys | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
restoreServices | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
doGenerateKeys | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
createSysop | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
56 | |||
subscribeToMediaWikiAnnounce | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
56 | |||
createMainpage | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
overrideConfig | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
6 | |||
addInstallStep | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableTimeLimit | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 |
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 Installer |
25 | */ |
26 | |
27 | namespace MediaWiki\Installer; |
28 | |
29 | use AutoLoader; |
30 | use EmptyBagOStuff; |
31 | use Exception; |
32 | use ExecutableFinder; |
33 | use ExtensionDependencyError; |
34 | use ExtensionProcessor; |
35 | use ExtensionRegistry; |
36 | use GuzzleHttp\Psr7\Header; |
37 | use IntlChar; |
38 | use InvalidArgumentException; |
39 | use Language; |
40 | use LogicException; |
41 | use MediaWiki\Config\Config; |
42 | use MediaWiki\Config\GlobalVarConfig; |
43 | use MediaWiki\Config\HashConfig; |
44 | use MediaWiki\Config\MultiConfig; |
45 | use MediaWiki\Context\RequestContext; |
46 | use MediaWiki\Deferred\SiteStatsUpdate; |
47 | use MediaWiki\HookContainer\HookContainer; |
48 | use MediaWiki\HookContainer\StaticHookRegistry; |
49 | use MediaWiki\Interwiki\NullInterwikiLookup; |
50 | use MediaWiki\MainConfigNames; |
51 | use MediaWiki\MainConfigSchema; |
52 | use MediaWiki\MediaWikiServices; |
53 | use MediaWiki\Parser\Parser; |
54 | use MediaWiki\Settings\SettingsBuilder; |
55 | use MediaWiki\Status\Status; |
56 | use MediaWiki\StubObject\StubGlobalUser; |
57 | use MediaWiki\Title\Title; |
58 | use MediaWiki\User\StaticUserOptionsLookup; |
59 | use MediaWiki\User\User; |
60 | use MWCryptRand; |
61 | use ParserOptions; |
62 | use RuntimeException; |
63 | use Wikimedia\AtEase\AtEase; |
64 | use Wikimedia\Services\ServiceDisabledException; |
65 | use WikitextContent; |
66 | |
67 | /** |
68 | * The Installer helps admins create or upgrade their wiki. |
69 | * |
70 | * The installer classes are exposed through these human interfaces: |
71 | * |
72 | * - The `maintenance/install.php` script, backed by CliInstaller. |
73 | * - The `maintenance/update.php` script, backed by DatabaseUpdater. |
74 | * - The `mw-config/index.php` web entry point, backed by WebInstaller. |
75 | * |
76 | * @defgroup Installer Installer |
77 | */ |
78 | |
79 | /** |
80 | * Base installer class. |
81 | * |
82 | * This class provides the base for installation and update functionality |
83 | * for both MediaWiki core and extensions. |
84 | * |
85 | * @ingroup Installer |
86 | * @since 1.17 |
87 | */ |
88 | abstract class Installer { |
89 | |
90 | /** |
91 | * URL to mediawiki-announce list summary page |
92 | */ |
93 | private const MEDIAWIKI_ANNOUNCE_URL = |
94 | 'https://lists.wikimedia.org/postorius/lists/mediawiki-announce.lists.wikimedia.org/'; |
95 | |
96 | /** |
97 | * @var array |
98 | */ |
99 | protected $settings; |
100 | |
101 | /** |
102 | * List of detected DBs, access using getCompiledDBs(). |
103 | * |
104 | * @var array |
105 | */ |
106 | protected $compiledDBs; |
107 | |
108 | /** |
109 | * Cached DB installer instances, access using getDBInstaller(). |
110 | * |
111 | * @var array |
112 | */ |
113 | protected $dbInstallers = []; |
114 | |
115 | /** |
116 | * Minimum memory size in MiB. |
117 | * |
118 | * @var int |
119 | */ |
120 | protected $minMemorySize = 50; |
121 | |
122 | /** |
123 | * Cached Title, used by parse(). |
124 | * |
125 | * @var Title |
126 | */ |
127 | protected $parserTitle; |
128 | |
129 | /** |
130 | * Cached ParserOptions, used by parse(). |
131 | * |
132 | * @var ParserOptions |
133 | */ |
134 | protected $parserOptions; |
135 | |
136 | /** |
137 | * Known database types. These correspond to the class names <type>Installer, |
138 | * and are also MediaWiki database types valid for $wgDBtype. |
139 | * |
140 | * To add a new type, create a <type>Installer class and a Database<type> |
141 | * class, and add a config-type-<type> message to MessagesEn.php. |
142 | * |
143 | * @var array |
144 | */ |
145 | protected static $dbTypes = [ |
146 | 'mysql', |
147 | 'postgres', |
148 | 'sqlite', |
149 | ]; |
150 | |
151 | /** |
152 | * A list of environment check methods called by doEnvironmentChecks(). |
153 | * These may output warnings using showMessage(), and/or abort the |
154 | * installation process by returning false. |
155 | * |
156 | * For the WebInstaller these are only called on the Welcome page, |
157 | * if these methods have side-effects that should affect later page loads |
158 | * (as well as the generated stylesheet), use envPreps instead. |
159 | * |
160 | * @var array |
161 | */ |
162 | protected $envChecks = [ |
163 | 'envCheckLibicu', |
164 | 'envCheckDB', |
165 | 'envCheckPCRE', |
166 | 'envCheckMemory', |
167 | 'envCheckCache', |
168 | 'envCheckModSecurity', |
169 | 'envCheckDiff3', |
170 | 'envCheckGraphics', |
171 | 'envCheckGit', |
172 | 'envCheckServer', |
173 | 'envCheckPath', |
174 | 'envCheckUploadsDirectory', |
175 | 'envCheckUploadsServerResponse', |
176 | 'envCheck64Bit', |
177 | ]; |
178 | |
179 | /** |
180 | * A list of environment preparation methods called by doEnvironmentPreps(). |
181 | * |
182 | * @var array |
183 | */ |
184 | protected $envPreps = [ |
185 | 'envPrepServer', |
186 | 'envPrepPath', |
187 | ]; |
188 | |
189 | /** |
190 | * MediaWiki configuration globals that will eventually be passed through |
191 | * to LocalSettings.php. The names only are given here, the defaults |
192 | * typically come from config-schema.yaml. |
193 | * |
194 | * @var array |
195 | */ |
196 | private const DEFAULT_VAR_NAMES = [ |
197 | MainConfigNames::Sitename, |
198 | MainConfigNames::PasswordSender, |
199 | MainConfigNames::LanguageCode, |
200 | MainConfigNames::Localtimezone, |
201 | MainConfigNames::RightsIcon, |
202 | MainConfigNames::RightsText, |
203 | MainConfigNames::RightsUrl, |
204 | MainConfigNames::EnableEmail, |
205 | MainConfigNames::EnableUserEmail, |
206 | MainConfigNames::EnotifUserTalk, |
207 | MainConfigNames::EnotifWatchlist, |
208 | MainConfigNames::EmailAuthentication, |
209 | MainConfigNames::DBname, |
210 | MainConfigNames::DBtype, |
211 | MainConfigNames::Diff3, |
212 | MainConfigNames::ImageMagickConvertCommand, |
213 | MainConfigNames::GitBin, |
214 | MainConfigNames::ScriptPath, |
215 | MainConfigNames::MetaNamespace, |
216 | MainConfigNames::DeletedDirectory, |
217 | MainConfigNames::EnableUploads, |
218 | MainConfigNames::SecretKey, |
219 | MainConfigNames::UseInstantCommons, |
220 | MainConfigNames::UpgradeKey, |
221 | MainConfigNames::DefaultSkin, |
222 | MainConfigNames::Pingback, |
223 | ]; |
224 | |
225 | /** |
226 | * Variables that are stored alongside globals, and are used for any |
227 | * configuration of the installation process aside from the MediaWiki |
228 | * configuration. Map of names to defaults. |
229 | * |
230 | * @var array |
231 | */ |
232 | protected $internalDefaults = [ |
233 | '_UserLang' => 'en', |
234 | '_Environment' => false, |
235 | '_RaiseMemory' => false, |
236 | '_UpgradeDone' => false, |
237 | '_InstallDone' => false, |
238 | '_Caches' => [], |
239 | '_InstallPassword' => '', |
240 | '_SameAccount' => true, |
241 | '_CreateDBAccount' => false, |
242 | '_NamespaceType' => 'site-name', |
243 | '_AdminName' => '', // will be set later, when the user selects language |
244 | '_AdminPassword' => '', |
245 | '_AdminPasswordConfirm' => '', |
246 | '_AdminEmail' => '', |
247 | '_Subscribe' => false, |
248 | '_SkipOptional' => 'continue', |
249 | '_RightsProfile' => 'wiki', |
250 | '_LicenseCode' => 'none', |
251 | '_CCDone' => false, |
252 | '_Extensions' => [], |
253 | '_Skins' => [], |
254 | '_MemCachedServers' => '', |
255 | '_UpgradeKeySupplied' => false, |
256 | '_ExistingDBSettings' => false, |
257 | '_LogoWordmark' => '', |
258 | '_LogoWordmarkWidth' => 119, |
259 | '_LogoWordmarkHeight' => 18, |
260 | // Single quotes are intentional, LocalSettingsGenerator must output this unescaped. |
261 | '_Logo1x' => '$wgResourceBasePath/resources/assets/change-your-logo.svg', |
262 | '_LogoIcon' => '$wgResourceBasePath/resources/assets/change-your-logo-icon.svg', |
263 | '_LogoTagline' => '', |
264 | '_LogoTaglineWidth' => 117, |
265 | '_LogoTaglineHeight' => 13, |
266 | '_WithDevelopmentSettings' => false, |
267 | 'wgAuthenticationTokenVersion' => 1, |
268 | ]; |
269 | |
270 | /** |
271 | * The actual list of installation steps. This will be initialized by getInstallSteps() |
272 | * |
273 | * @var array[] |
274 | * @phan-var array<int,array{name:string,callback:array{0:object,1:string}}> |
275 | */ |
276 | private $installSteps = []; |
277 | |
278 | /** |
279 | * Extra steps for installation, for things like DatabaseInstallers to modify |
280 | * |
281 | * @var array |
282 | */ |
283 | protected $extraInstallSteps = []; |
284 | |
285 | /** |
286 | * Known object cache types and the functions used to test for their existence. |
287 | * |
288 | * @var array |
289 | */ |
290 | protected $objectCaches = [ |
291 | 'apcu' => 'apcu_fetch', |
292 | 'wincache' => 'wincache_ucache_get' |
293 | ]; |
294 | |
295 | /** |
296 | * User rights profiles. |
297 | * |
298 | * @var array |
299 | */ |
300 | public $rightsProfiles = [ |
301 | 'wiki' => [], |
302 | 'no-anon' => [ |
303 | '*' => [ 'edit' => false ] |
304 | ], |
305 | 'fishbowl' => [ |
306 | '*' => [ |
307 | 'createaccount' => false, |
308 | 'edit' => false, |
309 | ], |
310 | ], |
311 | 'private' => [ |
312 | '*' => [ |
313 | 'createaccount' => false, |
314 | 'edit' => false, |
315 | 'read' => false, |
316 | ], |
317 | ], |
318 | ]; |
319 | |
320 | /** |
321 | * License types. |
322 | * |
323 | * @var array |
324 | */ |
325 | public $licenses = [ |
326 | 'cc-by' => [ |
327 | 'url' => 'https://creativecommons.org/licenses/by/4.0/', |
328 | 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by.png', |
329 | ], |
330 | 'cc-by-sa' => [ |
331 | 'url' => 'https://creativecommons.org/licenses/by-sa/4.0/', |
332 | 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-sa.png', |
333 | ], |
334 | 'cc-by-nc-sa' => [ |
335 | 'url' => 'https://creativecommons.org/licenses/by-nc-sa/4.0/', |
336 | 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-by-nc-sa.png', |
337 | ], |
338 | 'cc-0' => [ |
339 | 'url' => 'https://creativecommons.org/publicdomain/zero/1.0/', |
340 | 'icon' => '$wgResourceBasePath/resources/assets/licenses/cc-0.png', |
341 | ], |
342 | 'gfdl' => [ |
343 | 'url' => 'https://www.gnu.org/copyleft/fdl.html', |
344 | 'icon' => '$wgResourceBasePath/resources/assets/licenses/gnu-fdl.png', |
345 | ], |
346 | 'none' => [ |
347 | 'url' => '', |
348 | 'icon' => '', |
349 | 'text' => '' |
350 | ], |
351 | ]; |
352 | |
353 | /** |
354 | * @var HookContainer|null |
355 | */ |
356 | protected $autoExtensionHookContainer; |
357 | protected array $virtualDomains = []; |
358 | |
359 | /** |
360 | * UI interface for displaying a short message |
361 | * The parameters are like parameters to wfMessage(). |
362 | * The messages will be in wikitext format, which will be converted to an |
363 | * output format such as HTML or text before being sent to the user. |
364 | * @param string $msg |
365 | * @param mixed ...$params |
366 | */ |
367 | abstract public function showMessage( $msg, ...$params ); |
368 | |
369 | /** |
370 | * Same as showMessage(), but for displaying errors |
371 | * @param string $msg |
372 | * @param mixed ...$params |
373 | */ |
374 | abstract public function showError( $msg, ...$params ); |
375 | |
376 | /** |
377 | * Show a message to the installing user by using a Status object |
378 | * @param Status $status |
379 | */ |
380 | abstract public function showStatusMessage( Status $status ); |
381 | |
382 | /** |
383 | * Constructs a Config object that contains configuration settings that should be |
384 | * overwritten for the installation process. |
385 | * |
386 | * @since 1.27 |
387 | * |
388 | * @param Config $baseConfig |
389 | * |
390 | * @return Config The config to use during installation. |
391 | */ |
392 | public static function getInstallerConfig( Config $baseConfig ) { |
393 | $configOverrides = new HashConfig(); |
394 | |
395 | // disable (problematic) object cache types explicitly, preserving all other (working) ones |
396 | // bug T113843 |
397 | $emptyCache = [ 'class' => EmptyBagOStuff::class ]; |
398 | |
399 | $objectCaches = [ |
400 | CACHE_NONE => $emptyCache, |
401 | CACHE_DB => $emptyCache, |
402 | CACHE_ANYTHING => $emptyCache, |
403 | CACHE_MEMCACHED => $emptyCache, |
404 | ] + $baseConfig->get( MainConfigNames::ObjectCaches ); |
405 | |
406 | $configOverrides->set( MainConfigNames::ObjectCaches, $objectCaches ); |
407 | |
408 | // Load the installer's i18n. |
409 | $messageDirs = $baseConfig->get( MainConfigNames::MessagesDirs ); |
410 | $messageDirs['MediaWikiInstaller'] = __DIR__ . '/i18n'; |
411 | |
412 | $configOverrides->set( MainConfigNames::MessagesDirs, $messageDirs ); |
413 | |
414 | $installerConfig = new MultiConfig( [ $configOverrides, $baseConfig ] ); |
415 | |
416 | // make sure we use the installer config as the main config |
417 | $configRegistry = $baseConfig->get( MainConfigNames::ConfigRegistry ); |
418 | $configRegistry['main'] = static function () use ( $installerConfig ) { |
419 | return $installerConfig; |
420 | }; |
421 | |
422 | $configOverrides->set( MainConfigNames::ConfigRegistry, $configRegistry ); |
423 | |
424 | return $installerConfig; |
425 | } |
426 | |
427 | /** |
428 | * Constructor, always call this from child classes. |
429 | */ |
430 | public function __construct() { |
431 | $defaultConfig = new GlobalVarConfig(); // all the defaults from config-schema.yaml. |
432 | $installerConfig = self::getInstallerConfig( $defaultConfig ); |
433 | |
434 | // Disable all storage services, since we don't have any configuration yet! |
435 | $this->resetMediaWikiServices( $installerConfig, [], true ); |
436 | |
437 | $this->settings = $this->getDefaultSettings(); |
438 | |
439 | $this->doEnvironmentPreps(); |
440 | |
441 | $this->compiledDBs = []; |
442 | foreach ( self::getDBTypes() as $type ) { |
443 | $installer = $this->getDBInstaller( $type ); |
444 | |
445 | if ( !$installer->isCompiled() ) { |
446 | continue; |
447 | } |
448 | $this->compiledDBs[] = $type; |
449 | } |
450 | |
451 | $this->parserTitle = Title::newFromText( 'Installer' ); |
452 | } |
453 | |
454 | /** |
455 | * @return array |
456 | */ |
457 | private function getDefaultSettings(): array { |
458 | global $wgLocaltimezone; |
459 | |
460 | $ret = $this->internalDefaults; |
461 | |
462 | foreach ( self::DEFAULT_VAR_NAMES as $name ) { |
463 | $var = "wg{$name}"; |
464 | $ret[$var] = MainConfigSchema::getDefaultValue( $name ); |
465 | } |
466 | |
467 | // Set $wgLocaltimezone to the value of the global, which SetupDynamicConfig.php will have |
468 | // set to something that is a valid timezone. |
469 | $ret['wgLocaltimezone'] = $wgLocaltimezone; |
470 | |
471 | return $ret; |
472 | } |
473 | |
474 | /** |
475 | * Reset the global service container and associated global state |
476 | * to accommodate different stages of the installation. |
477 | * @since 1.35 |
478 | * |
479 | * @param Config|null $installerConfig Config override. If null, the previous |
480 | * config will be inherited. |
481 | * @param array $serviceOverrides Service definition overrides. Values can be null to |
482 | * disable specific overrides that would be applied per default, namely |
483 | * 'InterwikiLookup' and 'UserOptionsLookup'. |
484 | * @param bool $disableStorage Whether MediaWikiServices::disableStorage() should be called. |
485 | * |
486 | * @return MediaWikiServices |
487 | */ |
488 | public function resetMediaWikiServices( |
489 | Config $installerConfig = null, |
490 | $serviceOverrides = [], |
491 | bool $disableStorage = false |
492 | ) { |
493 | global $wgObjectCaches, $wgLang; |
494 | |
495 | // Reset all services and inject config overrides. |
496 | // NOTE: This will reset existing instances, but not previous wiring overrides! |
497 | MediaWikiServices::resetGlobalInstance( $installerConfig ); |
498 | |
499 | $mwServices = MediaWikiServices::getInstance(); |
500 | |
501 | if ( $disableStorage ) { |
502 | $mwServices->disableStorage(); |
503 | } else { |
504 | // Default to partially disabling services. |
505 | |
506 | $serviceOverrides += [ |
507 | // Disable interwiki lookup, to avoid database access during parses |
508 | 'InterwikiLookup' => static function () { |
509 | return new NullInterwikiLookup(); |
510 | }, |
511 | |
512 | // Disable user options database fetching, only rely on default options. |
513 | 'UserOptionsLookup' => static function ( MediaWikiServices $services ) { |
514 | return new StaticUserOptionsLookup( |
515 | [], |
516 | $services->getMainConfig()->get( MainConfigNames::DefaultUserOptions ) |
517 | ); |
518 | }, |
519 | |
520 | // Restore to default wiring, in case it was overwritten by disableStorage() |
521 | 'DBLoadBalancer' => static function ( MediaWikiServices $services ) { |
522 | return $services->getDBLoadBalancerFactory()->getMainLB(); |
523 | }, |
524 | ]; |
525 | } |
526 | |
527 | $lang = $this->getVar( '_UserLang', 'en' ); |
528 | |
529 | foreach ( $serviceOverrides as $name => $callback ) { |
530 | // Skip if the caller set $callback to null |
531 | // to suppress default overrides. |
532 | if ( $callback ) { |
533 | $mwServices->redefineService( $name, $callback ); |
534 | } |
535 | } |
536 | |
537 | // Disable i18n cache |
538 | $mwServices->getLocalisationCache()->disableBackend(); |
539 | |
540 | // Set a fake user. |
541 | // Note that this will reset the context's language, |
542 | // so set the user before setting the language. |
543 | $user = User::newFromId( 0 ); |
544 | StubGlobalUser::setUser( $user ); |
545 | |
546 | RequestContext::getMain()->setUser( $user ); |
547 | |
548 | // Don't attempt to load user language options (T126177) |
549 | // This will be overridden in the web installer with the user-specified language |
550 | // Ensure $wgLang does not have a reference to a stale LocalisationCache instance |
551 | // (T241638, T261081) |
552 | RequestContext::getMain()->setLanguage( $lang ); |
553 | $wgLang = RequestContext::getMain()->getLanguage(); |
554 | |
555 | // Disable object cache (otherwise CACHE_ANYTHING will try CACHE_DB and |
556 | // SqlBagOStuff will then throw since we just disabled wfGetDB) |
557 | $wgObjectCaches = $mwServices->getMainConfig()->get( MainConfigNames::ObjectCaches ); |
558 | |
559 | $this->parserOptions = new ParserOptions( $user ); // language will be wrong :( |
560 | // Don't try to access DB before user language is initialised |
561 | $this->setParserLanguage( $mwServices->getLanguageFactory()->getLanguage( 'en' ) ); |
562 | |
563 | return $mwServices; |
564 | } |
565 | |
566 | /** |
567 | * Get a list of known DB types. |
568 | * |
569 | * @return array |
570 | */ |
571 | public static function getDBTypes() { |
572 | return self::$dbTypes; |
573 | } |
574 | |
575 | /** |
576 | * Do initial checks of the PHP environment. Set variables according to |
577 | * the observed environment. |
578 | * |
579 | * It's possible that this may be called under the CLI SAPI, not the SAPI |
580 | * that the wiki will primarily run under. In that case, the subclass should |
581 | * initialise variables such as wgScriptPath, before calling this function. |
582 | * |
583 | * It can already be assumed that a supported PHP version is in use. Under |
584 | * the web subclass, it can also be assumed that sessions are working. |
585 | * |
586 | * @return Status |
587 | */ |
588 | public function doEnvironmentChecks() { |
589 | // PHP version has already been checked by entry scripts |
590 | // Show message here for information purposes |
591 | $this->showMessage( 'config-env-php', PHP_VERSION ); |
592 | |
593 | $good = true; |
594 | foreach ( $this->envChecks as $check ) { |
595 | $status = $this->$check(); |
596 | if ( $status === false ) { |
597 | $good = false; |
598 | } |
599 | } |
600 | |
601 | $this->setVar( '_Environment', $good ); |
602 | |
603 | return $good ? Status::newGood() : Status::newFatal( 'config-env-bad' ); |
604 | } |
605 | |
606 | public function doEnvironmentPreps() { |
607 | foreach ( $this->envPreps as $prep ) { |
608 | $this->$prep(); |
609 | } |
610 | } |
611 | |
612 | /** |
613 | * Set a MW configuration variable, or internal installer configuration variable. |
614 | * |
615 | * @param string $name |
616 | * @param mixed $value |
617 | */ |
618 | public function setVar( $name, $value ) { |
619 | $this->settings[$name] = $value; |
620 | } |
621 | |
622 | /** |
623 | * Get an MW configuration variable, or internal installer configuration variable. |
624 | * The defaults come from MainConfigSchema. |
625 | * Installer variables are typically prefixed by an underscore. |
626 | * |
627 | * @param string $name |
628 | * @param mixed|null $default |
629 | * |
630 | * @return mixed |
631 | */ |
632 | public function getVar( $name, $default = null ) { |
633 | return $this->settings[$name] ?? $default; |
634 | } |
635 | |
636 | /** |
637 | * Get a list of DBs supported by current PHP setup |
638 | * |
639 | * @return array |
640 | */ |
641 | public function getCompiledDBs() { |
642 | return $this->compiledDBs; |
643 | } |
644 | |
645 | /** |
646 | * Get the DatabaseInstaller class name for this type |
647 | * |
648 | * @param string $type database type ($wgDBtype) |
649 | * @return string Class name |
650 | * @since 1.30 |
651 | */ |
652 | public static function getDBInstallerClass( $type ) { |
653 | return '\\MediaWiki\\Installer\\' . ucfirst( $type ) . 'Installer'; |
654 | } |
655 | |
656 | /** |
657 | * Get an instance of DatabaseInstaller for the specified DB type. |
658 | * |
659 | * @param mixed $type DB installer for which is needed, false to use default. |
660 | * |
661 | * @return DatabaseInstaller |
662 | */ |
663 | public function getDBInstaller( $type = false ) { |
664 | if ( !$type ) { |
665 | $type = $this->getVar( 'wgDBtype' ); |
666 | } |
667 | |
668 | $type = strtolower( $type ); |
669 | |
670 | if ( !isset( $this->dbInstallers[$type] ) ) { |
671 | $class = self::getDBInstallerClass( $type ); |
672 | $this->dbInstallers[$type] = new $class( $this ); |
673 | } |
674 | |
675 | return $this->dbInstallers[$type]; |
676 | } |
677 | |
678 | /** |
679 | * Determine if LocalSettings.php exists. If it does, return its variables. |
680 | * |
681 | * @return array|false |
682 | */ |
683 | public static function getExistingLocalSettings() { |
684 | $IP = wfDetectInstallPath(); |
685 | |
686 | // You might be wondering why this is here. Well if you don't do this |
687 | // then some poorly-formed extensions try to call their own classes |
688 | // after immediately registering them. We really need to get extension |
689 | // registration out of the global scope and into a real format. |
690 | // @see https://phabricator.wikimedia.org/T69440 |
691 | global $wgAutoloadClasses; |
692 | $wgAutoloadClasses = []; |
693 | |
694 | // LocalSettings.php should not call functions, except wfLoadSkin/wfLoadExtensions |
695 | // Define the required globals here, to ensure, the functions can do it work correctly. |
696 | // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables |
697 | global $wgExtensionDirectory, $wgStyleDirectory; |
698 | |
699 | // This will also define MW_CONFIG_FILE |
700 | $lsFile = wfDetectLocalSettingsFile( $IP ); |
701 | // phpcs:ignore Generic.PHP.NoSilencedErrors |
702 | $lsExists = @file_exists( $lsFile ); |
703 | |
704 | if ( !$lsExists ) { |
705 | return false; |
706 | } |
707 | |
708 | if ( !str_ends_with( $lsFile, '.php' ) ) { |
709 | throw new RuntimeException( |
710 | 'The installer cannot yet handle non-php settings files: ' . $lsFile . '. ' . |
711 | 'Use `php maintenance/run.php update` to update an existing installation.' |
712 | ); |
713 | } |
714 | unset( $lsExists ); |
715 | |
716 | // Extract the defaults into the current scope |
717 | foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) { |
718 | $$var = $value; |
719 | } |
720 | |
721 | $wgExtensionDirectory = "$IP/extensions"; |
722 | $wgStyleDirectory = "$IP/skins"; |
723 | |
724 | // NOTE: To support YAML settings files, this needs to start using SettingsBuilder. |
725 | // However, as of 1.38, YAML settings files are still experimental and |
726 | // SettingsBuilder is still unstable. For now, the installer will fail if |
727 | // the existing settings file is not PHP. The updater should still work though. |
728 | // NOTE: When adding support for YAML settings file, all references to LocalSettings.php |
729 | // in localisation messages need to be replaced. |
730 | // NOTE: This assumes simple variable assignments. More complex setups may involve |
731 | // settings coming from sub-required and/or functions that assign globals |
732 | // directly. This is fine here because this isn't used as the "real" include. |
733 | // It is only used for reading out a small set of variables that the installer |
734 | // validates and/or displays. |
735 | require $lsFile; |
736 | |
737 | return get_defined_vars(); |
738 | } |
739 | |
740 | /** |
741 | * Get a fake password for sending back to the user in HTML. |
742 | * This is a security mechanism to avoid compromise of the password in the |
743 | * event of session ID compromise. |
744 | * |
745 | * @param string $realPassword |
746 | * |
747 | * @return string |
748 | */ |
749 | public function getFakePassword( $realPassword ) { |
750 | return str_repeat( '*', strlen( $realPassword ) ); |
751 | } |
752 | |
753 | /** |
754 | * Set a variable which stores a password, except if the new value is a |
755 | * fake password in which case leave it as it is. |
756 | * |
757 | * @param string $name |
758 | * @param mixed $value |
759 | */ |
760 | public function setPassword( $name, $value ) { |
761 | if ( !preg_match( '/^\*+$/', $value ) ) { |
762 | $this->setVar( $name, $value ); |
763 | } |
764 | } |
765 | |
766 | /** |
767 | * On POSIX systems return the primary group of the webserver we're running under. |
768 | * On other systems just returns null. |
769 | * |
770 | * This is used to advice the user that he should chgrp his mw-config/data/images directory as the |
771 | * webserver user before he can install. |
772 | * |
773 | * Public because SqliteInstaller needs it, and doesn't subclass Installer. |
774 | * |
775 | * @return mixed |
776 | */ |
777 | public static function maybeGetWebserverPrimaryGroup() { |
778 | if ( !function_exists( 'posix_getegid' ) || !function_exists( 'posix_getpwuid' ) ) { |
779 | # I don't know this, this isn't UNIX. |
780 | return null; |
781 | } |
782 | |
783 | # posix_getegid() *not* getmygid() because we want the group of the webserver, |
784 | # not whoever owns the current script. |
785 | $gid = posix_getegid(); |
786 | return posix_getpwuid( $gid )['name'] ?? null; |
787 | } |
788 | |
789 | /** |
790 | * Convert wikitext $text to HTML. |
791 | * |
792 | * This is potentially error prone since many parser features require a complete |
793 | * installed MW database. The solution is to just not use those features when you |
794 | * write your messages. This appears to work well enough. Basic formatting and |
795 | * external links work just fine. |
796 | * |
797 | * But in case a translator decides to throw in a "#ifexist" or internal link or |
798 | * whatever, this function is guarded to catch the attempted DB access and to present |
799 | * some fallback text. |
800 | * |
801 | * @param string $text |
802 | * @param bool $lineStart |
803 | * @return string |
804 | */ |
805 | public function parse( $text, $lineStart = false ) { |
806 | $parser = MediaWikiServices::getInstance()->getParser(); |
807 | |
808 | try { |
809 | $out = $parser->parse( $text, $this->parserTitle, $this->parserOptions, $lineStart ); |
810 | $html = $out->getText( [ |
811 | 'enableSectionEditLinks' => false, |
812 | 'unwrap' => true, |
813 | ] ); |
814 | $html = Parser::stripOuterParagraph( $html ); |
815 | } catch ( ServiceDisabledException $e ) { |
816 | $html = '<!--DB access attempted during parse--> ' . htmlspecialchars( $text ); |
817 | } |
818 | |
819 | return $html; |
820 | } |
821 | |
822 | /** |
823 | * @return ParserOptions |
824 | */ |
825 | public function getParserOptions() { |
826 | return $this->parserOptions; |
827 | } |
828 | |
829 | public function disableLinkPopups() { |
830 | // T317647: This ParserOptions method is deprecated; we should be |
831 | // updating ExternalLinkTarget in the Configuration instead. |
832 | $this->parserOptions->setExternalLinkTarget( false ); |
833 | } |
834 | |
835 | public function restoreLinkPopups() { |
836 | // T317647: This ParserOptions method is deprecated; we should be |
837 | // updating ExternalLinkTarget in the Configuration instead. |
838 | global $wgExternalLinkTarget; |
839 | $this->parserOptions->setExternalLinkTarget( $wgExternalLinkTarget ); |
840 | } |
841 | |
842 | /** |
843 | * Install step which adds a row to the site_stats table with appropriate |
844 | * initial values. |
845 | * |
846 | * @param DatabaseInstaller $installer |
847 | * |
848 | * @return Status |
849 | */ |
850 | public function populateSiteStats( DatabaseInstaller $installer ) { |
851 | $status = $installer->getConnection(); |
852 | if ( !$status->isOK() ) { |
853 | return $status; |
854 | } |
855 | $status->getDB()->insert( |
856 | 'site_stats', |
857 | [ |
858 | 'ss_row_id' => 1, |
859 | 'ss_total_edits' => 0, |
860 | 'ss_good_articles' => 0, |
861 | 'ss_total_pages' => 0, |
862 | 'ss_users' => 0, |
863 | 'ss_active_users' => 0, |
864 | 'ss_images' => 0 |
865 | ], |
866 | __METHOD__, |
867 | 'IGNORE' |
868 | ); |
869 | |
870 | return Status::newGood(); |
871 | } |
872 | |
873 | /** |
874 | * Environment check for DB types. |
875 | * @return bool |
876 | */ |
877 | protected function envCheckDB() { |
878 | global $wgLang; |
879 | /** @var string|null $dbType The user-specified database type */ |
880 | $dbType = $this->getVar( 'wgDBtype' ); |
881 | |
882 | $allNames = []; |
883 | |
884 | // Messages: config-type-mysql, config-type-postgres, config-type-sqlite |
885 | foreach ( self::getDBTypes() as $name ) { |
886 | $allNames[] = wfMessage( "config-type-$name" )->text(); |
887 | } |
888 | |
889 | $databases = $this->getCompiledDBs(); |
890 | |
891 | $databases = array_flip( $databases ); |
892 | $ok = true; |
893 | foreach ( $databases as $db => $_ ) { |
894 | $installer = $this->getDBInstaller( $db ); |
895 | $status = $installer->checkPrerequisites(); |
896 | if ( !$status->isGood() ) { |
897 | if ( !$this instanceof WebInstaller && $db === $dbType ) { |
898 | // Strictly check the key database type instead of just outputting message |
899 | // Note: No perform this check run from the web installer, since this method always called by |
900 | // the welcome page under web installation, so $dbType will always be 'mysql' |
901 | $ok = false; |
902 | } |
903 | $this->showStatusMessage( $status ); |
904 | unset( $databases[$db] ); |
905 | } |
906 | } |
907 | $databases = array_flip( $databases ); |
908 | if ( !$databases ) { |
909 | $this->showError( 'config-no-db', $wgLang->commaList( $allNames ), count( $allNames ) ); |
910 | return false; |
911 | } |
912 | return $ok; |
913 | } |
914 | |
915 | /** |
916 | * Check for known PCRE-related compatibility issues. |
917 | * |
918 | * @note We don't bother checking for Unicode support here. If it were |
919 | * missing, the parser would probably throw an exception before the |
920 | * result of this check is shown to the user. |
921 | * |
922 | * @return bool |
923 | */ |
924 | protected function envCheckPCRE() { |
925 | // PCRE2 must be compiled using NEWLINE_DEFAULT other than 4 (ANY); |
926 | // otherwise, it will misidentify UTF-8 trailing byte value 0x85 |
927 | // as a line ending character when in non-UTF mode. |
928 | if ( preg_match( '/^b.*c$/', 'bÄ…c' ) === 0 ) { |
929 | $this->showError( 'config-pcre-invalid-newline' ); |
930 | return false; |
931 | } |
932 | return true; |
933 | } |
934 | |
935 | /** |
936 | * Environment check for available memory. |
937 | * @return bool |
938 | */ |
939 | protected function envCheckMemory() { |
940 | $limit = ini_get( 'memory_limit' ); |
941 | |
942 | if ( !$limit || $limit == -1 ) { |
943 | return true; |
944 | } |
945 | |
946 | $n = wfShorthandToInteger( $limit ); |
947 | |
948 | if ( $n < $this->minMemorySize * 1024 * 1024 ) { |
949 | $newLimit = "{$this->minMemorySize}M"; |
950 | |
951 | if ( ini_set( "memory_limit", $newLimit ) === false ) { |
952 | $this->showMessage( 'config-memory-bad', $limit ); |
953 | } else { |
954 | $this->showMessage( 'config-memory-raised', $limit, $newLimit ); |
955 | $this->setVar( '_RaiseMemory', true ); |
956 | } |
957 | } |
958 | |
959 | return true; |
960 | } |
961 | |
962 | /** |
963 | * Environment check for compiled object cache types. |
964 | */ |
965 | protected function envCheckCache() { |
966 | $caches = []; |
967 | foreach ( $this->objectCaches as $name => $function ) { |
968 | if ( function_exists( $function ) ) { |
969 | $caches[$name] = true; |
970 | } |
971 | } |
972 | |
973 | if ( !$caches ) { |
974 | $this->showMessage( 'config-no-cache-apcu' ); |
975 | } |
976 | |
977 | $this->setVar( '_Caches', $caches ); |
978 | } |
979 | |
980 | /** |
981 | * Scare user to death if they have mod_security or mod_security2 |
982 | * @return bool |
983 | */ |
984 | protected function envCheckModSecurity() { |
985 | if ( self::apacheModulePresent( 'mod_security' ) |
986 | || self::apacheModulePresent( 'mod_security2' ) ) { |
987 | $this->showMessage( 'config-mod-security' ); |
988 | } |
989 | |
990 | return true; |
991 | } |
992 | |
993 | /** |
994 | * Search for GNU diff3. |
995 | * @return bool |
996 | */ |
997 | protected function envCheckDiff3() { |
998 | $names = [ "gdiff3", "diff3" ]; |
999 | if ( wfIsWindows() ) { |
1000 | $names[] = 'diff3.exe'; |
1001 | } |
1002 | $versionInfo = [ '--version', 'GNU diffutils' ]; |
1003 | |
1004 | $diff3 = ExecutableFinder::findInDefaultPaths( $names, $versionInfo ); |
1005 | |
1006 | if ( $diff3 ) { |
1007 | $this->setVar( 'wgDiff3', $diff3 ); |
1008 | } else { |
1009 | $this->setVar( 'wgDiff3', false ); |
1010 | $this->showMessage( 'config-diff3-bad' ); |
1011 | } |
1012 | |
1013 | return true; |
1014 | } |
1015 | |
1016 | /** |
1017 | * Environment check for ImageMagick and GD. |
1018 | * @return bool |
1019 | */ |
1020 | protected function envCheckGraphics() { |
1021 | $names = wfIsWindows() ? 'convert.exe' : 'convert'; |
1022 | $versionInfo = [ '-version', 'ImageMagick' ]; |
1023 | $convert = ExecutableFinder::findInDefaultPaths( $names, $versionInfo ); |
1024 | |
1025 | $this->setVar( 'wgImageMagickConvertCommand', '' ); |
1026 | if ( $convert ) { |
1027 | $this->setVar( 'wgImageMagickConvertCommand', $convert ); |
1028 | $this->showMessage( 'config-imagemagick', $convert ); |
1029 | } elseif ( function_exists( 'imagejpeg' ) ) { |
1030 | $this->showMessage( 'config-gd' ); |
1031 | } else { |
1032 | $this->showMessage( 'config-no-scaling' ); |
1033 | } |
1034 | |
1035 | return true; |
1036 | } |
1037 | |
1038 | /** |
1039 | * Search for git. |
1040 | * |
1041 | * @since 1.22 |
1042 | * @return bool |
1043 | */ |
1044 | protected function envCheckGit() { |
1045 | $names = wfIsWindows() ? 'git.exe' : 'git'; |
1046 | $versionInfo = [ '--version', 'git version' ]; |
1047 | |
1048 | $git = ExecutableFinder::findInDefaultPaths( $names, $versionInfo ); |
1049 | |
1050 | if ( $git ) { |
1051 | $this->setVar( 'wgGitBin', $git ); |
1052 | $this->showMessage( 'config-git', $git ); |
1053 | } else { |
1054 | $this->setVar( 'wgGitBin', false ); |
1055 | $this->showMessage( 'config-git-bad' ); |
1056 | } |
1057 | |
1058 | return true; |
1059 | } |
1060 | |
1061 | /** |
1062 | * Environment check to inform user which server we've assumed. |
1063 | * |
1064 | * @return bool |
1065 | */ |
1066 | protected function envCheckServer() { |
1067 | $server = $this->envGetDefaultServer(); |
1068 | if ( $server !== null ) { |
1069 | $this->showMessage( 'config-using-server', $server ); |
1070 | } |
1071 | return true; |
1072 | } |
1073 | |
1074 | /** |
1075 | * Environment check to inform user which paths we've assumed. |
1076 | * |
1077 | * @return bool |
1078 | */ |
1079 | protected function envCheckPath() { |
1080 | $this->showMessage( |
1081 | 'config-using-uri', |
1082 | $this->getVar( 'wgServer' ), |
1083 | $this->getVar( 'wgScriptPath' ) |
1084 | ); |
1085 | return true; |
1086 | } |
1087 | |
1088 | /** |
1089 | * Environment check for the permissions of the uploads directory |
1090 | * @return bool |
1091 | */ |
1092 | protected function envCheckUploadsDirectory() { |
1093 | global $IP; |
1094 | |
1095 | $dir = $IP . '/images/'; |
1096 | $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/'; |
1097 | $safe = !$this->dirIsExecutable( $dir, $url ); |
1098 | |
1099 | if ( !$safe ) { |
1100 | $this->showMessage( 'config-uploads-not-safe', $dir ); |
1101 | } |
1102 | |
1103 | return true; |
1104 | } |
1105 | |
1106 | protected function envCheckUploadsServerResponse() { |
1107 | $url = $this->getVar( 'wgServer' ) . $this->getVar( 'wgScriptPath' ) . '/images/README'; |
1108 | $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory(); |
1109 | $status = null; |
1110 | |
1111 | $req = $httpRequestFactory->create( |
1112 | $url, |
1113 | [ |
1114 | 'method' => 'GET', |
1115 | 'timeout' => 3, |
1116 | 'followRedirects' => true |
1117 | ], |
1118 | __METHOD__ |
1119 | ); |
1120 | try { |
1121 | $status = $req->execute(); |
1122 | } catch ( Exception $e ) { |
1123 | // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl |
1124 | // extension. |
1125 | } |
1126 | |
1127 | if ( !$status || !$status->isGood() ) { |
1128 | $this->showMessage( 'config-uploads-security-requesterror', 'X-Content-Type-Options: nosniff' ); |
1129 | return true; |
1130 | } |
1131 | |
1132 | $headerValue = $req->getResponseHeader( 'X-Content-Type-Options' ) ?? ''; |
1133 | $responseList = Header::splitList( $headerValue ); |
1134 | if ( !in_array( 'nosniff', $responseList, true ) ) { |
1135 | $this->showMessage( 'config-uploads-security-headers', 'X-Content-Type-Options: nosniff' ); |
1136 | } |
1137 | |
1138 | return true; |
1139 | } |
1140 | |
1141 | /** |
1142 | * Checks if we're running on 64 bit or not. 32 bit is becoming increasingly |
1143 | * hard to support, so let's at least warn people. |
1144 | * |
1145 | * @return bool |
1146 | */ |
1147 | protected function envCheck64Bit() { |
1148 | if ( PHP_INT_SIZE == 4 ) { |
1149 | $this->showMessage( 'config-using-32bit' ); |
1150 | } |
1151 | |
1152 | return true; |
1153 | } |
1154 | |
1155 | /** |
1156 | * Check and display the libicu and Unicode versions |
1157 | */ |
1158 | protected function envCheckLibicu() { |
1159 | $unicodeVersion = implode( '.', array_slice( IntlChar::getUnicodeVersion(), 0, 3 ) ); |
1160 | $this->showMessage( 'config-env-icu', INTL_ICU_VERSION, $unicodeVersion ); |
1161 | } |
1162 | |
1163 | /** |
1164 | * Environment prep for the server hostname. |
1165 | */ |
1166 | protected function envPrepServer() { |
1167 | $server = $this->envGetDefaultServer(); |
1168 | if ( $server !== null ) { |
1169 | $this->setVar( 'wgServer', $server ); |
1170 | } |
1171 | } |
1172 | |
1173 | /** |
1174 | * Helper function to be called from envPrepServer() |
1175 | * @return string |
1176 | */ |
1177 | abstract protected function envGetDefaultServer(); |
1178 | |
1179 | /** |
1180 | * Environment prep for setting $IP and $wgScriptPath. |
1181 | */ |
1182 | protected function envPrepPath() { |
1183 | global $IP; |
1184 | $IP = dirname( dirname( __DIR__ ) ); |
1185 | $this->setVar( 'IP', $IP ); |
1186 | } |
1187 | |
1188 | /** |
1189 | * Checks if scripts located in the given directory can be executed via the given URL. |
1190 | * |
1191 | * Used only by environment checks. |
1192 | * @param string $dir |
1193 | * @param string $url |
1194 | * @return bool|int|string |
1195 | */ |
1196 | public function dirIsExecutable( $dir, $url ) { |
1197 | $scriptTypes = [ |
1198 | 'php' => [ |
1199 | "<?php echo 'exec';", |
1200 | "#!/var/env php\n<?php echo 'exec';", |
1201 | ], |
1202 | ]; |
1203 | |
1204 | // it would be good to check other popular languages here, but it'll be slow. |
1205 | // TODO no need to have a loop if there is going to only be one script type |
1206 | |
1207 | $httpRequestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory(); |
1208 | |
1209 | AtEase::suppressWarnings(); |
1210 | |
1211 | foreach ( $scriptTypes as $ext => $contents ) { |
1212 | foreach ( $contents as $source ) { |
1213 | $file = 'exectest.' . $ext; |
1214 | |
1215 | if ( !file_put_contents( $dir . $file, $source ) ) { |
1216 | break; |
1217 | } |
1218 | |
1219 | try { |
1220 | $text = $httpRequestFactory->get( |
1221 | $url . $file, |
1222 | [ 'timeout' => 3 ], |
1223 | __METHOD__ |
1224 | ); |
1225 | } catch ( Exception $e ) { |
1226 | // HttpRequestFactory::get can throw with allow_url_fopen = false and no curl |
1227 | // extension. |
1228 | $text = null; |
1229 | } |
1230 | unlink( $dir . $file ); |
1231 | |
1232 | if ( $text == 'exec' ) { |
1233 | AtEase::restoreWarnings(); |
1234 | |
1235 | return $ext; |
1236 | } |
1237 | } |
1238 | } |
1239 | |
1240 | AtEase::restoreWarnings(); |
1241 | |
1242 | return false; |
1243 | } |
1244 | |
1245 | /** |
1246 | * Checks for presence of an Apache module. Works only if PHP is running as an Apache module, too. |
1247 | * |
1248 | * @param string $moduleName Name of module to check. |
1249 | * @return bool |
1250 | */ |
1251 | public static function apacheModulePresent( $moduleName ) { |
1252 | if ( function_exists( 'apache_get_modules' ) && in_array( $moduleName, apache_get_modules() ) ) { |
1253 | return true; |
1254 | } |
1255 | // try it the hard way |
1256 | ob_start(); |
1257 | phpinfo( INFO_MODULES ); |
1258 | $info = ob_get_clean(); |
1259 | |
1260 | return strpos( $info, $moduleName ) !== false; |
1261 | } |
1262 | |
1263 | /** |
1264 | * ParserOptions are constructed before we determined the language, so fix it |
1265 | * |
1266 | * @param Language $lang |
1267 | */ |
1268 | public function setParserLanguage( $lang ) { |
1269 | $this->parserOptions->setTargetLanguage( $lang ); |
1270 | $this->parserOptions->setUserLang( $lang ); |
1271 | } |
1272 | |
1273 | /** |
1274 | * Overridden by WebInstaller to provide lastPage parameters. |
1275 | * @param string $page |
1276 | * @return string |
1277 | */ |
1278 | protected function getDocUrl( $page ) { |
1279 | return "{$_SERVER['PHP_SELF']}?page=" . urlencode( $page ); |
1280 | } |
1281 | |
1282 | /** |
1283 | * Find extensions or skins in a subdirectory of $IP. |
1284 | * Returns an array containing the value for 'Name' for each found extension. |
1285 | * |
1286 | * @param string $directory Directory to search in, relative to $IP, must be either "extensions" |
1287 | * or "skins" |
1288 | * @return Status An object containing an error list. If there were no errors, an associative |
1289 | * array of information about the extension can be found in $status->value. |
1290 | */ |
1291 | public function findExtensions( $directory = 'extensions' ) { |
1292 | switch ( $directory ) { |
1293 | case 'extensions': |
1294 | return $this->findExtensionsByType( 'extension', 'extensions' ); |
1295 | case 'skins': |
1296 | return $this->findExtensionsByType( 'skin', 'skins' ); |
1297 | default: |
1298 | throw new InvalidArgumentException( "Invalid extension type" ); |
1299 | } |
1300 | } |
1301 | |
1302 | /** |
1303 | * Find extensions or skins, and return an array containing the value for 'Name' for each found |
1304 | * extension. |
1305 | * |
1306 | * @param string $type Either "extension" or "skin" |
1307 | * @param string $directory Directory to search in, relative to $IP |
1308 | * @return Status An object containing an error list. If there were no errors, an associative |
1309 | * array of information about the extension can be found in $status->value. |
1310 | */ |
1311 | protected function findExtensionsByType( $type = 'extension', $directory = 'extensions' ) { |
1312 | if ( $this->getVar( 'IP' ) === null ) { |
1313 | return Status::newGood( [] ); |
1314 | } |
1315 | |
1316 | $extDir = $this->getVar( 'IP' ) . '/' . $directory; |
1317 | if ( !is_readable( $extDir ) || !is_dir( $extDir ) ) { |
1318 | return Status::newGood( [] ); |
1319 | } |
1320 | |
1321 | $dh = opendir( $extDir ); |
1322 | $exts = []; |
1323 | $status = new Status; |
1324 | while ( ( $file = readdir( $dh ) ) !== false ) { |
1325 | // skip non-dirs and hidden directories |
1326 | if ( !is_dir( "$extDir/$file" ) || $file[0] === '.' ) { |
1327 | continue; |
1328 | } |
1329 | $extStatus = $this->getExtensionInfo( $type, $directory, $file ); |
1330 | if ( $extStatus->isOK() ) { |
1331 | $exts[$file] = $extStatus->value; |
1332 | } elseif ( $extStatus->hasMessage( 'config-extension-not-found' ) ) { |
1333 | // (T225512) The directory is not actually an extension. Downgrade to warning. |
1334 | $status->warning( 'config-extension-not-found', $file ); |
1335 | } else { |
1336 | $status->merge( $extStatus ); |
1337 | } |
1338 | } |
1339 | closedir( $dh ); |
1340 | uksort( $exts, 'strnatcasecmp' ); |
1341 | |
1342 | $status->value = $exts; |
1343 | |
1344 | return $status; |
1345 | } |
1346 | |
1347 | /** |
1348 | * @param string $type Either "extension" or "skin" |
1349 | * @param string $parentRelPath The parent directory relative to $IP |
1350 | * @param string $name The extension or skin name |
1351 | * @return Status An object containing an error list. If there were no errors, an associative |
1352 | * array of information about the extension can be found in $status->value. |
1353 | */ |
1354 | protected function getExtensionInfo( $type, $parentRelPath, $name ) { |
1355 | if ( $this->getVar( 'IP' ) === null ) { |
1356 | throw new RuntimeException( 'Cannot find extensions since the IP variable is not yet set' ); |
1357 | } |
1358 | if ( $type !== 'extension' && $type !== 'skin' ) { |
1359 | throw new InvalidArgumentException( "Invalid extension type" ); |
1360 | } |
1361 | $absDir = $this->getVar( 'IP' ) . "/$parentRelPath/$name"; |
1362 | $relDir = "../$parentRelPath/$name"; |
1363 | if ( !is_dir( $absDir ) ) { |
1364 | return Status::newFatal( 'config-extension-not-found', $name ); |
1365 | } |
1366 | $jsonFile = $type . '.json'; |
1367 | $fullJsonFile = "$absDir/$jsonFile"; |
1368 | $isJson = file_exists( $fullJsonFile ); |
1369 | $isPhp = false; |
1370 | if ( !$isJson ) { |
1371 | // Only fallback to PHP file if JSON doesn't exist |
1372 | $fullPhpFile = "$absDir/$name.php"; |
1373 | $isPhp = file_exists( $fullPhpFile ); |
1374 | } |
1375 | if ( !$isJson && !$isPhp ) { |
1376 | return Status::newFatal( 'config-extension-not-found', $name ); |
1377 | } |
1378 | |
1379 | // Extension exists. Now see if there are screenshots |
1380 | $info = []; |
1381 | if ( is_dir( "$absDir/screenshots" ) ) { |
1382 | $paths = glob( "$absDir/screenshots/*.png" ); |
1383 | foreach ( $paths as $path ) { |
1384 | $info['screenshots'][] = str_replace( $absDir, $relDir, $path ); |
1385 | } |
1386 | } |
1387 | |
1388 | if ( $isJson ) { |
1389 | $jsonStatus = $this->readExtension( $fullJsonFile ); |
1390 | if ( !$jsonStatus->isOK() ) { |
1391 | return $jsonStatus; |
1392 | } |
1393 | $info += $jsonStatus->value; |
1394 | } |
1395 | |
1396 | return Status::newGood( $info ); |
1397 | } |
1398 | |
1399 | /** |
1400 | * @param string $fullJsonFile |
1401 | * @param array $extDeps |
1402 | * @param array $skinDeps |
1403 | * |
1404 | * @return Status On success, an array of extension information is in $status->value. On |
1405 | * failure, the Status object will have an error list. |
1406 | */ |
1407 | private function readExtension( $fullJsonFile, $extDeps = [], $skinDeps = [] ) { |
1408 | $load = [ |
1409 | $fullJsonFile => 1 |
1410 | ]; |
1411 | if ( $extDeps ) { |
1412 | $extDir = $this->getVar( 'IP' ) . '/extensions'; |
1413 | foreach ( $extDeps as $dep ) { |
1414 | $fname = "$extDir/$dep/extension.json"; |
1415 | if ( !file_exists( $fname ) ) { |
1416 | return Status::newFatal( 'config-extension-not-found', $dep ); |
1417 | } |
1418 | $load[$fname] = 1; |
1419 | } |
1420 | } |
1421 | if ( $skinDeps ) { |
1422 | $skinDir = $this->getVar( 'IP' ) . '/skins'; |
1423 | foreach ( $skinDeps as $dep ) { |
1424 | $fname = "$skinDir/$dep/skin.json"; |
1425 | if ( !file_exists( $fname ) ) { |
1426 | return Status::newFatal( 'config-extension-not-found', $dep ); |
1427 | } |
1428 | $load[$fname] = 1; |
1429 | } |
1430 | } |
1431 | $registry = new ExtensionRegistry(); |
1432 | try { |
1433 | $info = $registry->readFromQueue( $load ); |
1434 | } catch ( ExtensionDependencyError $e ) { |
1435 | if ( $e->incompatibleCore || $e->incompatibleSkins |
1436 | || $e->incompatibleExtensions |
1437 | ) { |
1438 | // If something is incompatible with a dependency, we have no real |
1439 | // option besides skipping it |
1440 | return Status::newFatal( 'config-extension-dependency', |
1441 | basename( dirname( $fullJsonFile ) ), $e->getMessage() ); |
1442 | } elseif ( $e->missingExtensions || $e->missingSkins ) { |
1443 | // There's an extension missing in the dependency tree, |
1444 | // so add those to the dependency list and try again |
1445 | $status = $this->readExtension( |
1446 | $fullJsonFile, |
1447 | array_merge( $extDeps, $e->missingExtensions ), |
1448 | array_merge( $skinDeps, $e->missingSkins ) |
1449 | ); |
1450 | if ( !$status->isOK() && !$status->hasMessage( 'config-extension-dependency' ) ) { |
1451 | $status = Status::newFatal( 'config-extension-dependency', |
1452 | basename( dirname( $fullJsonFile ) ), $status->getMessage() ); |
1453 | } |
1454 | return $status; |
1455 | } |
1456 | // Some other kind of dependency error? |
1457 | return Status::newFatal( 'config-extension-dependency', |
1458 | basename( dirname( $fullJsonFile ) ), $e->getMessage() ); |
1459 | } |
1460 | $ret = []; |
1461 | // The order of credits will be the order of $load, |
1462 | // so the first extension is the one we want to load, |
1463 | // everything else is a dependency |
1464 | $i = 0; |
1465 | foreach ( $info['credits'] as $credit ) { |
1466 | $i++; |
1467 | if ( $i == 1 ) { |
1468 | // Extension we want to load |
1469 | continue; |
1470 | } |
1471 | $type = basename( $credit['path'] ) === 'skin.json' ? 'skins' : 'extensions'; |
1472 | $ret['requires'][$type][] = $credit['name']; |
1473 | } |
1474 | $credits = array_values( $info['credits'] )[0]; |
1475 | if ( isset( $credits['url'] ) ) { |
1476 | $ret['url'] = $credits['url']; |
1477 | } |
1478 | $ret['type'] = $credits['type']; |
1479 | |
1480 | return Status::newGood( $ret ); |
1481 | } |
1482 | |
1483 | /** |
1484 | * Returns a default value to be used for $wgDefaultSkin: normally the DefaultSkin from |
1485 | * config-schema.yaml, but will fall back to another if the default skin is missing |
1486 | * and some other one is present instead. |
1487 | * |
1488 | * @param string[] $skinNames Names of installed skins. |
1489 | * @return string |
1490 | */ |
1491 | public function getDefaultSkin( array $skinNames ) { |
1492 | $defaultSkin = $GLOBALS['wgDefaultSkin']; |
1493 | |
1494 | if ( in_array( 'vector', $skinNames ) ) { |
1495 | $skinNames[] = 'vector-2022'; |
1496 | } |
1497 | |
1498 | // T346332: Minerva skin uses different name from its directory name |
1499 | if ( in_array( 'minervaneue', $skinNames ) ) { |
1500 | $minervaNeue = array_search( 'minervaneue', $skinNames ); |
1501 | $skinNames[$minervaNeue] = 'minerva'; |
1502 | } |
1503 | |
1504 | if ( !$skinNames || in_array( $defaultSkin, $skinNames ) ) { |
1505 | return $defaultSkin; |
1506 | } else { |
1507 | return $skinNames[0]; |
1508 | } |
1509 | } |
1510 | |
1511 | /** |
1512 | * Installs the auto-detected extensions. |
1513 | * |
1514 | * @return Status |
1515 | */ |
1516 | protected function includeExtensions() { |
1517 | // Marker for DatabaseUpdater::loadExtensions so we don't |
1518 | // double load extensions |
1519 | define( 'MW_EXTENSIONS_LOADED', true ); |
1520 | |
1521 | $legacySchemaHooks = $this->getAutoExtensionLegacyHooks(); |
1522 | $data = $this->getAutoExtensionData(); |
1523 | if ( isset( $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ) ) { |
1524 | $legacySchemaHooks = array_merge( $legacySchemaHooks, |
1525 | $data['globals']['wgHooks']['LoadExtensionSchemaUpdates'] ); |
1526 | } |
1527 | $extDeprecatedHooks = $data['attributes']['DeprecatedHooks'] ?? []; |
1528 | |
1529 | $this->autoExtensionHookContainer = new HookContainer( |
1530 | new StaticHookRegistry( |
1531 | [ 'LoadExtensionSchemaUpdates' => $legacySchemaHooks ], |
1532 | $data['attributes']['Hooks'] ?? [], |
1533 | $extDeprecatedHooks |
1534 | ), |
1535 | MediaWikiServices::getInstance()->getObjectFactory() |
1536 | ); |
1537 | $this->virtualDomains = $data['attributes']['DatabaseVirtualDomains'] ?? []; |
1538 | |
1539 | return Status::newGood(); |
1540 | } |
1541 | |
1542 | /** |
1543 | * Auto-detect extensions with an old style .php registration file, load |
1544 | * the extensions, and return the merged $wgHooks array. |
1545 | * |
1546 | * @suppress SecurityCheck-PathTraversal It thinks $exts/$IP is user controlled but they are not. |
1547 | * @return array |
1548 | */ |
1549 | protected function getAutoExtensionLegacyHooks() { |
1550 | $exts = $this->getVar( '_Extensions' ); |
1551 | $installPath = $this->getVar( 'IP' ); |
1552 | $files = []; |
1553 | foreach ( $exts as $e ) { |
1554 | if ( file_exists( "$installPath/extensions/$e/$e.php" ) ) { |
1555 | $files[] = "$installPath/extensions/$e/$e.php"; |
1556 | } |
1557 | } |
1558 | |
1559 | if ( $files ) { |
1560 | return $this->includeExtensionFiles( $files ); |
1561 | } else { |
1562 | return []; |
1563 | } |
1564 | } |
1565 | |
1566 | /** |
1567 | * Include the specified extension PHP files. Populate $wgAutoloadClasses |
1568 | * and return the LoadExtensionSchemaUpdates hooks. |
1569 | * |
1570 | * @param string[] $files |
1571 | * @return array LoadExtensionSchemaUpdates legacy hooks |
1572 | */ |
1573 | protected function includeExtensionFiles( $files ) { |
1574 | global $IP; |
1575 | $IP = $this->getVar( 'IP' ); |
1576 | |
1577 | /** |
1578 | * We need to define the $wgXyz variables before including extensions to avoid |
1579 | * warnings about unset variables. However, the only thing we really |
1580 | * want here is $wgHooks['LoadExtensionSchemaUpdates']. This won't work |
1581 | * if the extension has hidden hook registration in $wgExtensionFunctions, |
1582 | * but we're not opening that can of worms |
1583 | * @see https://phabricator.wikimedia.org/T28857 |
1584 | */ |
1585 | // Extract the defaults into the current scope |
1586 | foreach ( MainConfigSchema::listDefaultValues( 'wg' ) as $var => $value ) { |
1587 | $$var = $value; |
1588 | } |
1589 | |
1590 | // phpcs:ignore MediaWiki.VariableAnalysis.UnusedGlobalVariables |
1591 | global $wgAutoloadClasses, $wgExtensionDirectory, $wgStyleDirectory; |
1592 | $wgExtensionDirectory = "$IP/extensions"; |
1593 | $wgStyleDirectory = "$IP/skins"; |
1594 | |
1595 | foreach ( $files as $file ) { |
1596 | require_once $file; |
1597 | } |
1598 | |
1599 | // @phpcs:disable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks |
1600 | // @phpcs:ignore Generic.Files.LineLength.TooLong |
1601 | // @phan-suppress-next-line PhanUndeclaredVariable,PhanCoalescingAlwaysNull $wgHooks is defined by MainConfigSchema |
1602 | $hooksWeWant = $wgHooks['LoadExtensionSchemaUpdates'] ?? []; |
1603 | // @phpcs:enable MediaWiki.VariableAnalysis.MisleadingGlobalNames.Misleading$wgHooks |
1604 | |
1605 | // Ignore everyone else's hooks. Lord knows what someone might be doing |
1606 | // in ParserFirstCallInit (see T29171) |
1607 | return [ 'LoadExtensionSchemaUpdates' => $hooksWeWant ]; |
1608 | } |
1609 | |
1610 | /** |
1611 | * Auto-detect extensions with an extension.json file. Load the extensions, |
1612 | * register classes with the autoloader and return the merged registry data. |
1613 | * |
1614 | * @return array |
1615 | */ |
1616 | protected function getAutoExtensionData() { |
1617 | $exts = $this->getVar( '_Extensions' ); |
1618 | $installPath = $this->getVar( 'IP' ); |
1619 | |
1620 | $extensionProcessor = new ExtensionProcessor(); |
1621 | foreach ( $exts as $e ) { |
1622 | $jsonPath = "$installPath/extensions/$e/extension.json"; |
1623 | if ( file_exists( $jsonPath ) ) { |
1624 | $extensionProcessor->extractInfoFromFile( $jsonPath ); |
1625 | } |
1626 | } |
1627 | |
1628 | $autoload = $extensionProcessor->getExtractedAutoloadInfo(); |
1629 | AutoLoader::loadFiles( $autoload['files'] ); |
1630 | AutoLoader::registerClasses( $autoload['classes'] ); |
1631 | AutoLoader::registerNamespaces( $autoload['namespaces'] ); |
1632 | |
1633 | return $extensionProcessor->getExtractedInfo(); |
1634 | } |
1635 | |
1636 | /** |
1637 | * Get the hook container previously populated by includeExtensions(). |
1638 | * |
1639 | * @internal For use by DatabaseInstaller |
1640 | * @since 1.36 |
1641 | * @return HookContainer |
1642 | */ |
1643 | public function getAutoExtensionHookContainer() { |
1644 | if ( !$this->autoExtensionHookContainer ) { |
1645 | throw new LogicException( __METHOD__ . |
1646 | ': includeExtensions() has not been called' ); |
1647 | } |
1648 | return $this->autoExtensionHookContainer; |
1649 | } |
1650 | |
1651 | /** |
1652 | * Get the virtual domains |
1653 | * |
1654 | * @internal For use by DatabaseInstaller |
1655 | * @since 1.42 |
1656 | * @return array |
1657 | */ |
1658 | public function getVirtualDomains(): array { |
1659 | return $this->virtualDomains; |
1660 | } |
1661 | |
1662 | /** |
1663 | * Get an array of install steps. Should always be in the format of |
1664 | * [ |
1665 | * 'name' => 'someuniquename', |
1666 | * 'callback' => [ $obj, 'method' ], |
1667 | * ] |
1668 | * There must be a config-install-$name message defined per step, which will |
1669 | * be shown on install. |
1670 | * |
1671 | * @param DatabaseInstaller $installer DatabaseInstaller so we can make callbacks |
1672 | * @return array[] |
1673 | * @phan-return array<int,array{name:string,callback:array{0:object,1:string}}> |
1674 | */ |
1675 | protected function getInstallSteps( DatabaseInstaller $installer ) { |
1676 | $coreInstallSteps = [ |
1677 | [ 'name' => 'database', 'callback' => [ $installer, 'setupDatabase' ] ], |
1678 | [ 'name' => 'tables', 'callback' => [ $installer, 'createTables' ] ], |
1679 | [ 'name' => 'tables-manual', 'callback' => [ $installer, 'createManualTables' ] ], |
1680 | [ 'name' => 'interwiki', 'callback' => [ $installer, 'populateInterwikiTable' ] ], |
1681 | [ 'name' => 'stats', 'callback' => [ $this, 'populateSiteStats' ] ], |
1682 | [ 'name' => 'keys', 'callback' => [ $this, 'generateKeys' ] ], |
1683 | [ 'name' => 'updates', 'callback' => [ $installer, 'insertUpdateKeys' ] ], |
1684 | [ 'name' => 'restore-services', 'callback' => [ $this, 'restoreServices' ] ], |
1685 | [ 'name' => 'sysop', 'callback' => [ $this, 'createSysop' ] ], |
1686 | [ 'name' => 'mainpage', 'callback' => [ $this, 'createMainpage' ] ], |
1687 | ]; |
1688 | |
1689 | // Build the array of install steps starting from the core install list, |
1690 | // then adding any callbacks that wanted to attach after a given step |
1691 | foreach ( $coreInstallSteps as $step ) { |
1692 | $this->installSteps[] = $step; |
1693 | if ( isset( $this->extraInstallSteps[$step['name']] ) ) { |
1694 | $this->installSteps = array_merge( |
1695 | $this->installSteps, |
1696 | $this->extraInstallSteps[$step['name']] |
1697 | ); |
1698 | } |
1699 | } |
1700 | |
1701 | // Prepend any steps that want to be at the beginning |
1702 | if ( isset( $this->extraInstallSteps['BEGINNING'] ) ) { |
1703 | $this->installSteps = array_merge( |
1704 | $this->extraInstallSteps['BEGINNING'], |
1705 | $this->installSteps |
1706 | ); |
1707 | } |
1708 | |
1709 | // Extensions should always go first, chance to tie into hooks and such |
1710 | if ( count( $this->getVar( '_Extensions' ) ) ) { |
1711 | array_unshift( $this->installSteps, |
1712 | [ 'name' => 'extensions', 'callback' => [ $this, 'includeExtensions' ] ] |
1713 | ); |
1714 | $this->installSteps[] = [ |
1715 | 'name' => 'extension-tables', |
1716 | 'callback' => [ $installer, 'createExtensionTables' ] |
1717 | ]; |
1718 | } |
1719 | |
1720 | return $this->installSteps; |
1721 | } |
1722 | |
1723 | /** |
1724 | * Actually perform the installation. |
1725 | * |
1726 | * @param callable $startCB A callback array for the beginning of each step |
1727 | * @param callable $endCB A callback array for the end of each step |
1728 | * |
1729 | * @return Status[] |
1730 | */ |
1731 | public function performInstallation( $startCB, $endCB ) { |
1732 | $installResults = []; |
1733 | $installer = $this->getDBInstaller(); |
1734 | $installer->preInstall(); |
1735 | $steps = $this->getInstallSteps( $installer ); |
1736 | foreach ( $steps as $stepObj ) { |
1737 | $name = $stepObj['name']; |
1738 | call_user_func_array( $startCB, [ $name ] ); |
1739 | |
1740 | // Perform the callback step |
1741 | $status = call_user_func( $stepObj['callback'], $installer ); |
1742 | |
1743 | // Output and save the results |
1744 | call_user_func( $endCB, $name, $status ); |
1745 | $installResults[$name] = $status; |
1746 | |
1747 | // If we've hit some sort of fatal, we need to bail. |
1748 | // Callback already had a chance to do output above. |
1749 | if ( !$status->isOK() ) { |
1750 | break; |
1751 | } |
1752 | } |
1753 | // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable |
1754 | // $steps has at least one element and that defines $status |
1755 | if ( $status->isOK() ) { |
1756 | $this->showMessage( |
1757 | 'config-install-db-success' |
1758 | ); |
1759 | $this->setVar( '_InstallDone', true ); |
1760 | } |
1761 | |
1762 | return $installResults; |
1763 | } |
1764 | |
1765 | /** |
1766 | * Generate $wgSecretKey. Will warn if we had to use an insecure random source. |
1767 | * |
1768 | * @return Status |
1769 | */ |
1770 | public function generateKeys() { |
1771 | $keys = [ 'wgSecretKey' => 64 ]; |
1772 | if ( strval( $this->getVar( 'wgUpgradeKey' ) ) === '' ) { |
1773 | $keys['wgUpgradeKey'] = 16; |
1774 | } |
1775 | |
1776 | return $this->doGenerateKeys( $keys ); |
1777 | } |
1778 | |
1779 | /** |
1780 | * Restore services that have been redefined in the early stage of installation |
1781 | * @return Status |
1782 | */ |
1783 | public function restoreServices() { |
1784 | $this->resetMediaWikiServices( null, [ |
1785 | 'UserOptionsLookup' => static function ( MediaWikiServices $services ) { |
1786 | return $services->get( 'UserOptionsManager' ); |
1787 | } |
1788 | ] ); |
1789 | return Status::newGood(); |
1790 | } |
1791 | |
1792 | /** |
1793 | * Generate a secret value for variables using a secure generator. |
1794 | * |
1795 | * @param array $keys |
1796 | * @return Status |
1797 | */ |
1798 | protected function doGenerateKeys( $keys ) { |
1799 | foreach ( $keys as $name => $length ) { |
1800 | $secretKey = MWCryptRand::generateHex( $length ); |
1801 | $this->setVar( $name, $secretKey ); |
1802 | } |
1803 | return Status::newGood(); |
1804 | } |
1805 | |
1806 | /** |
1807 | * Create the first user account, grant it sysop, bureaucrat and interface-admin rights |
1808 | * |
1809 | * @return Status |
1810 | */ |
1811 | protected function createSysop() { |
1812 | $name = $this->getVar( '_AdminName' ); |
1813 | $user = User::newFromName( $name ); |
1814 | |
1815 | if ( !$user ) { |
1816 | // We should've validated this earlier anyway! |
1817 | return Status::newFatal( 'config-admin-error-user', $name ); |
1818 | } |
1819 | |
1820 | if ( $user->idForName() == 0 ) { |
1821 | $user->addToDatabase(); |
1822 | |
1823 | $password = $this->getVar( '_AdminPassword' ); |
1824 | $status = $user->changeAuthenticationData( [ |
1825 | 'username' => $user->getName(), |
1826 | 'password' => $password, |
1827 | 'retype' => $password, |
1828 | ] ); |
1829 | if ( !$status->isGood() ) { |
1830 | return Status::newFatal( 'config-admin-error-password', |
1831 | $name, $status->getWikiText( false, false, $this->getVar( '_UserLang' ) ) ); |
1832 | } |
1833 | |
1834 | $userGroupManager = MediaWikiServices::getInstance()->getUserGroupManager(); |
1835 | $userGroupManager->addUserToGroup( $user, 'sysop' ); |
1836 | $userGroupManager->addUserToGroup( $user, 'bureaucrat' ); |
1837 | $userGroupManager->addUserToGroup( $user, 'interface-admin' ); |
1838 | if ( $this->getVar( '_AdminEmail' ) ) { |
1839 | $user->setEmail( $this->getVar( '_AdminEmail' ) ); |
1840 | } |
1841 | $user->saveSettings(); |
1842 | |
1843 | // Update user count |
1844 | $ssUpdate = SiteStatsUpdate::factory( [ 'users' => 1 ] ); |
1845 | $ssUpdate->doUpdate(); |
1846 | } |
1847 | |
1848 | if ( $this->getVar( '_Subscribe' ) && $this->getVar( '_AdminEmail' ) ) { |
1849 | return $this->subscribeToMediaWikiAnnounce(); |
1850 | } |
1851 | return Status::newGood(); |
1852 | } |
1853 | |
1854 | /** |
1855 | * @return Status |
1856 | */ |
1857 | private function subscribeToMediaWikiAnnounce() { |
1858 | $status = Status::newGood(); |
1859 | $http = MediaWikiServices::getInstance()->getHttpRequestFactory(); |
1860 | if ( !$http->canMakeRequests() ) { |
1861 | $status->warning( 'config-install-subscribe-fail', |
1862 | wfMessage( 'config-install-subscribe-notpossible' ) ); |
1863 | return $status; |
1864 | } |
1865 | |
1866 | // Create subscription request |
1867 | $params = [ 'email' => $this->getVar( '_AdminEmail' ) ]; |
1868 | $req = $http->create( self::MEDIAWIKI_ANNOUNCE_URL . 'anonymous_subscribe', |
1869 | [ 'method' => 'POST', 'postData' => $params ], __METHOD__ ); |
1870 | |
1871 | // Add headers needed to pass Django's CSRF checks |
1872 | $token = str_repeat( 'a', 64 ); |
1873 | $req->setHeader( 'Referer', self::MEDIAWIKI_ANNOUNCE_URL ); |
1874 | $req->setHeader( 'Cookie', "csrftoken=$token" ); |
1875 | $req->setHeader( 'X-CSRFToken', $token ); |
1876 | |
1877 | // Send subscription request |
1878 | $reqStatus = $req->execute(); |
1879 | if ( !$reqStatus->isOK() ) { |
1880 | $status->warning( 'config-install-subscribe-fail', |
1881 | Status::wrap( $reqStatus )->getMessage() ); |
1882 | return $status; |
1883 | } |
1884 | |
1885 | // Was the request submitted successfully? |
1886 | // The status message is displayed after a redirect, using Django's messages |
1887 | // framework, so load the list summary page and look for the expected text. |
1888 | // (Though parsing the cookie set by the framework may be possible, it isn't |
1889 | // simple, since the format of the cookie has changed between versions.) |
1890 | $checkReq = $http->create( self::MEDIAWIKI_ANNOUNCE_URL, [], __METHOD__ ); |
1891 | $checkReq->setCookieJar( $req->getCookieJar() ); |
1892 | if ( !$checkReq->execute()->isOK() ) { |
1893 | $status->warning( 'config-install-subscribe-possiblefail' ); |
1894 | return $status; |
1895 | } |
1896 | $html = $checkReq->getContent(); |
1897 | if ( strpos( $html, 'Please check your inbox for further instructions' ) !== false ) { |
1898 | // Success |
1899 | } elseif ( strpos( $html, 'Member already subscribed' ) !== false ) { |
1900 | $status->warning( 'config-install-subscribe-alreadysubscribed' ); |
1901 | } elseif ( strpos( $html, 'Subscription request already pending' ) !== false ) { |
1902 | $status->warning( 'config-install-subscribe-alreadypending' ); |
1903 | } else { |
1904 | $status->warning( 'config-install-subscribe-possiblefail' ); |
1905 | } |
1906 | return $status; |
1907 | } |
1908 | |
1909 | /** |
1910 | * Insert Main Page with default content. |
1911 | * |
1912 | * @param DatabaseInstaller $installer |
1913 | * @return Status |
1914 | */ |
1915 | protected function createMainpage( DatabaseInstaller $installer ) { |
1916 | $status = Status::newGood(); |
1917 | $title = Title::newMainPage(); |
1918 | if ( $title->exists() ) { |
1919 | $status->warning( 'config-install-mainpage-exists' ); |
1920 | return $status; |
1921 | } |
1922 | try { |
1923 | $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
1924 | $content = new WikitextContent( |
1925 | wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" . |
1926 | wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text() |
1927 | ); |
1928 | |
1929 | $status = $page->doUserEditContent( |
1930 | $content, |
1931 | User::newSystemUser( 'MediaWiki default' ), |
1932 | '', |
1933 | EDIT_NEW |
1934 | ); |
1935 | } catch ( Exception $e ) { |
1936 | // using raw, because $wgShowExceptionDetails can not be set yet |
1937 | $status->fatal( 'config-install-mainpage-failed', $e->getMessage() ); |
1938 | } |
1939 | |
1940 | return $status; |
1941 | } |
1942 | |
1943 | /** |
1944 | * Override the necessary bits of the config to run an installation. |
1945 | * |
1946 | * @param SettingsBuilder $settings |
1947 | */ |
1948 | public static function overrideConfig( SettingsBuilder $settings ) { |
1949 | // Use PHP's built-in session handling, since MediaWiki's |
1950 | // SessionHandler can't work before we have an object cache set up. |
1951 | if ( !defined( 'MW_NO_SESSION_HANDLER' ) ) { |
1952 | define( 'MW_NO_SESSION_HANDLER', 1 ); |
1953 | } |
1954 | |
1955 | $settings->overrideConfigValues( [ |
1956 | |
1957 | // Don't access the database |
1958 | MainConfigNames::UseDatabaseMessages => false, |
1959 | |
1960 | // Don't cache langconv tables |
1961 | MainConfigNames::LanguageConverterCacheType => CACHE_NONE, |
1962 | |
1963 | // Don't try to cache ResourceLoader dependencies in the database |
1964 | MainConfigNames::ResourceLoaderUseObjectCacheForDeps => true, |
1965 | |
1966 | // Debug-friendly |
1967 | MainConfigNames::ShowExceptionDetails => true, |
1968 | MainConfigNames::ShowHostnames => true, |
1969 | |
1970 | // Don't break forms |
1971 | MainConfigNames::ExternalLinkTarget => '_blank', |
1972 | |
1973 | // Allow multiple ob_flush() calls |
1974 | MainConfigNames::DisableOutputCompression => true, |
1975 | |
1976 | // Use a sensible cookie prefix (not my_wiki) |
1977 | MainConfigNames::CookiePrefix => 'mw_installer', |
1978 | |
1979 | // Some of the environment checks make shell requests, remove limits |
1980 | MainConfigNames::MaxShellMemory => 0, |
1981 | |
1982 | // Override the default CookieSessionProvider with a dummy |
1983 | // implementation that won't stomp on PHP's cookies. |
1984 | MainConfigNames::SessionProviders => [ |
1985 | [ |
1986 | 'class' => InstallerSessionProvider::class, |
1987 | 'args' => [ [ |
1988 | 'priority' => 1, |
1989 | ] ] |
1990 | ] |
1991 | ], |
1992 | |
1993 | // Don't use the DB as the main stash |
1994 | MainConfigNames::MainStash => CACHE_NONE, |
1995 | |
1996 | // Don't try to use any object cache for SessionManager either. |
1997 | MainConfigNames::SessionCacheType => CACHE_NONE, |
1998 | |
1999 | // Set a dummy $wgServer to bypass the check in Setup.php, the |
2000 | // web installer will automatically detect it and not use this value. |
2001 | MainConfigNames::Server => 'https://🌻.invalid', |
2002 | ] ); |
2003 | } |
2004 | |
2005 | /** |
2006 | * Add an installation step following the given step. |
2007 | * |
2008 | * @param array $callback A valid installation callback array, in this form: |
2009 | * [ 'name' => 'some-unique-name', 'callback' => [ $obj, 'function' ] ]; |
2010 | * @param string $findStep The step to find. Omit to put the step at the beginning |
2011 | */ |
2012 | public function addInstallStep( $callback, $findStep = 'BEGINNING' ) { |
2013 | $this->extraInstallSteps[$findStep][] = $callback; |
2014 | } |
2015 | |
2016 | /** |
2017 | * Disable the time limit for execution. |
2018 | * Some long-running pages (Install, Upgrade) will want to do this |
2019 | */ |
2020 | protected function disableTimeLimit() { |
2021 | AtEase::suppressWarnings(); |
2022 | set_time_limit( 0 ); |
2023 | AtEase::restoreWarnings(); |
2024 | } |
2025 | } |