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;
0 ignored issues
show
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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() ) ) {
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