Completed
Branch master (939199)
by
unknown
39:35
created

includes/context/RequestContext.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
26
use MediaWiki\Logger\LoggerFactory;
27
use MediaWiki\MediaWikiServices;
28
use Wikimedia\ScopedCallback;
29
30
/**
31
 * Group all the pieces relevant to the context of a request into one instance
32
 */
33
class RequestContext implements IContextSource, MutableContext {
34
	/**
35
	 * @var WebRequest
36
	 */
37
	private $request;
38
39
	/**
40
	 * @var Title
41
	 */
42
	private $title;
43
44
	/**
45
	 * @var WikiPage
46
	 */
47
	private $wikipage;
48
49
	/**
50
	 * @var OutputPage
51
	 */
52
	private $output;
53
54
	/**
55
	 * @var User
56
	 */
57
	private $user;
58
59
	/**
60
	 * @var Language
61
	 */
62
	private $lang;
63
64
	/**
65
	 * @var Skin
66
	 */
67
	private $skin;
68
69
	/**
70
	 * @var Timing
71
	 */
72
	private $timing;
73
74
	/**
75
	 * @var Config
76
	 */
77
	private $config;
78
79
	/**
80
	 * @var RequestContext
81
	 */
82
	private static $instance = null;
83
84
	/**
85
	 * Set the Config object
86
	 *
87
	 * @param Config $c
88
	 */
89
	public function setConfig( Config $c ) {
90
		$this->config = $c;
91
	}
92
93
	/**
94
	 * Get the Config object
95
	 *
96
	 * @return Config
97
	 */
98
	public function getConfig() {
99
		if ( $this->config === null ) {
100
			// @todo In the future, we could move this to WebStart.php so
101
			// the Config object is ready for when initialization happens
102
			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
103
		}
104
105
		return $this->config;
106
	}
107
108
	/**
109
	 * Set the WebRequest object
110
	 *
111
	 * @param WebRequest $r
112
	 */
113
	public function setRequest( WebRequest $r ) {
114
		$this->request = $r;
115
	}
116
117
	/**
118
	 * Get the WebRequest object
119
	 *
120
	 * @return WebRequest
121
	 */
122
	public function getRequest() {
123
		if ( $this->request === null ) {
124
			global $wgCommandLineMode;
125
			// create the WebRequest object on the fly
126
			if ( $wgCommandLineMode ) {
127
				$this->request = new FauxRequest( [] );
128
			} else {
129
				$this->request = new WebRequest();
130
			}
131
		}
132
133
		return $this->request;
134
	}
135
136
	/**
137
	 * Get the Stats object
138
	 *
139
	 * @deprecated since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)
140
	 *
141
	 * @return StatsdDataFactory
142
	 */
143
	public function getStats() {
144
		return MediaWikiServices::getInstance()->getStatsdDataFactory();
145
	}
146
147
	/**
148
	 * Get the timing object
149
	 *
150
	 * @return Timing
151
	 */
152
	public function getTiming() {
153
		if ( $this->timing === null ) {
154
			$this->timing = new Timing( [
155
				'logger' => LoggerFactory::getInstance( 'Timing' )
156
			] );
157
		}
158
		return $this->timing;
159
	}
160
161
	/**
162
	 * Set the Title object
163
	 *
164
	 * @param Title|null $title
165
	 */
166
	public function setTitle( Title $title = null ) {
167
		$this->title = $title;
168
		// Erase the WikiPage so a new one with the new title gets created.
169
		$this->wikipage = null;
170
	}
171
172
	/**
173
	 * Get the Title object
174
	 *
175
	 * @return Title|null
176
	 */
177
	public function getTitle() {
178
		if ( $this->title === null ) {
179
			global $wgTitle; # fallback to $wg till we can improve this
180
			$this->title = $wgTitle;
181
			wfDebugLog(
182
				'GlobalTitleFail',
183
				__METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
184
			);
185
		}
186
187
		return $this->title;
188
	}
189
190
	/**
191
	 * Check, if a Title object is set
192
	 *
193
	 * @since 1.25
194
	 * @return bool
195
	 */
196
	public function hasTitle() {
197
		return $this->title !== null;
198
	}
199
200
	/**
201
	 * Check whether a WikiPage object can be get with getWikiPage().
202
	 * Callers should expect that an exception is thrown from getWikiPage()
203
	 * if this method returns false.
204
	 *
205
	 * @since 1.19
206
	 * @return bool
207
	 */
208
	public function canUseWikiPage() {
209
		if ( $this->wikipage ) {
210
			// If there's a WikiPage object set, we can for sure get it
211
			return true;
212
		}
213
		// Only pages with legitimate titles can have WikiPages.
214
		// That usually means pages in non-virtual namespaces.
215
		$title = $this->getTitle();
216
		return $title ? $title->canExist() : false;
217
	}
218
219
	/**
220
	 * Set the WikiPage object
221
	 *
222
	 * @since 1.19
223
	 * @param WikiPage $p
224
	 */
225
	public function setWikiPage( WikiPage $p ) {
226
		$pageTitle = $p->getTitle();
227
		if ( !$this->hasTitle() || !$pageTitle->equals( $this->getTitle() ) ) {
0 ignored issues
show
It seems like $this->getTitle() can be null; however, equals() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
228
			$this->setTitle( $pageTitle );
229
		}
230
		// Defer this to the end since setTitle sets it to null.
231
		$this->wikipage = $p;
232
	}
233
234
	/**
235
	 * Get the WikiPage object.
236
	 * May throw an exception if there's no Title object set or the Title object
237
	 * belongs to a special namespace that doesn't have WikiPage, so use first
238
	 * canUseWikiPage() to check whether this method can be called safely.
239
	 *
240
	 * @since 1.19
241
	 * @throws MWException
242
	 * @return WikiPage
243
	 */
244
	public function getWikiPage() {
245
		if ( $this->wikipage === null ) {
246
			$title = $this->getTitle();
247
			if ( $title === null ) {
248
				throw new MWException( __METHOD__ . ' called without Title object set' );
249
			}
250
			$this->wikipage = WikiPage::factory( $title );
251
		}
252
253
		return $this->wikipage;
254
	}
255
256
	/**
257
	 * @param OutputPage $o
258
	 */
259
	public function setOutput( OutputPage $o ) {
260
		$this->output = $o;
261
	}
262
263
	/**
264
	 * Get the OutputPage object
265
	 *
266
	 * @return OutputPage
267
	 */
268
	public function getOutput() {
269
		if ( $this->output === null ) {
270
			$this->output = new OutputPage( $this );
271
		}
272
273
		return $this->output;
274
	}
275
276
	/**
277
	 * Set the User object
278
	 *
279
	 * @param User $u
280
	 */
281
	public function setUser( User $u ) {
282
		$this->user = $u;
283
	}
284
285
	/**
286
	 * Get the User object
287
	 *
288
	 * @return User
289
	 */
290
	public function getUser() {
291
		if ( $this->user === null ) {
292
			$this->user = User::newFromSession( $this->getRequest() );
293
		}
294
295
		return $this->user;
296
	}
297
298
	/**
299
	 * Accepts a language code and ensures it's sane. Outputs a cleaned up language
300
	 * code and replaces with $wgLanguageCode if not sane.
301
	 * @param string $code Language code
302
	 * @return string
303
	 */
304
	public static function sanitizeLangCode( $code ) {
305
		global $wgLanguageCode;
306
307
		// BCP 47 - letter case MUST NOT carry meaning
308
		$code = strtolower( $code );
309
310
		# Validate $code
311
		if ( !$code || !Language::isValidCode( $code ) || $code === 'qqq' ) {
312
			wfDebug( "Invalid user language code\n" );
313
			$code = $wgLanguageCode;
314
		}
315
316
		return $code;
317
	}
318
319
	/**
320
	 * Set the Language object
321
	 *
322
	 * @param Language|string $l Language instance or language code
323
	 * @throws MWException
324
	 * @since 1.19
325
	 */
326 View Code Duplication
	public function setLanguage( $l ) {
327
		if ( $l instanceof Language ) {
328
			$this->lang = $l;
329
		} elseif ( is_string( $l ) ) {
330
			$l = self::sanitizeLangCode( $l );
331
			$obj = Language::factory( $l );
332
			$this->lang = $obj;
333
		} else {
334
			throw new MWException( __METHOD__ . " was passed an invalid type of data." );
335
		}
336
	}
337
338
	/**
339
	 * Get the Language object.
340
	 * Initialization of user or request objects can depend on this.
341
	 * @return Language
342
	 * @throws Exception
343
	 * @since 1.19
344
	 */
345
	public function getLanguage() {
346
		if ( isset( $this->recursion ) ) {
347
			trigger_error( "Recursion detected in " . __METHOD__, E_USER_WARNING );
348
			$e = new Exception;
349
			wfDebugLog( 'recursion-guard', "Recursion detected:\n" . $e->getTraceAsString() );
350
351
			$code = $this->getConfig()->get( 'LanguageCode' ) ?: 'en';
352
			$this->lang = Language::factory( $code );
353
		} elseif ( $this->lang === null ) {
354
			$this->recursion = true;
355
356
			global $wgContLang;
357
358
			try {
359
				$request = $this->getRequest();
360
				$user = $this->getUser();
361
362
				$code = $request->getVal( 'uselang', 'user' );
363
				if ( $code === 'user' ) {
364
					$code = $user->getOption( 'language' );
365
				}
366
				$code = self::sanitizeLangCode( $code );
367
368
				Hooks::run( 'UserGetLanguageObject', [ $user, &$code, $this ] );
369
370
				if ( $code === $this->getConfig()->get( 'LanguageCode' ) ) {
371
					$this->lang = $wgContLang;
372
				} else {
373
					$obj = Language::factory( $code );
374
					$this->lang = $obj;
375
				}
376
377
				unset( $this->recursion );
378
			}
379
			catch ( Exception $ex ) {
380
				unset( $this->recursion );
381
				throw $ex;
382
			}
383
		}
384
385
		return $this->lang;
386
	}
387
388
	/**
389
	 * Set the Skin object
390
	 *
391
	 * @param Skin $s
392
	 */
393
	public function setSkin( Skin $s ) {
394
		$this->skin = clone $s;
395
		$this->skin->setContext( $this );
396
	}
397
398
	/**
399
	 * Get the Skin object
400
	 *
401
	 * @return Skin
402
	 */
403
	public function getSkin() {
404
		if ( $this->skin === null ) {
405
			$skin = null;
406
			Hooks::run( 'RequestContextCreateSkin', [ $this, &$skin ] );
407
			$factory = SkinFactory::getDefaultInstance();
408
409
			// If the hook worked try to set a skin from it
410
			if ( $skin instanceof Skin ) {
411
				$this->skin = $skin;
412
			} elseif ( is_string( $skin ) ) {
413
				// Normalize the key, just in case the hook did something weird.
414
				$normalized = Skin::normalizeKey( $skin );
415
				$this->skin = $factory->makeSkin( $normalized );
416
			}
417
418
			// If this is still null (the hook didn't run or didn't work)
419
			// then go through the normal processing to load a skin
420
			if ( $this->skin === null ) {
421
				if ( !in_array( 'skin', $this->getConfig()->get( 'HiddenPrefs' ) ) ) {
422
					# get the user skin
423
					$userSkin = $this->getUser()->getOption( 'skin' );
424
					$userSkin = $this->getRequest()->getVal( 'useskin', $userSkin );
425
				} else {
426
					# if we're not allowing users to override, then use the default
427
					$userSkin = $this->getConfig()->get( 'DefaultSkin' );
428
				}
429
430
				// Normalize the key in case the user is passing gibberish
431
				// or has old preferences (bug 69566).
432
				$normalized = Skin::normalizeKey( $userSkin );
433
434
				// Skin::normalizeKey will also validate it, so
435
				// this won't throw an exception
436
				$this->skin = $factory->makeSkin( $normalized );
437
			}
438
439
			// After all that set a context on whatever skin got created
440
			$this->skin->setContext( $this );
441
		}
442
443
		return $this->skin;
444
	}
445
446
	/** Helpful methods **/
447
448
	/**
449
	 * Get a Message object with context set
450
	 * Parameters are the same as wfMessage()
451
	 *
452
	 * @param mixed ...
453
	 * @return Message
454
	 */
455
	public function msg() {
456
		$args = func_get_args();
457
458
		return call_user_func_array( 'wfMessage', $args )->setContext( $this );
459
	}
460
461
	/** Static methods **/
462
463
	/**
464
	 * Get the RequestContext object associated with the main request
465
	 *
466
	 * @return RequestContext
467
	 */
468
	public static function getMain() {
469
		if ( self::$instance === null ) {
470
			self::$instance = new self;
471
		}
472
473
		return self::$instance;
474
	}
475
476
	/**
477
	 * Get the RequestContext object associated with the main request
478
	 * and gives a warning to the log, to find places, where a context maybe is missing.
479
	 *
480
	 * @param string $func
481
	 * @return RequestContext
482
	 * @since 1.24
483
	 */
484
	public static function getMainAndWarn( $func = __METHOD__ ) {
485
		wfDebug( $func . ' called without context. ' .
486
			"Using RequestContext::getMain() for sanity\n" );
487
488
		return self::getMain();
489
	}
490
491
	/**
492
	 * Resets singleton returned by getMain(). Should be called only from unit tests.
493
	 */
494
	public static function resetMain() {
495
		if ( !( defined( 'MW_PHPUNIT_TEST' ) || defined( 'MW_PARSER_TEST' ) ) ) {
496
			throw new MWException( __METHOD__ . '() should be called only from unit tests!' );
497
		}
498
		self::$instance = null;
499
	}
500
501
	/**
502
	 * Export the resolved user IP, HTTP headers, user ID, and session ID.
503
	 * The result will be reasonably sized to allow for serialization.
504
	 *
505
	 * @return array
506
	 * @since 1.21
507
	 */
508
	public function exportSession() {
509
		$session = MediaWiki\Session\SessionManager::getGlobalSession();
510
		return [
511
			'ip' => $this->getRequest()->getIP(),
512
			'headers' => $this->getRequest()->getAllHeaders(),
513
			'sessionId' => $session->isPersistent() ? $session->getId() : '',
514
			'userId' => $this->getUser()->getId()
515
		];
516
	}
517
518
	/**
519
	 * Import an client IP address, HTTP headers, user ID, and session ID
520
	 *
521
	 * This sets the current session, $wgUser, and $wgRequest from $params.
522
	 * Once the return value falls out of scope, the old context is restored.
523
	 * This method should only be called in contexts where there is no session
524
	 * ID or end user receiving the response (CLI or HTTP job runners). This
525
	 * is partly enforced, and is done so to avoid leaking cookies if certain
526
	 * error conditions arise.
527
	 *
528
	 * This is useful when background scripts inherit context when acting on
529
	 * behalf of a user. In general the 'sessionId' parameter should be set
530
	 * to an empty string unless session importing is *truly* needed. This
531
	 * feature is somewhat deprecated.
532
	 *
533
	 * @note suhosin.session.encrypt may interfere with this method.
534
	 *
535
	 * @param array $params Result of RequestContext::exportSession()
536
	 * @return ScopedCallback
537
	 * @throws MWException
538
	 * @since 1.21
539
	 */
540
	public static function importScopedSession( array $params ) {
541
		if ( strlen( $params['sessionId'] ) &&
542
			MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent()
543
		) {
544
			// Sanity check to avoid sending random cookies for the wrong users.
545
			// This method should only called by CLI scripts or by HTTP job runners.
546
			throw new MWException( "Sessions can only be imported when none is active." );
547
		} elseif ( !IP::isValid( $params['ip'] ) ) {
548
			throw new MWException( "Invalid client IP address '{$params['ip']}'." );
549
		}
550
551
		if ( $params['userId'] ) { // logged-in user
552
			$user = User::newFromId( $params['userId'] );
553
			$user->load();
554
			if ( !$user->getId() ) {
555
				throw new MWException( "No user with ID '{$params['userId']}'." );
556
			}
557
		} else { // anon user
558
			$user = User::newFromName( $params['ip'], false );
559
		}
560
561
		$importSessionFunc = function ( User $user, array $params ) {
562
			global $wgRequest, $wgUser;
563
564
			$context = RequestContext::getMain();
565
566
			// Commit and close any current session
567
			if ( MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
568
				session_write_close(); // persist
569
				session_id( '' ); // detach
570
				$_SESSION = []; // clear in-memory array
571
			}
572
573
			// Get new session, if applicable
574
			$session = null;
575
			if ( strlen( $params['sessionId'] ) ) { // don't make a new random ID
576
				$manager = MediaWiki\Session\SessionManager::singleton();
577
				$session = $manager->getSessionById( $params['sessionId'], true )
578
					?: $manager->getEmptySession();
579
			}
580
581
			// Remove any user IP or agent information, and attach the request
582
			// with the new session.
583
			$context->setRequest( new FauxRequest( [], false, $session ) );
584
			$wgRequest = $context->getRequest(); // b/c
585
586
			// Now that all private information is detached from the user, it should
587
			// be safe to load the new user. If errors occur or an exception is thrown
588
			// and caught (leaving the main context in a mixed state), there is no risk
589
			// of the User object being attached to the wrong IP, headers, or session.
590
			$context->setUser( $user );
591
			$wgUser = $context->getUser(); // b/c
592
			if ( $session && MediaWiki\Session\PHPSessionHandler::isEnabled() ) {
593
				session_id( $session->getId() );
594
				MediaWiki\quietCall( 'session_start' );
595
			}
596
			$request = new FauxRequest( [], false, $session );
597
			$request->setIP( $params['ip'] );
598
			foreach ( $params['headers'] as $name => $value ) {
599
				$request->setHeader( $name, $value );
600
			}
601
			// Set the current context to use the new WebRequest
602
			$context->setRequest( $request );
603
			$wgRequest = $context->getRequest(); // b/c
604
		};
605
606
		// Stash the old session and load in the new one
607
		$oUser = self::getMain()->getUser();
608
		$oParams = self::getMain()->exportSession();
609
		$oRequest = self::getMain()->getRequest();
610
		$importSessionFunc( $user, $params );
611
612
		// Set callback to save and close the new session and reload the old one
613
		return new ScopedCallback(
614
			function () use ( $importSessionFunc, $oUser, $oParams, $oRequest ) {
615
				global $wgRequest;
616
				$importSessionFunc( $oUser, $oParams );
617
				// Restore the exact previous Request object (instead of leaving FauxRequest)
618
				RequestContext::getMain()->setRequest( $oRequest );
619
				$wgRequest = RequestContext::getMain()->getRequest(); // b/c
620
			}
621
		);
622
	}
623
624
	/**
625
	 * Create a new extraneous context. The context is filled with information
626
	 * external to the current session.
627
	 * - Title is specified by argument
628
	 * - Request is a FauxRequest, or a FauxRequest can be specified by argument
629
	 * - User is an anonymous user, for separation IPv4 localhost is used
630
	 * - Language will be based on the anonymous user and request, may be content
631
	 *   language or a uselang param in the fauxrequest data may change the lang
632
	 * - Skin will be based on the anonymous user, should be the wiki's default skin
633
	 *
634
	 * @param Title $title Title to use for the extraneous request
635
	 * @param WebRequest|array $request A WebRequest or data to use for a FauxRequest
636
	 * @return RequestContext
637
	 */
638
	public static function newExtraneousContext( Title $title, $request = [] ) {
639
		$context = new self;
640
		$context->setTitle( $title );
641
		if ( $request instanceof WebRequest ) {
642
			$context->setRequest( $request );
643
		} else {
644
			$context->setRequest( new FauxRequest( $request ) );
645
		}
646
		$context->user = User::newFromName( '127.0.0.1', false );
647
648
		return $context;
649
	}
650
}
651