Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/session/SessionBackend.php (2 issues)

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
 * MediaWiki session backend
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Session
22
 */
23
24
namespace MediaWiki\Session;
25
26
use CachedBagOStuff;
27
use Psr\Log\LoggerInterface;
28
use User;
29
use WebRequest;
30
31
/**
32
 * This is the actual workhorse for Session.
33
 *
34
 * Most code does not need to use this class, you want \MediaWiki\Session\Session.
35
 * The exceptions are SessionProviders and SessionMetadata hook functions,
36
 * which get an instance of this class rather than Session.
37
 *
38
 * The reasons for this split are:
39
 * 1. A session can be attached to multiple requests, but we want the Session
40
 *    object to have some features that correspond to just one of those
41
 *    requests.
42
 * 2. We want reasonable garbage collection behavior, but we also want the
43
 *    SessionManager to hold a reference to every active session so it can be
44
 *    saved when the request ends.
45
 *
46
 * @ingroup Session
47
 * @since 1.27
48
 */
49
final class SessionBackend {
50
	/** @var SessionId */
51
	private $id;
52
53
	private $persist = false;
54
	private $remember = false;
55
	private $forceHTTPS = false;
56
57
	/** @var array|null */
58
	private $data = null;
59
60
	private $forcePersist = false;
61
	private $metaDirty = false;
62
	private $dataDirty = false;
63
64
	/** @var string Used to detect subarray modifications */
65
	private $dataHash = null;
66
67
	/** @var CachedBagOStuff */
68
	private $store;
69
70
	/** @var LoggerInterface */
71
	private $logger;
72
73
	/** @var int */
74
	private $lifetime;
75
76
	/** @var User */
77
	private $user;
78
79
	private $curIndex = 0;
80
81
	/** @var WebRequest[] Session requests */
82
	private $requests = [];
83
84
	/** @var SessionProvider provider */
85
	private $provider;
86
87
	/** @var array|null provider-specified metadata */
88
	private $providerMetadata = null;
89
90
	private $expires = 0;
91
	private $loggedOut = 0;
92
	private $delaySave = 0;
93
94
	private $usePhpSessionHandling = true;
95
	private $checkPHPSessionRecursionGuard = false;
96
97
	private $shutdown = false;
98
99
	/**
100
	 * @param SessionId $id Session ID object
101
	 * @param SessionInfo $info Session info to populate from
102
	 * @param CachedBagOStuff $store Backend data store
103
	 * @param LoggerInterface $logger
104
	 * @param int $lifetime Session data lifetime in seconds
105
	 */
106
	public function __construct(
107
		SessionId $id, SessionInfo $info, CachedBagOStuff $store, LoggerInterface $logger, $lifetime
108
	) {
109
		$phpSessionHandling = \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' );
110
		$this->usePhpSessionHandling = $phpSessionHandling !== 'disable';
111
112
		if ( $info->getUserInfo() && !$info->getUserInfo()->isVerified() ) {
113
			throw new \InvalidArgumentException(
114
				"Refusing to create session for unverified user {$info->getUserInfo()}"
115
			);
116
		}
117
		if ( $info->getProvider() === null ) {
118
			throw new \InvalidArgumentException( 'Cannot create session without a provider' );
119
		}
120
		if ( $info->getId() !== $id->getId() ) {
121
			throw new \InvalidArgumentException( 'SessionId and SessionInfo don\'t match' );
122
		}
123
124
		$this->id = $id;
125
		$this->user = $info->getUserInfo() ? $info->getUserInfo()->getUser() : new User;
126
		$this->store = $store;
127
		$this->logger = $logger;
128
		$this->lifetime = $lifetime;
129
		$this->provider = $info->getProvider();
130
		$this->persist = $info->wasPersisted();
131
		$this->remember = $info->wasRemembered();
132
		$this->forceHTTPS = $info->forceHTTPS();
133
		$this->providerMetadata = $info->getProviderMetadata();
134
135
		$blob = $store->get( wfMemcKey( 'MWSession', (string)$this->id ) );
136
		if ( !is_array( $blob ) ||
137
			!isset( $blob['metadata'] ) || !is_array( $blob['metadata'] ) ||
138
			!isset( $blob['data'] ) || !is_array( $blob['data'] )
139
		) {
140
			$this->data = [];
141
			$this->dataDirty = true;
142
			$this->metaDirty = true;
143
			$this->logger->debug(
144
				'SessionBackend "{session}" is unsaved, marking dirty in constructor',
145
				[
146
					'session' => $this->id,
147
			] );
148
		} else {
149
			$this->data = $blob['data'];
150
			if ( isset( $blob['metadata']['loggedOut'] ) ) {
151
				$this->loggedOut = (int)$blob['metadata']['loggedOut'];
152
			}
153
			if ( isset( $blob['metadata']['expires'] ) ) {
154
				$this->expires = (int)$blob['metadata']['expires'];
155
			} else {
156
				$this->metaDirty = true;
157
				$this->logger->debug(
158
					'SessionBackend "{session}" metadata dirty due to missing expiration timestamp',
159
				[
160
					'session' => $this->id,
161
				] );
162
			}
163
		}
164
		$this->dataHash = md5( serialize( $this->data ) );
165
	}
166
167
	/**
168
	 * Return a new Session for this backend
169
	 * @param WebRequest $request
170
	 * @return Session
171
	 */
172
	public function getSession( WebRequest $request ) {
173
		$index = ++$this->curIndex;
174
		$this->requests[$index] = $request;
175
		$session = new Session( $this, $index, $this->logger );
176
		return $session;
177
	}
178
179
	/**
180
	 * Deregister a Session
181
	 * @private For use by \MediaWiki\Session\Session::__destruct() only
182
	 * @param int $index
183
	 */
184
	public function deregisterSession( $index ) {
185
		unset( $this->requests[$index] );
186
		if ( !$this->shutdown && !count( $this->requests ) ) {
187
			$this->save( true );
188
			$this->provider->getManager()->deregisterSessionBackend( $this );
189
		}
190
	}
191
192
	/**
193
	 * Shut down a session
194
	 * @private For use by \MediaWiki\Session\SessionManager::shutdown() only
195
	 */
196
	public function shutdown() {
197
		$this->save( true );
198
		$this->shutdown = true;
199
	}
200
201
	/**
202
	 * Returns the session ID.
203
	 * @return string
204
	 */
205
	public function getId() {
206
		return (string)$this->id;
207
	}
208
209
	/**
210
	 * Fetch the SessionId object
211
	 * @private For internal use by WebRequest
212
	 * @return SessionId
213
	 */
214
	public function getSessionId() {
215
		return $this->id;
216
	}
217
218
	/**
219
	 * Changes the session ID
220
	 * @return string New ID (might be the same as the old)
221
	 */
222
	public function resetId() {
223
		if ( $this->provider->persistsSessionId() ) {
224
			$oldId = (string)$this->id;
225
			$restart = $this->usePhpSessionHandling && $oldId === session_id() &&
226
				PHPSessionHandler::isEnabled();
227
228
			if ( $restart ) {
229
				// If this session is the one behind PHP's $_SESSION, we need
230
				// to close then reopen it.
231
				session_write_close();
232
			}
233
234
			$this->provider->getManager()->changeBackendId( $this );
235
			$this->provider->sessionIdWasReset( $this, $oldId );
236
			$this->metaDirty = true;
237
			$this->logger->debug(
238
				'SessionBackend "{session}" metadata dirty due to ID reset (formerly "{oldId}")',
239
				[
240
					'session' => $this->id,
241
					'oldId' => $oldId,
242
			] );
243
244
			if ( $restart ) {
245
				session_id( (string)$this->id );
246
				\MediaWiki\quietCall( 'session_start' );
247
			}
248
249
			$this->autosave();
250
251
			// Delete the data for the old session ID now
252
			$this->store->delete( wfMemcKey( 'MWSession', $oldId ) );
253
		}
254
	}
255
256
	/**
257
	 * Fetch the SessionProvider for this session
258
	 * @return SessionProviderInterface
259
	 */
260
	public function getProvider() {
261
		return $this->provider;
262
	}
263
264
	/**
265
	 * Indicate whether this session is persisted across requests
266
	 *
267
	 * For example, if cookies are set.
268
	 *
269
	 * @return bool
270
	 */
271
	public function isPersistent() {
272
		return $this->persist;
273
	}
274
275
	/**
276
	 * Make this session persisted across requests
277
	 *
278
	 * If the session is already persistent, equivalent to calling
279
	 * $this->renew().
280
	 */
281
	public function persist() {
282
		if ( !$this->persist ) {
283
			$this->persist = true;
284
			$this->forcePersist = true;
285
			$this->metaDirty = true;
286
			$this->logger->debug(
287
				'SessionBackend "{session}" force-persist due to persist()',
288
				[
289
					'session' => $this->id,
290
			] );
291
			$this->autosave();
292
		} else {
293
			$this->renew();
294
		}
295
	}
296
297
	/**
298
	 * Make this session not persisted across requests
299
	 */
300
	public function unpersist() {
301
		if ( $this->persist ) {
302
			// Close the PHP session, if we're the one that's open
303 View Code Duplication
			if ( $this->usePhpSessionHandling && PHPSessionHandler::isEnabled() &&
304
				session_id() === (string)$this->id
305
			) {
306
				$this->logger->debug(
307
					'SessionBackend "{session}" Closing PHP session for unpersist',
308
					[ 'session' => $this->id ]
309
				);
310
				session_write_close();
311
				session_id( '' );
312
			}
313
314
			$this->persist = false;
315
			$this->forcePersist = true;
316
			$this->metaDirty = true;
317
318
			// Delete the session data, so the local cache-only write in
319
			// self::save() doesn't get things out of sync with the backend.
320
			$this->store->delete( wfMemcKey( 'MWSession', (string)$this->id ) );
321
322
			$this->autosave();
323
		}
324
	}
325
326
	/**
327
	 * Indicate whether the user should be remembered independently of the
328
	 * session ID.
329
	 * @return bool
330
	 */
331
	public function shouldRememberUser() {
332
		return $this->remember;
333
	}
334
335
	/**
336
	 * Set whether the user should be remembered independently of the session
337
	 * ID.
338
	 * @param bool $remember
339
	 */
340 View Code Duplication
	public function setRememberUser( $remember ) {
341
		if ( $this->remember !== (bool)$remember ) {
342
			$this->remember = (bool)$remember;
343
			$this->metaDirty = true;
344
			$this->logger->debug(
345
				'SessionBackend "{session}" metadata dirty due to remember-user change',
346
				[
347
					'session' => $this->id,
348
			] );
349
			$this->autosave();
350
		}
351
	}
352
353
	/**
354
	 * Returns the request associated with a Session
355
	 * @param int $index Session index
356
	 * @return WebRequest
357
	 */
358
	public function getRequest( $index ) {
359
		if ( !isset( $this->requests[$index] ) ) {
360
			throw new \InvalidArgumentException( 'Invalid session index' );
361
		}
362
		return $this->requests[$index];
363
	}
364
365
	/**
366
	 * Returns the authenticated user for this session
367
	 * @return User
368
	 */
369
	public function getUser() {
370
		return $this->user;
371
	}
372
373
	/**
374
	 * Fetch the rights allowed the user when this session is active.
375
	 * @return null|string[] Allowed user rights, or null to allow all.
376
	 */
377
	public function getAllowedUserRights() {
378
		return $this->provider->getAllowedUserRights( $this );
379
	}
380
381
	/**
382
	 * Indicate whether the session user info can be changed
383
	 * @return bool
384
	 */
385
	public function canSetUser() {
386
		return $this->provider->canChangeUser();
387
	}
388
389
	/**
390
	 * Set a new user for this session
391
	 * @note This should only be called when the user has been authenticated via a login process
392
	 * @param User $user User to set on the session.
393
	 *   This may become a "UserValue" in the future, or User may be refactored
394
	 *   into such.
395
	 */
396
	public function setUser( $user ) {
397
		if ( !$this->canSetUser() ) {
398
			throw new \BadMethodCallException(
399
				'Cannot set user on this session; check $session->canSetUser() first'
400
			);
401
		}
402
403
		$this->user = $user;
404
		$this->metaDirty = true;
405
		$this->logger->debug(
406
			'SessionBackend "{session}" metadata dirty due to user change',
407
			[
408
				'session' => $this->id,
409
		] );
410
		$this->autosave();
411
	}
412
413
	/**
414
	 * Get a suggested username for the login form
415
	 * @param int $index Session index
416
	 * @return string|null
417
	 */
418
	public function suggestLoginUsername( $index ) {
419
		if ( !isset( $this->requests[$index] ) ) {
420
			throw new \InvalidArgumentException( 'Invalid session index' );
421
		}
422
		return $this->provider->suggestLoginUsername( $this->requests[$index] );
423
	}
424
425
	/**
426
	 * Whether HTTPS should be forced
427
	 * @return bool
428
	 */
429
	public function shouldForceHTTPS() {
430
		return $this->forceHTTPS;
431
	}
432
433
	/**
434
	 * Set whether HTTPS should be forced
435
	 * @param bool $force
436
	 */
437 View Code Duplication
	public function setForceHTTPS( $force ) {
438
		if ( $this->forceHTTPS !== (bool)$force ) {
439
			$this->forceHTTPS = (bool)$force;
440
			$this->metaDirty = true;
441
			$this->logger->debug(
442
				'SessionBackend "{session}" metadata dirty due to force-HTTPS change',
443
				[
444
					'session' => $this->id,
445
			] );
446
			$this->autosave();
447
		}
448
	}
449
450
	/**
451
	 * Fetch the "logged out" timestamp
452
	 * @return int
453
	 */
454
	public function getLoggedOutTimestamp() {
455
		return $this->loggedOut;
456
	}
457
458
	/**
459
	 * Set the "logged out" timestamp
460
	 * @param int $ts
461
	 */
462
	public function setLoggedOutTimestamp( $ts = null ) {
463
		$ts = (int)$ts;
464
		if ( $this->loggedOut !== $ts ) {
465
			$this->loggedOut = $ts;
466
			$this->metaDirty = true;
467
			$this->logger->debug(
468
				'SessionBackend "{session}" metadata dirty due to logged-out-timestamp change',
469
				[
470
					'session' => $this->id,
471
			] );
472
			$this->autosave();
473
		}
474
	}
475
476
	/**
477
	 * Fetch provider metadata
478
	 * @protected For use by SessionProvider subclasses only
479
	 * @return array|null
480
	 */
481
	public function getProviderMetadata() {
482
		return $this->providerMetadata;
483
	}
484
485
	/**
486
	 * Set provider metadata
487
	 * @protected For use by SessionProvider subclasses only
488
	 * @param array|null $metadata
489
	 */
490
	public function setProviderMetadata( $metadata ) {
491
		if ( $metadata !== null && !is_array( $metadata ) ) {
492
			throw new \InvalidArgumentException( '$metadata must be an array or null' );
493
		}
494
		if ( $this->providerMetadata !== $metadata ) {
495
			$this->providerMetadata = $metadata;
496
			$this->metaDirty = true;
497
			$this->logger->debug(
498
				'SessionBackend "{session}" metadata dirty due to provider metadata change',
499
				[
500
					'session' => $this->id,
501
			] );
502
			$this->autosave();
503
		}
504
	}
505
506
	/**
507
	 * Fetch the session data array
508
	 *
509
	 * Note the caller is responsible for calling $this->dirty() if anything in
510
	 * the array is changed.
511
	 *
512
	 * @private For use by \MediaWiki\Session\Session only.
513
	 * @return array
514
	 */
515
	public function &getData() {
516
		return $this->data;
517
	}
518
519
	/**
520
	 * Add data to the session.
521
	 *
522
	 * Overwrites any existing data under the same keys.
523
	 *
524
	 * @param array $newData Key-value pairs to add to the session
525
	 */
526
	public function addData( array $newData ) {
527
		$data = &$this->getData();
528
		foreach ( $newData as $key => $value ) {
529
			if ( !array_key_exists( $key, $data ) || $data[$key] !== $value ) {
530
				$data[$key] = $value;
531
				$this->dataDirty = true;
532
				$this->logger->debug(
533
					'SessionBackend "{session}" data dirty due to addData(): {callers}',
534
					[
535
						'session' => $this->id,
536
						'callers' => wfGetAllCallers( 5 ),
537
				] );
538
			}
539
		}
540
	}
541
542
	/**
543
	 * Mark data as dirty
544
	 * @private For use by \MediaWiki\Session\Session only.
545
	 */
546
	public function dirty() {
547
		$this->dataDirty = true;
548
		$this->logger->debug(
549
			'SessionBackend "{session}" data dirty due to dirty(): {callers}',
550
			[
551
				'session' => $this->id,
552
				'callers' => wfGetAllCallers( 5 ),
553
		] );
554
	}
555
556
	/**
557
	 * Renew the session by resaving everything
558
	 *
559
	 * Resets the TTL in the backend store if the session is near expiring, and
560
	 * re-persists the session to any active WebRequests if persistent.
561
	 */
562
	public function renew() {
563
		if ( time() + $this->lifetime / 2 > $this->expires ) {
564
			$this->metaDirty = true;
565
			$this->logger->debug(
566
				'SessionBackend "{callers}" metadata dirty for renew(): {callers}',
567
				[
568
					'session' => $this->id,
569
					'callers' => wfGetAllCallers( 5 ),
570
			] );
571
			if ( $this->persist ) {
572
				$this->forcePersist = true;
573
				$this->logger->debug(
574
					'SessionBackend "{session}" force-persist for renew(): {callers}',
575
					[
576
						'session' => $this->id,
577
						'callers' => wfGetAllCallers( 5 ),
578
				] );
579
			}
580
		}
581
		$this->autosave();
582
	}
583
584
	/**
585
	 * Delay automatic saving while multiple updates are being made
586
	 *
587
	 * Calls to save() will not be delayed.
588
	 *
589
	 * @return \Wikimedia\ScopedCallback When this goes out of scope, a save will be triggered
590
	 */
591
	public function delaySave() {
592
		$this->delaySave++;
593
		return new \Wikimedia\ScopedCallback( function () {
594
			if ( --$this->delaySave <= 0 ) {
595
				$this->delaySave = 0;
596
				$this->save();
597
			}
598
		} );
599
	}
600
601
	/**
602
	 * Save the session, unless delayed
603
	 * @see SessionBackend::save()
604
	 */
605
	private function autosave() {
606
		if ( $this->delaySave <= 0 ) {
607
			$this->save();
608
		}
609
	}
610
611
	/**
612
	 * Save the session
613
	 *
614
	 * Update both the backend data and the associated WebRequest(s) to
615
	 * reflect the state of the the SessionBackend. This might include
616
	 * persisting or unpersisting the session.
617
	 *
618
	 * @param bool $closing Whether the session is being closed
619
	 */
620
	public function save( $closing = false ) {
621
		$anon = $this->user->isAnon();
622
623
		if ( !$anon && $this->provider->getManager()->isUserSessionPrevented( $this->user->getName() ) ) {
624
			$this->logger->debug(
625
				'SessionBackend "{session}" not saving, user {user} was ' .
626
				'passed to SessionManager::preventSessionsForUser',
627
				[
628
					'session' => $this->id,
629
					'user' => $this->user,
630
			] );
631
			return;
632
		}
633
634
		// Ensure the user has a token
635
		// @codeCoverageIgnoreStart
636
		if ( !$anon && !$this->user->getToken( false ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->user->getToken(false) of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
637
			$this->logger->debug(
638
				'SessionBackend "{session}" creating token for user {user} on save',
639
				[
640
					'session' => $this->id,
641
					'user' => $this->user,
642
			] );
643
			$this->user->setToken();
644
			if ( !wfReadOnly() ) {
645
				// Promise that the token set here will be valid; save it at end of request
646
				$user = $this->user;
647
				\DeferredUpdates::addCallableUpdate( function () use ( $user ) {
648
					$user->saveSettings();
649
				} );
650
			}
651
			$this->metaDirty = true;
652
		}
653
		// @codeCoverageIgnoreEnd
654
655
		if ( !$this->metaDirty && !$this->dataDirty &&
656
			$this->dataHash !== md5( serialize( $this->data ) )
657
		) {
658
			$this->logger->debug(
659
				'SessionBackend "{session}" data dirty due to hash mismatch, {expected} !== {got}',
660
				[
661
					'session' => $this->id,
662
					'expected' => $this->dataHash,
663
					'got' => md5( serialize( $this->data ) ),
664
			] );
665
			$this->dataDirty = true;
666
		}
667
668
		if ( !$this->metaDirty && !$this->dataDirty && !$this->forcePersist ) {
669
			return;
670
		}
671
672
		$this->logger->debug(
673
			'SessionBackend "{session}" save: dataDirty={dataDirty} ' .
674
			'metaDirty={metaDirty} forcePersist={forcePersist}',
675
			[
676
				'session' => $this->id,
677
				'dataDirty' => (int)$this->dataDirty,
678
				'metaDirty' => (int)$this->metaDirty,
679
				'forcePersist' => (int)$this->forcePersist,
680
		] );
681
682
		// Persist or unpersist to the provider, if necessary
683
		if ( $this->metaDirty || $this->forcePersist ) {
684
			if ( $this->persist ) {
685
				foreach ( $this->requests as $request ) {
686
					$request->setSessionId( $this->getSessionId() );
687
					$this->provider->persistSession( $this, $request );
688
				}
689
				if ( !$closing ) {
690
					$this->checkPHPSession();
691
				}
692
			} else {
693
				foreach ( $this->requests as $request ) {
694
					if ( $request->getSessionId() === $this->id ) {
695
						$this->provider->unpersistSession( $request );
696
					}
697
				}
698
			}
699
		}
700
701
		$this->forcePersist = false;
702
703
		if ( !$this->metaDirty && !$this->dataDirty ) {
704
			return;
705
		}
706
707
		// Save session data to store, if necessary
708
		$metadata = $origMetadata = [
709
			'provider' => (string)$this->provider,
710
			'providerMetadata' => $this->providerMetadata,
711
			'userId' => $anon ? 0 : $this->user->getId(),
712
			'userName' => User::isValidUserName( $this->user->getName() ) ? $this->user->getName() : null,
713
			'userToken' => $anon ? null : $this->user->getToken(),
714
			'remember' => !$anon && $this->remember,
715
			'forceHTTPS' => $this->forceHTTPS,
716
			'expires' => time() + $this->lifetime,
717
			'loggedOut' => $this->loggedOut,
718
			'persisted' => $this->persist,
719
		];
720
721
		\Hooks::run( 'SessionMetadata', [ $this, &$metadata, $this->requests ] );
722
723
		foreach ( $origMetadata as $k => $v ) {
724
			if ( $metadata[$k] !== $v ) {
725
				throw new \UnexpectedValueException( "SessionMetadata hook changed metadata key \"$k\"" );
726
			}
727
		}
728
729
		$flags = $this->persist ? 0 : CachedBagOStuff::WRITE_CACHE_ONLY;
730
		$flags |= CachedBagOStuff::WRITE_SYNC; // write to all datacenters
731
		$this->store->set(
732
			wfMemcKey( 'MWSession', (string)$this->id ),
733
			[
734
				'data' => $this->data,
735
				'metadata' => $metadata,
736
			],
737
			$metadata['expires'],
738
			$flags
739
		);
740
741
		$this->metaDirty = false;
742
		$this->dataDirty = false;
743
		$this->dataHash = md5( serialize( $this->data ) );
744
		$this->expires = $metadata['expires'];
745
	}
746
747
	/**
748
	 * For backwards compatibility, open the PHP session when the global
749
	 * session is persisted
750
	 */
751
	private function checkPHPSession() {
752
		if ( !$this->checkPHPSessionRecursionGuard ) {
753
			$this->checkPHPSessionRecursionGuard = true;
754
			$reset = new \Wikimedia\ScopedCallback( function () {
0 ignored issues
show
$reset is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
755
				$this->checkPHPSessionRecursionGuard = false;
756
			} );
757
758 View Code Duplication
			if ( $this->usePhpSessionHandling && session_id() === '' && PHPSessionHandler::isEnabled() &&
759
				SessionManager::getGlobalSession()->getId() === (string)$this->id
760
			) {
761
				$this->logger->debug(
762
					'SessionBackend "{session}" Taking over PHP session',
763
					[
764
						'session' => $this->id,
765
				] );
766
				session_id( (string)$this->id );
767
				\MediaWiki\quietCall( 'session_start' );
768
			}
769
		}
770
	}
771
772
}
773