Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
66.06% |
146 / 221 |
|
48.72% |
19 / 39 |
CRAP | |
0.00% |
0 / 1 |
RequestContext | |
66.36% |
146 / 220 |
|
48.72% |
19 / 39 |
359.96 | |
0.00% |
0 / 1 |
setConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getConfig | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRequest | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getTiming | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
setTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTitle | |
25.00% |
2 / 8 |
|
0.00% |
0 / 1 |
3.69 | |||
hasTitle | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canUseWikiPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
setWikiPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getWikiPage | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
setActionName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getActionName | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
clearActionName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setOutput | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOutput | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setUser | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getUser | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
hasUser | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
setAuthority | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getAuthority | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
sanitizeLangCode | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
setLanguage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getLanguage | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
getLanguageCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setSkin | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getSkinName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
fetchSkinName | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
4.10 | |||
getSkinFromHook | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getSkin | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
4 | |||
msg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMain | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getMainAndWarn | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
resetMain | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
3.33 | |||
exportSession | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getCsrfTokenSet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
importScopedSession | |
93.75% |
45 / 48 |
|
0.00% |
0 / 1 |
12.04 | |||
newExtraneousContext | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
__clone | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @since 1.18 |
19 | * |
20 | * @author Alexandre Emsenhuber |
21 | * @author Daniel Friesen |
22 | * @file |
23 | */ |
24 | |
25 | namespace MediaWiki\Context; |
26 | |
27 | use BadMethodCallException; |
28 | use InvalidArgumentException; |
29 | use LogicException; |
30 | use MediaWiki\Config\Config; |
31 | use MediaWiki\HookContainer\HookRunner; |
32 | use MediaWiki\Language\Language; |
33 | use MediaWiki\Logger\LoggerFactory; |
34 | use MediaWiki\MainConfigNames; |
35 | use MediaWiki\MediaWikiServices; |
36 | use MediaWiki\Message\Message; |
37 | use MediaWiki\Output\OutputPage; |
38 | use MediaWiki\Permissions\Authority; |
39 | use MediaWiki\Request\FauxRequest; |
40 | use MediaWiki\Request\WebRequest; |
41 | use MediaWiki\Session\CsrfTokenSet; |
42 | use MediaWiki\Session\PHPSessionHandler; |
43 | use MediaWiki\Session\SessionManager; |
44 | use MediaWiki\StubObject\StubGlobalUser; |
45 | use MediaWiki\Title\Title; |
46 | use MediaWiki\User\User; |
47 | use MediaWiki\User\UserRigorOptions; |
48 | use RuntimeException; |
49 | use Skin; |
50 | use Timing; |
51 | use Wikimedia\Assert\Assert; |
52 | use Wikimedia\AtEase\AtEase; |
53 | use Wikimedia\Bcp47Code\Bcp47Code; |
54 | use Wikimedia\IPUtils; |
55 | use Wikimedia\Message\MessageParam; |
56 | use Wikimedia\Message\MessageSpecifier; |
57 | use Wikimedia\NonSerializable\NonSerializableTrait; |
58 | use Wikimedia\ScopedCallback; |
59 | use WikiPage; |
60 | |
61 | /** |
62 | * Group all the pieces relevant to the context of a request into one instance |
63 | * @newable |
64 | * @note marked as newable in 1.35 for lack of a better alternative, |
65 | * but should use a factory in the future and should be narrowed |
66 | * down to not expose heavy weight objects. |
67 | */ |
68 | class RequestContext implements IContextSource, MutableContext { |
69 | use NonSerializableTrait; |
70 | |
71 | /** |
72 | * @var WebRequest |
73 | */ |
74 | private $request; |
75 | |
76 | /** |
77 | * @var Title |
78 | */ |
79 | private $title; |
80 | |
81 | /** |
82 | * @var WikiPage|null |
83 | */ |
84 | private $wikipage; |
85 | |
86 | /** |
87 | * @var null|string |
88 | */ |
89 | private $action; |
90 | |
91 | /** |
92 | * @var OutputPage |
93 | */ |
94 | private $output; |
95 | |
96 | /** |
97 | * @var User|null |
98 | */ |
99 | private $user; |
100 | |
101 | /** |
102 | * @var Authority |
103 | */ |
104 | private $authority; |
105 | |
106 | /** |
107 | * @var Language|null |
108 | */ |
109 | private $lang; |
110 | |
111 | /** |
112 | * @var Skin|null |
113 | */ |
114 | private $skin; |
115 | |
116 | /** |
117 | * @var Timing |
118 | */ |
119 | private $timing; |
120 | |
121 | /** |
122 | * @var Config |
123 | */ |
124 | private $config; |
125 | |
126 | /** |
127 | * @var RequestContext|null |
128 | */ |
129 | private static $instance = null; |
130 | |
131 | /** |
132 | * Boolean flag to guard against recursion in getLanguage |
133 | * @var bool |
134 | */ |
135 | private $languageRecursion = false; |
136 | |
137 | /** @var Skin|string|null */ |
138 | private $skinFromHook; |
139 | |
140 | /** @var bool */ |
141 | private $skinHookCalled = false; |
142 | |
143 | /** @var string|null */ |
144 | private $skinName; |
145 | |
146 | /** |
147 | * @param Config $config |
148 | */ |
149 | public function setConfig( Config $config ) { |
150 | $this->config = $config; |
151 | } |
152 | |
153 | /** |
154 | * @return Config |
155 | */ |
156 | public function getConfig() { |
157 | // @todo In the future, we could move this to WebStart.php so |
158 | // the Config object is ready for when initialization happens |
159 | $this->config ??= MediaWikiServices::getInstance()->getMainConfig(); |
160 | |
161 | return $this->config; |
162 | } |
163 | |
164 | /** |
165 | * @param WebRequest $request |
166 | */ |
167 | public function setRequest( WebRequest $request ) { |
168 | $this->request = $request; |
169 | } |
170 | |
171 | /** |
172 | * @return WebRequest |
173 | */ |
174 | public function getRequest() { |
175 | if ( $this->request === null ) { |
176 | // create the WebRequest object on the fly |
177 | if ( MW_ENTRY_POINT === 'cli' ) { |
178 | // Don't use real WebRequest in CLI mode, it throws errors when trying to access |
179 | // things that don't exist, e.g. "Unable to determine IP". |
180 | $this->request = new FauxRequest( [] ); |
181 | } else { |
182 | $this->request = new WebRequest(); |
183 | } |
184 | } |
185 | |
186 | return $this->request; |
187 | } |
188 | |
189 | /** |
190 | * @return Timing |
191 | */ |
192 | public function getTiming() { |
193 | $this->timing ??= new Timing( [ |
194 | 'logger' => LoggerFactory::getInstance( 'Timing' ) |
195 | ] ); |
196 | return $this->timing; |
197 | } |
198 | |
199 | /** |
200 | * @param Title|null $title |
201 | */ |
202 | public function setTitle( ?Title $title = null ) { |
203 | $this->title = $title; |
204 | // Clear cache of derived getters |
205 | $this->wikipage = null; |
206 | $this->clearActionName(); |
207 | } |
208 | |
209 | /** |
210 | * @return Title|null |
211 | */ |
212 | public function getTitle() { |
213 | if ( $this->title === null ) { |
214 | // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle |
215 | global $wgTitle; # fallback to $wg till we can improve this |
216 | $this->title = $wgTitle; |
217 | $logger = LoggerFactory::getInstance( 'GlobalTitleFail' ); |
218 | $logger->info( |
219 | __METHOD__ . ' called with no title set.', |
220 | [ 'exception' => new RuntimeException ] |
221 | ); |
222 | } |
223 | |
224 | return $this->title; |
225 | } |
226 | |
227 | /** |
228 | * Check, if a Title object is set |
229 | * |
230 | * @since 1.25 |
231 | * @return bool |
232 | */ |
233 | public function hasTitle() { |
234 | return $this->title !== null; |
235 | } |
236 | |
237 | /** |
238 | * Check whether a WikiPage object can be get with getWikiPage(). |
239 | * Callers should expect that an exception is thrown from getWikiPage() |
240 | * if this method returns false. |
241 | * |
242 | * @since 1.19 |
243 | * @return bool |
244 | */ |
245 | public function canUseWikiPage() { |
246 | if ( $this->wikipage ) { |
247 | // If there's a WikiPage object set, we can for sure get it |
248 | return true; |
249 | } |
250 | // Only pages with legitimate titles can have WikiPages. |
251 | // That usually means pages in non-virtual namespaces. |
252 | $title = $this->getTitle(); |
253 | return $title && $title->canExist(); |
254 | } |
255 | |
256 | /** |
257 | * @since 1.19 |
258 | * @param WikiPage $wikiPage |
259 | */ |
260 | public function setWikiPage( WikiPage $wikiPage ) { |
261 | $pageTitle = $wikiPage->getTitle(); |
262 | if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) { |
263 | $this->setTitle( $pageTitle ); |
264 | } |
265 | // Defer this to the end since setTitle sets it to null. |
266 | $this->wikipage = $wikiPage; |
267 | // Clear cache of derived getter |
268 | $this->clearActionName(); |
269 | } |
270 | |
271 | /** |
272 | * Get the WikiPage object. |
273 | * May throw an exception if there's no Title object set or the Title object |
274 | * belongs to a special namespace that doesn't have WikiPage, so use first |
275 | * canUseWikiPage() to check whether this method can be called safely. |
276 | * |
277 | * @since 1.19 |
278 | * @return WikiPage |
279 | */ |
280 | public function getWikiPage() { |
281 | if ( $this->wikipage === null ) { |
282 | $title = $this->getTitle(); |
283 | if ( $title === null ) { |
284 | throw new BadMethodCallException( __METHOD__ . ' called without Title object set' ); |
285 | } |
286 | $this->wikipage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $title ); |
287 | } |
288 | |
289 | return $this->wikipage; |
290 | } |
291 | |
292 | /** |
293 | * @since 1.38 |
294 | * @param string $action |
295 | */ |
296 | public function setActionName( string $action ): void { |
297 | $this->action = $action; |
298 | } |
299 | |
300 | /** |
301 | * Get the action name for the current web request. |
302 | * |
303 | * This generally returns "view" if the current request or process is |
304 | * not for a skinned index.php web request (e.g. load.php, thumb.php, |
305 | * job runner, CLI, API). |
306 | * |
307 | * @warning This must not be called before or during the Setup.php phase, |
308 | * and may cause an error or warning if called too early. |
309 | * |
310 | * @since 1.38 |
311 | * @return string Action |
312 | */ |
313 | public function getActionName(): string { |
314 | // Optimisation: This is cached to avoid repeated running of the |
315 | // expensive operations to compute this. The computation involves creation |
316 | // of Article, WikiPage, and ContentHandler objects (and the various |
317 | // database queries these classes require to be instantiated), as well |
318 | // as potentially slow extension hooks in these classes. |
319 | // |
320 | // This value is frequently needed in OutputPage and in various |
321 | // Skin-related methods and classes. |
322 | $this->action ??= MediaWikiServices::getInstance() |
323 | ->getActionFactory() |
324 | ->getActionName( $this ); |
325 | |
326 | return $this->action; |
327 | } |
328 | |
329 | private function clearActionName(): void { |
330 | if ( $this->action !== null ) { |
331 | // If we're clearing after something else has actually already computed the action, |
332 | // emit a warning. |
333 | // |
334 | // Doing so is unstable, given the first caller got something that turns out to be |
335 | // incomplete or incorrect. Even if we end up re-creating an instance of the same |
336 | // class, we may now be acting on a different title/skin/user etc. |
337 | // |
338 | // Re-computing the action is expensive and can be a performance problem (T302623). |
339 | trigger_error( 'Unexpected clearActionName after getActionName already called' ); |
340 | $this->action = null; |
341 | } |
342 | } |
343 | |
344 | /** |
345 | * @param OutputPage $output |
346 | */ |
347 | public function setOutput( OutputPage $output ) { |
348 | $this->output = $output; |
349 | } |
350 | |
351 | /** |
352 | * @return OutputPage |
353 | */ |
354 | public function getOutput() { |
355 | $this->output ??= new OutputPage( $this ); |
356 | |
357 | return $this->output; |
358 | } |
359 | |
360 | /** |
361 | * @param User $user |
362 | */ |
363 | public function setUser( User $user ) { |
364 | $this->user = $user; |
365 | // Keep authority consistent |
366 | $this->authority = $user; |
367 | // Invalidate cached user interface language and skin |
368 | $this->lang = null; |
369 | $this->skin = null; |
370 | $this->skinName = null; |
371 | } |
372 | |
373 | /** |
374 | * @return User |
375 | */ |
376 | public function getUser() { |
377 | if ( $this->user === null ) { |
378 | if ( $this->authority !== null ) { |
379 | // Keep user consistent by using a possible set authority |
380 | $this->user = MediaWikiServices::getInstance() |
381 | ->getUserFactory() |
382 | ->newFromAuthority( $this->authority ); |
383 | } else { |
384 | $this->user = User::newFromSession( $this->getRequest() ); |
385 | } |
386 | } |
387 | |
388 | return $this->user; |
389 | } |
390 | |
391 | public function hasUser(): bool { |
392 | if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) { |
393 | throw new LogicException( __METHOD__ . '() should be called only from tests!' ); |
394 | } |
395 | return $this->user !== null; |
396 | } |
397 | |
398 | /** |
399 | * @param Authority $authority |
400 | */ |
401 | public function setAuthority( Authority $authority ) { |
402 | $this->authority = $authority; |
403 | // If needed, a User object is constructed from this authority |
404 | $this->user = null; |
405 | // Invalidate cached user interface language and skin |
406 | $this->lang = null; |
407 | $this->skin = null; |
408 | $this->skinName = null; |
409 | } |
410 | |
411 | /** |
412 | * @since 1.36 |
413 | * @return Authority |
414 | */ |
415 | public function getAuthority(): Authority { |
416 | return $this->authority ?: $this->getUser(); |
417 | } |
418 | |
419 | /** |
420 | * Accepts a language code and ensures it's sensible. Outputs a cleaned up language |
421 | * code and replaces with $wgLanguageCode if not sensible. |
422 | * @param ?string $code Language code |
423 | * @return string |
424 | */ |
425 | public static function sanitizeLangCode( $code ) { |
426 | global $wgLanguageCode; |
427 | |
428 | if ( !$code ) { |
429 | return $wgLanguageCode; |
430 | } |
431 | |
432 | // BCP 47 - letter case MUST NOT carry meaning |
433 | $code = strtolower( $code ); |
434 | |
435 | # Validate $code |
436 | if ( !MediaWikiServices::getInstance()->getLanguageNameUtils() |
437 | ->isValidCode( $code ) |
438 | || $code === 'qqq' |
439 | ) { |
440 | $code = $wgLanguageCode; |
441 | } |
442 | |
443 | return $code; |
444 | } |
445 | |
446 | /** |
447 | * @param Language|string $language Language instance or language code |
448 | * @since 1.19 |
449 | */ |
450 | public function setLanguage( $language ) { |
451 | Assert::parameterType( [ Language::class, 'string' ], $language, '$language' ); |
452 | if ( $language instanceof Language ) { |
453 | $this->lang = $language; |
454 | } else { |
455 | $language = self::sanitizeLangCode( $language ); |
456 | $obj = MediaWikiServices::getInstance()->getLanguageFactory()->getLanguage( $language ); |
457 | $this->lang = $obj; |
458 | } |
459 | OutputPage::resetOOUI(); |
460 | } |
461 | |
462 | /** |
463 | * Get the Language object. |
464 | * Initialization of user or request objects can depend on this. |
465 | * @return Language |
466 | * @throws LogicException |
467 | * @since 1.19 |
468 | */ |
469 | public function getLanguage() { |
470 | if ( $this->languageRecursion === true ) { |
471 | throw new LogicException( 'Recursion detected' ); |
472 | } |
473 | |
474 | if ( $this->lang === null ) { |
475 | $this->languageRecursion = true; |
476 | |
477 | try { |
478 | $request = $this->getRequest(); |
479 | $user = $this->getUser(); |
480 | $services = MediaWikiServices::getInstance(); |
481 | |
482 | // Optimisation: Avoid slow getVal(), this isn't user-generated content. |
483 | $code = $request->getRawVal( 'uselang' ) ?? 'user'; |
484 | if ( $code === 'user' ) { |
485 | $userOptionsLookup = $services->getUserOptionsLookup(); |
486 | $code = $userOptionsLookup->getOption( $user, 'language' ); |
487 | } |
488 | |
489 | // There are certain characters we don't allow in language code strings, |
490 | // but by and large almost any valid UTF-8 string will makes it past |
491 | // this check and the LanguageNameUtils::isValidCode method it uses. |
492 | // This is to support on-wiki interface message overrides for |
493 | // non-existent language codes. Also known as "Uselang hacks". |
494 | // See <https://www.mediawiki.org/wiki/Manual:Uselang_hack> |
495 | // For something like "en-whatever" or "de-whatever" it will end up |
496 | // with a mostly "en" or "de" interface, but with an extra layer of |
497 | // possible MessageCache overrides from `MediaWiki:*/<code>` titles. |
498 | // While non-ASCII works here, it is required that they are in |
499 | // NFC form given this will not convert to normalised form. |
500 | $code = self::sanitizeLangCode( $code ); |
501 | |
502 | ( new HookRunner( $services->getHookContainer() ) )->onUserGetLanguageObject( $user, $code, $this ); |
503 | |
504 | if ( $code === $this->getConfig()->get( MainConfigNames::LanguageCode ) ) { |
505 | $this->lang = $services->getContentLanguage(); |
506 | } else { |
507 | $obj = $services->getLanguageFactory() |
508 | ->getLanguage( $code ); |
509 | $this->lang = $obj; |
510 | } |
511 | } finally { |
512 | $this->languageRecursion = false; |
513 | } |
514 | } |
515 | |
516 | return $this->lang; |
517 | } |
518 | |
519 | /** |
520 | * @since 1.42 |
521 | * @return Bcp47Code |
522 | */ |
523 | public function getLanguageCode() { |
524 | return $this->getLanguage(); |
525 | } |
526 | |
527 | /** |
528 | * @param Skin $skin |
529 | */ |
530 | public function setSkin( Skin $skin ) { |
531 | $this->skin = clone $skin; |
532 | $this->skin->setContext( $this ); |
533 | $this->skinName = $skin->getSkinName(); |
534 | OutputPage::resetOOUI(); |
535 | } |
536 | |
537 | /** |
538 | * Get the name of the skin |
539 | * |
540 | * @since 1.41 |
541 | * @return string |
542 | */ |
543 | public function getSkinName() { |
544 | if ( $this->skinName === null ) { |
545 | $this->skinName = $this->fetchSkinName(); |
546 | } |
547 | return $this->skinName; |
548 | } |
549 | |
550 | /** |
551 | * Get the name of the skin, without caching |
552 | * |
553 | * @return string |
554 | */ |
555 | private function fetchSkinName() { |
556 | $skinFromHook = $this->getSkinFromHook(); |
557 | if ( $skinFromHook instanceof Skin ) { |
558 | // The hook provided a skin object |
559 | return $skinFromHook->getSkinName(); |
560 | } elseif ( is_string( $skinFromHook ) ) { |
561 | // The hook provided a skin name |
562 | $skinName = $skinFromHook; |
563 | } elseif ( !in_array( 'skin', $this->getConfig()->get( MainConfigNames::HiddenPrefs ) ) ) { |
564 | // The normal case |
565 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
566 | $userSkin = $userOptionsLookup->getOption( $this->getUser(), 'skin' ); |
567 | // Optimisation: Avoid slow getVal(), this isn't user-generated content. |
568 | $skinName = $this->getRequest()->getRawVal( 'useskin' ) ?? $userSkin; |
569 | } else { |
570 | // User preference disabled |
571 | $skinName = $this->getConfig()->get( MainConfigNames::DefaultSkin ); |
572 | } |
573 | return Skin::normalizeKey( $skinName ); |
574 | } |
575 | |
576 | /** |
577 | * Get the skin set by the RequestContextCreateSkin hook, if there is any. |
578 | * |
579 | * @return Skin|string|null |
580 | */ |
581 | private function getSkinFromHook() { |
582 | if ( !$this->skinHookCalled ) { |
583 | $this->skinHookCalled = true; |
584 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
585 | ->onRequestContextCreateSkin( $this, $this->skinFromHook ); |
586 | } |
587 | return $this->skinFromHook; |
588 | } |
589 | |
590 | /** |
591 | * @return Skin |
592 | */ |
593 | public function getSkin() { |
594 | if ( $this->skin === null ) { |
595 | $skinFromHook = $this->getSkinFromHook(); |
596 | if ( $skinFromHook instanceof Skin ) { |
597 | $this->skin = $skinFromHook; |
598 | } else { |
599 | $skinName = is_string( $skinFromHook ) |
600 | ? Skin::normalizeKey( $skinFromHook ) |
601 | : $this->getSkinName(); |
602 | $factory = MediaWikiServices::getInstance()->getSkinFactory(); |
603 | $this->skin = $factory->makeSkin( $skinName ); |
604 | } |
605 | $this->skin->setContext( $this ); |
606 | } |
607 | return $this->skin; |
608 | } |
609 | |
610 | /** |
611 | * Get a Message object with context set |
612 | * Parameters are the same as wfMessage() |
613 | * |
614 | * @param string|string[]|MessageSpecifier $key Message key, or array of keys, |
615 | * or a MessageSpecifier. |
616 | * @phpcs:ignore Generic.Files.LineLength |
617 | * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params |
618 | * See Message::params() |
619 | * @return Message |
620 | */ |
621 | public function msg( $key, ...$params ) { |
622 | return wfMessage( $key, ...$params )->setContext( $this ); |
623 | } |
624 | |
625 | /** |
626 | * Get the RequestContext object associated with the main request |
627 | * |
628 | * @return RequestContext |
629 | */ |
630 | public static function getMain(): RequestContext { |
631 | self::$instance ??= new self; |
632 | |
633 | return self::$instance; |
634 | } |
635 | |
636 | /** |
637 | * Get the RequestContext object associated with the main request |
638 | * and gives a warning to the log, to find places, where a context maybe is missing. |
639 | * |
640 | * @param string $func @phan-mandatory-param |
641 | * @return RequestContext |
642 | * @since 1.24 |
643 | */ |
644 | public static function getMainAndWarn( $func = __METHOD__ ) { |
645 | wfDebug( $func . ' called without context. ' . |
646 | "Using RequestContext::getMain()" ); |
647 | |
648 | return self::getMain(); |
649 | } |
650 | |
651 | /** |
652 | * Resets singleton returned by getMain(). Should be called only from unit tests. |
653 | */ |
654 | public static function resetMain() { |
655 | if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MW_PARSER_TEST' ) ) { |
656 | throw new LogicException( __METHOD__ . '() should be called only from unit tests!' ); |
657 | } |
658 | self::$instance = null; |
659 | } |
660 | |
661 | /** |
662 | * Export the resolved user IP, HTTP headers, user ID, and session ID. |
663 | * The result will be reasonably sized to allow for serialization. |
664 | * |
665 | * @return array |
666 | * @since 1.21 |
667 | */ |
668 | public function exportSession() { |
669 | $session = SessionManager::getGlobalSession(); |
670 | return [ |
671 | 'ip' => $this->getRequest()->getIP(), |
672 | 'headers' => $this->getRequest()->getAllHeaders(), |
673 | 'sessionId' => $session->isPersistent() ? $session->getId() : '', |
674 | 'userId' => $this->getUser()->getId() |
675 | ]; |
676 | } |
677 | |
678 | public function getCsrfTokenSet(): CsrfTokenSet { |
679 | return new CsrfTokenSet( $this->getRequest() ); |
680 | } |
681 | |
682 | /** |
683 | * Import a client IP address, HTTP headers, user ID, and session ID |
684 | * |
685 | * This sets the current session, $wgUser, and $wgRequest from $params. |
686 | * Once the return value falls out of scope, the old context is restored. |
687 | * This method should only be called in contexts where there is no session |
688 | * ID or end user receiving the response (CLI or HTTP job runners). This |
689 | * is partly enforced, and is done so to avoid leaking cookies if certain |
690 | * error conditions arise. |
691 | * |
692 | * This is useful when background scripts inherit context when acting on |
693 | * behalf of a user. In general the 'sessionId' parameter should be set |
694 | * to an empty string unless session importing is *truly* needed. This |
695 | * feature is somewhat deprecated. |
696 | * |
697 | * @param array $params Result of RequestContext::exportSession() |
698 | * @return ScopedCallback |
699 | * @since 1.21 |
700 | */ |
701 | public static function importScopedSession( array $params ) { |
702 | if ( strlen( $params['sessionId'] ) && |
703 | SessionManager::getGlobalSession()->isPersistent() |
704 | ) { |
705 | // Check to avoid sending random cookies for the wrong users. |
706 | // This method should only called by CLI scripts or by HTTP job runners. |
707 | throw new BadMethodCallException( "Sessions can only be imported when none is active." ); |
708 | } elseif ( !IPUtils::isValid( $params['ip'] ) ) { |
709 | throw new InvalidArgumentException( "Invalid client IP address '{$params['ip']}'." ); |
710 | } |
711 | |
712 | $userFactory = MediaWikiServices::getInstance()->getUserFactory(); |
713 | |
714 | if ( $params['userId'] ) { // logged-in user |
715 | $user = $userFactory->newFromId( (int)$params['userId'] ); |
716 | $user->load(); |
717 | if ( !$user->isRegistered() ) { |
718 | throw new InvalidArgumentException( "No user with ID '{$params['userId']}'." ); |
719 | } |
720 | } else { // anon user |
721 | $user = $userFactory->newFromName( $params['ip'], UserRigorOptions::RIGOR_NONE ); |
722 | } |
723 | |
724 | $importSessionFunc = static function ( User $user, array $params ) { |
725 | global $wgRequest; |
726 | |
727 | $context = RequestContext::getMain(); |
728 | |
729 | // Commit and close any current session |
730 | if ( PHPSessionHandler::isEnabled() ) { |
731 | session_write_close(); // persist |
732 | session_id( '' ); // detach |
733 | $_SESSION = []; // clear in-memory array |
734 | } |
735 | |
736 | // Get new session, if applicable |
737 | $session = null; |
738 | if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID |
739 | $manager = SessionManager::singleton(); |
740 | $session = $manager->getSessionById( $params['sessionId'], true ) |
741 | ?: $manager->getEmptySession(); |
742 | } |
743 | |
744 | // Remove any user IP or agent information, and attach the request |
745 | // with the new session. |
746 | $context->setRequest( new FauxRequest( [], false, $session ) ); |
747 | $wgRequest = $context->getRequest(); // b/c |
748 | |
749 | // Now that all private information is detached from the user, it should |
750 | // be safe to load the new user. If errors occur or an exception is thrown |
751 | // and caught (leaving the main context in a mixed state), there is no risk |
752 | // of the User object being attached to the wrong IP, headers, or session. |
753 | $context->setUser( $user ); |
754 | StubGlobalUser::setUser( $context->getUser() ); // b/c |
755 | if ( $session && PHPSessionHandler::isEnabled() ) { |
756 | session_id( $session->getId() ); |
757 | AtEase::quietCall( 'session_start' ); |
758 | } |
759 | $request = new FauxRequest( [], false, $session ); |
760 | $request->setIP( $params['ip'] ); |
761 | foreach ( $params['headers'] as $name => $value ) { |
762 | $request->setHeader( $name, $value ); |
763 | } |
764 | // Set the current context to use the new WebRequest |
765 | $context->setRequest( $request ); |
766 | $wgRequest = $context->getRequest(); // b/c |
767 | }; |
768 | |
769 | // Stash the old session and load in the new one |
770 | $oUser = self::getMain()->getUser(); |
771 | $oParams = self::getMain()->exportSession(); |
772 | $oRequest = self::getMain()->getRequest(); |
773 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable exceptions triggered above prevent the null case |
774 | $importSessionFunc( $user, $params ); |
775 | |
776 | // Set callback to save and close the new session and reload the old one |
777 | return new ScopedCallback( |
778 | static function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) { |
779 | global $wgRequest; |
780 | $importSessionFunc( $oUser, $oParams ); |
781 | // Restore the exact previous Request object (instead of leaving MediaWiki\Request\FauxRequest) |
782 | RequestContext::getMain()->setRequest( $oRequest ); |
783 | $wgRequest = RequestContext::getMain()->getRequest(); // b/c |
784 | } |
785 | ); |
786 | } |
787 | |
788 | /** |
789 | * Create a new extraneous context. The context is filled with information |
790 | * external to the current session. |
791 | * - Title is specified by argument |
792 | * - Request is a MediaWiki\Request\FauxRequest, or a MediaWiki\Request\FauxRequest can be specified by argument |
793 | * - User is an anonymous user, for separation IPv4 localhost is used |
794 | * - Language will be based on the anonymous user and request, may be content |
795 | * language or a uselang param in the fauxrequest data may change the lang |
796 | * - Skin will be based on the anonymous user, should be the wiki's default skin |
797 | * |
798 | * @param Title $title Title to use for the extraneous request |
799 | * @param WebRequest|array $request A WebRequest or data to use for a MediaWiki\Request\FauxRequest |
800 | * @return RequestContext |
801 | */ |
802 | public static function newExtraneousContext( Title $title, $request = [] ) { |
803 | $context = new self; |
804 | $context->setTitle( $title ); |
805 | if ( $request instanceof WebRequest ) { |
806 | $context->setRequest( $request ); |
807 | } else { |
808 | $context->setRequest( new FauxRequest( $request ) ); |
809 | } |
810 | $context->user = MediaWikiServices::getInstance()->getUserFactory()->newFromName( |
811 | '127.0.0.1', |
812 | UserRigorOptions::RIGOR_NONE |
813 | ); |
814 | |
815 | return $context; |
816 | } |
817 | |
818 | /** @return never */ |
819 | public function __clone() { |
820 | throw new LogicException( |
821 | __CLASS__ . ' should not be cloned, use DerivativeContext instead.' |
822 | ); |
823 | } |
824 | |
825 | } |
826 | |
827 | /** @deprecated class alias since 1.42 */ |
828 | class_alias( RequestContext::class, 'RequestContext' ); |