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