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