Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / session / SessionManagerTest.php
blob9d06c1ff1b49f8211eb9723ae787d89ad170674d
1 <?php
3 namespace MediaWiki\Tests\Session;
5 use DummySessionProvider;
6 use InvalidArgumentException;
7 use MediaWiki\Config\HashConfig;
8 use MediaWiki\Context\RequestContext;
9 use MediaWiki\MainConfigNames;
10 use MediaWiki\Request\FauxRequest;
11 use MediaWiki\Request\ProxyLookup;
12 use MediaWiki\Session\CookieSessionProvider;
13 use MediaWiki\Session\MetadataMergeException;
14 use MediaWiki\Session\PHPSessionHandler;
15 use MediaWiki\Session\Session;
16 use MediaWiki\Session\SessionInfo;
17 use MediaWiki\Session\SessionManager;
18 use MediaWiki\Session\SessionOverflowException;
19 use MediaWiki\Session\SessionProvider;
20 use MediaWiki\Session\UserInfo;
21 use MediaWiki\Utils\MWTimestamp;
22 use MediaWikiIntegrationTestCase;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\LogLevel;
25 use Psr\Log\NullLogger;
26 use ReflectionClass;
27 use stdClass;
28 use TestLogger;
29 use UnexpectedValueException;
30 use Wikimedia\ScopedCallback;
31 use Wikimedia\TestingAccessWrapper;
33 /**
34 * @group Session
35 * @group Database
36 * @covers \MediaWiki\Session\SessionManager
38 class SessionManagerTest extends MediaWikiIntegrationTestCase {
39 use SessionProviderTestTrait;
41 private HashConfig $config;
42 private TestLogger $logger;
43 private TestBagOStuff $store;
45 protected function getManager() {
46 $this->store = new TestBagOStuff();
47 $cacheType = $this->setMainCache( $this->store );
49 $this->config = new HashConfig( [
50 MainConfigNames::LanguageCode => 'en',
51 MainConfigNames::SessionCacheType => $cacheType,
52 MainConfigNames::ObjectCacheSessionExpiry => 100,
53 MainConfigNames::SessionProviders => [
54 [ 'class' => DummySessionProvider::class ],
56 ] );
57 $this->logger = new TestLogger( false, static function ( $m ) {
58 return ( str_starts_with( $m, 'SessionBackend ' )
59 || str_starts_with( $m, 'SessionManager using store ' )
60 // These were added for T264793 and behave somewhat erratically, not worth testing
61 || str_starts_with( $m, 'Failed to load session, unpersisting' )
62 || preg_match( '/^(Persisting|Unpersisting) session (for|due to)/', $m )
63 ) ? null : $m;
64 } );
66 return new SessionManager( [
67 'config' => $this->config,
68 'logger' => $this->logger,
69 'store' => $this->store,
70 ] );
73 protected function objectCacheDef( $object ) {
74 return [ 'factory' => static function () use ( $object ) {
75 return $object;
76 } ];
79 public function testSingleton() {
80 $reset = TestUtils::setSessionManagerSingleton( null );
82 $singleton = SessionManager::singleton();
83 $this->assertInstanceOf( SessionManager::class, $singleton );
84 $this->assertSame( $singleton, SessionManager::singleton() );
87 public function testGetGlobalSession() {
88 $context = RequestContext::getMain();
90 if ( !PHPSessionHandler::isInstalled() ) {
91 PHPSessionHandler::install( SessionManager::singleton() );
93 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
94 $handler = TestingAccessWrapper::newFromObject( $staticAccess->instance );
95 $oldEnable = $handler->enable;
96 $reset[] = new ScopedCallback( static function () use ( $handler, $oldEnable ) {
97 if ( $handler->enable ) {
98 session_write_close();
100 $handler->enable = $oldEnable;
101 } );
102 $reset[] = TestUtils::setSessionManagerSingleton( $this->getManager() );
104 $handler->enable = true;
105 $request = new FauxRequest();
106 $context->setRequest( $request );
107 $id = $request->getSession()->getId();
109 session_write_close();
110 session_id( '' );
111 $session = SessionManager::getGlobalSession();
112 $this->assertSame( $id, $session->getId() );
114 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
115 $session = SessionManager::getGlobalSession();
116 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() );
117 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() );
119 session_write_close();
120 $handler->enable = false;
121 $request = new FauxRequest();
122 $context->setRequest( $request );
123 $id = $request->getSession()->getId();
125 session_id( '' );
126 $session = SessionManager::getGlobalSession();
127 $this->assertSame( $id, $session->getId() );
129 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
130 $session = SessionManager::getGlobalSession();
131 $this->assertSame( $id, $session->getId() );
132 $this->assertSame( $id, $request->getSession()->getId() );
135 public function testConstructor() {
136 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
137 $this->assertSame( $this->config, $manager->config );
138 $this->assertSame( $this->logger, $manager->logger );
139 $this->assertSame( $this->store, $manager->store );
141 $manager = TestingAccessWrapper::newFromObject( new SessionManager() );
142 $this->assertSame( $this->getServiceContainer()->getMainConfig(), $manager->config );
144 $manager = TestingAccessWrapper::newFromObject( new SessionManager( [
145 'config' => $this->config,
146 'store' => $this->store,
147 ] ) );
148 $this->assertSame( $this->store, $manager->store );
150 foreach ( [
151 'config' => '$options[\'config\'] must be an instance of Config',
152 'logger' => '$options[\'logger\'] must be an instance of LoggerInterface',
153 'store' => '$options[\'store\'] must be an instance of BagOStuff',
154 ] as $key => $error ) {
155 try {
156 new SessionManager( [ $key => new stdClass ] );
157 $this->fail( 'Expected exception not thrown' );
158 } catch ( InvalidArgumentException $ex ) {
159 $this->assertSame( $error, $ex->getMessage() );
164 public function testGetSessionForRequest() {
165 $manager = $this->getManager();
166 $request = new FauxRequest();
167 $requestUnpersist1 = false;
168 $requestUnpersist2 = false;
169 $requestInfo1 = null;
170 $requestInfo2 = null;
172 $id1 = '';
173 $id2 = '';
174 $idEmpty = 'empty-session-------------------';
176 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
177 ->onlyMethods(
178 [ 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe', 'unpersistSession' ]
181 $provider1 = $providerBuilder->getMock();
182 $provider1->method( 'provideSessionInfo' )
183 ->with( $this->identicalTo( $request ) )
184 ->willReturnCallback( static function ( $request ) use ( &$requestInfo1 ) {
185 return $requestInfo1;
186 } );
187 $provider1->method( 'newSessionInfo' )
188 ->willReturnCallback( static function () use ( $idEmpty, $provider1 ) {
189 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
190 'provider' => $provider1,
191 'id' => $idEmpty,
192 'persisted' => true,
193 'idIsSafe' => true,
194 ] );
195 } );
196 $provider1->method( '__toString' )
197 ->willReturn( 'Provider1' );
198 $provider1->method( 'describe' )
199 ->willReturn( '#1 sessions' );
200 $provider1->method( 'unpersistSession' )
201 ->willReturnCallback( static function ( $request ) use ( &$requestUnpersist1 ) {
202 $requestUnpersist1 = true;
203 } );
205 $provider2 = $providerBuilder->getMock();
206 $provider2->method( 'provideSessionInfo' )
207 ->with( $this->identicalTo( $request ) )
208 ->willReturnCallback( static function ( $request ) use ( &$requestInfo2 ) {
209 return $requestInfo2;
210 } );
211 $provider2->method( '__toString' )
212 ->willReturn( 'Provider2' );
213 $provider2->method( 'describe' )
214 ->willReturn( '#2 sessions' );
215 $provider2->method( 'unpersistSession' )
216 ->willReturnCallback( static function ( $request ) use ( &$requestUnpersist2 ) {
217 $requestUnpersist2 = true;
218 } );
220 $this->config->set( MainConfigNames::SessionProviders, [
221 $this->objectCacheDef( $provider1 ),
222 $this->objectCacheDef( $provider2 ),
223 ] );
225 // No provider returns info
226 $session = $manager->getSessionForRequest( $request );
227 $this->assertInstanceOf( Session::class, $session );
228 $this->assertSame( $idEmpty, $session->getId() );
229 $this->assertFalse( $requestUnpersist1 );
230 $this->assertFalse( $requestUnpersist2 );
232 // Both providers return info, picks best one
233 $requestInfo1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
234 'provider' => $provider1,
235 'id' => ( $id1 = $manager->generateSessionId() ),
236 'persisted' => true,
237 'idIsSafe' => true,
238 ] );
239 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
240 'provider' => $provider2,
241 'id' => ( $id2 = $manager->generateSessionId() ),
242 'persisted' => true,
243 'idIsSafe' => true,
244 ] );
245 $session = $manager->getSessionForRequest( $request );
246 $this->assertInstanceOf( Session::class, $session );
247 $this->assertSame( $id2, $session->getId() );
248 $this->assertFalse( $requestUnpersist1 );
249 $this->assertFalse( $requestUnpersist2 );
251 $requestInfo1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
252 'provider' => $provider1,
253 'id' => ( $id1 = $manager->generateSessionId() ),
254 'persisted' => true,
255 'idIsSafe' => true,
256 ] );
257 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
258 'provider' => $provider2,
259 'id' => ( $id2 = $manager->generateSessionId() ),
260 'persisted' => true,
261 'idIsSafe' => true,
262 ] );
263 $session = $manager->getSessionForRequest( $request );
264 $this->assertInstanceOf( Session::class, $session );
265 $this->assertSame( $id1, $session->getId() );
266 $this->assertFalse( $requestUnpersist1 );
267 $this->assertFalse( $requestUnpersist2 );
269 // Tied priorities
270 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
271 'provider' => $provider1,
272 'id' => ( $id1 = $manager->generateSessionId() ),
273 'persisted' => true,
274 'userInfo' => UserInfo::newAnonymous(),
275 'idIsSafe' => true,
276 ] );
277 $requestInfo2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
278 'provider' => $provider2,
279 'id' => ( $id2 = $manager->generateSessionId() ),
280 'persisted' => true,
281 'userInfo' => UserInfo::newAnonymous(),
282 'idIsSafe' => true,
283 ] );
284 try {
285 $manager->getSessionForRequest( $request );
286 $this->fail( 'Expcected exception not thrown' );
287 } catch ( SessionOverflowException $ex ) {
288 $this->assertStringStartsWith(
289 'Multiple sessions for this request tied for top priority: ',
290 $ex->getMessage()
292 $this->assertCount( 2, $ex->getSessionInfos() );
293 $this->assertContains( $requestInfo1, $ex->getSessionInfos() );
294 $this->assertContains( $requestInfo2, $ex->getSessionInfos() );
296 $this->assertFalse( $requestUnpersist1 );
297 $this->assertFalse( $requestUnpersist2 );
299 // Bad provider
300 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
301 'provider' => $provider2,
302 'id' => ( $id1 = $manager->generateSessionId() ),
303 'persisted' => true,
304 'idIsSafe' => true,
305 ] );
306 $requestInfo2 = null;
307 try {
308 $manager->getSessionForRequest( $request );
309 $this->fail( 'Expcected exception not thrown' );
310 } catch ( UnexpectedValueException $ex ) {
311 $this->assertSame(
312 'Provider1 returned session info for a different provider: ' . $requestInfo1,
313 $ex->getMessage()
316 $this->assertFalse( $requestUnpersist1 );
317 $this->assertFalse( $requestUnpersist2 );
319 // Unusable session info
320 $this->logger->setCollect( true );
321 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
322 'provider' => $provider1,
323 'id' => ( $id1 = $manager->generateSessionId() ),
324 'persisted' => true,
325 'userInfo' => UserInfo::newFromName( 'TestGetSessionForRequest', false ),
326 'idIsSafe' => true,
327 ] );
328 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
329 'provider' => $provider2,
330 'id' => ( $id2 = $manager->generateSessionId() ),
331 'persisted' => true,
332 'idIsSafe' => true,
333 ] );
334 $session = $manager->getSessionForRequest( $request );
335 $this->assertInstanceOf( Session::class, $session );
336 $this->assertSame( $id2, $session->getId() );
337 $this->logger->setCollect( false );
338 $this->assertTrue( $requestUnpersist1 );
339 $this->assertFalse( $requestUnpersist2 );
340 $requestUnpersist1 = false;
342 $this->logger->setCollect( true );
343 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
344 'provider' => $provider1,
345 'id' => ( $id1 = $manager->generateSessionId() ),
346 'persisted' => true,
347 'idIsSafe' => true,
348 ] );
349 $requestInfo2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
350 'provider' => $provider2,
351 'id' => ( $id2 = $manager->generateSessionId() ),
352 'persisted' => true,
353 'userInfo' => UserInfo::newFromName( 'TestGetSessionForRequest', false ),
354 'idIsSafe' => true,
355 ] );
356 $session = $manager->getSessionForRequest( $request );
357 $this->assertInstanceOf( Session::class, $session );
358 $this->assertSame( $id1, $session->getId() );
359 $this->logger->setCollect( false );
360 $this->assertFalse( $requestUnpersist1 );
361 $this->assertTrue( $requestUnpersist2 );
362 $requestUnpersist2 = false;
364 // Unpersisted session ID
365 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
366 'provider' => $provider1,
367 'id' => ( $id1 = $manager->generateSessionId() ),
368 'persisted' => false,
369 'userInfo' => UserInfo::newFromName( 'TestGetSessionForRequest', true ),
370 'idIsSafe' => true,
371 ] );
372 $requestInfo2 = null;
373 $session = $manager->getSessionForRequest( $request );
374 $this->assertInstanceOf( Session::class, $session );
375 $this->assertSame( $id1, $session->getId() );
376 $this->assertTrue( $requestUnpersist1 ); // The saving of the session does it
377 $this->assertFalse( $requestUnpersist2 );
378 $session->persist();
379 $this->assertTrue( $session->isPersistent() );
382 public function testGetSessionById() {
383 $manager = $this->getManager();
384 try {
385 $manager->getSessionById( 'bad' );
386 $this->fail( 'Expected exception not thrown' );
387 } catch ( InvalidArgumentException $ex ) {
388 $this->assertSame( 'Invalid session ID', $ex->getMessage() );
391 // Unknown session ID
392 $id = $manager->generateSessionId();
393 $session = $manager->getSessionById( $id, true );
394 $this->assertInstanceOf( Session::class, $session );
395 $this->assertSame( $id, $session->getId() );
397 $id = $manager->generateSessionId();
398 $this->assertNull( $manager->getSessionById( $id, false ) );
400 $userIdentity = $this->getTestSysop()->getUserIdentity();
401 // Known but unloadable session ID
402 $this->logger->setCollect( true );
403 $id = $manager->generateSessionId();
404 $this->store->setSession( $id, [ 'metadata' => [
405 'userId' => $userIdentity->getId(),
406 'userToken' => 'bad',
407 ] ] );
409 $this->assertNull( $manager->getSessionById( $id, true ) );
410 $this->assertNull( $manager->getSessionById( $id, false ) );
411 $this->logger->setCollect( false );
413 // Known session ID
414 $this->store->setSession( $id, [] );
415 $session = $manager->getSessionById( $id, false );
416 $this->assertInstanceOf( Session::class, $session );
417 $this->assertSame( $id, $session->getId() );
419 // Store isn't checked if the session is already loaded
420 $this->store->setSession( $id, [ 'metadata' => [
421 'userId' => $userIdentity->getId(),
422 'userToken' => 'bad',
423 ] ] );
424 $session2 = $manager->getSessionById( $id, false );
425 $this->assertInstanceOf( Session::class, $session2 );
426 $this->assertSame( $id, $session2->getId() );
427 unset( $session, $session2 );
428 $this->logger->setCollect( true );
429 $this->assertNull( $manager->getSessionById( $id, true ) );
430 $this->logger->setCollect( false );
432 // Failure to create an empty session
433 $manager = $this->getManager();
434 $provider = $this->getMockBuilder( DummySessionProvider::class )
435 ->onlyMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] )
436 ->getMock();
437 $provider->method( 'provideSessionInfo' )
438 ->willReturn( null );
439 $provider->method( 'newSessionInfo' )
440 ->willReturn( null );
441 $provider->method( '__toString' )
442 ->willReturn( 'MockProvider' );
443 $this->config->set( MainConfigNames::SessionProviders, [
444 $this->objectCacheDef( $provider ),
445 ] );
446 $this->logger->setCollect( true );
447 $this->assertNull( $manager->getSessionById( $id, true ) );
448 $this->logger->setCollect( false );
449 $this->assertSame( [
450 [ LogLevel::ERROR, 'Failed to create empty session: {exception}' ]
451 ], $this->logger->getBuffer() );
454 public function testGetEmptySession() {
455 $manager = $this->getManager();
456 $pmanager = TestingAccessWrapper::newFromObject( $manager );
458 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
459 ->onlyMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] );
461 $expectId = null;
462 $info1 = null;
463 $info2 = null;
465 $provider1 = $providerBuilder->getMock();
466 $provider1->method( 'provideSessionInfo' )
467 ->willReturn( null );
468 $provider1->method( 'newSessionInfo' )
469 ->with( $this->callback( static function ( $id ) use ( &$expectId ) {
470 return $id === $expectId;
471 } ) )
472 ->willReturnCallback( static function () use ( &$info1 ) {
473 return $info1;
474 } );
475 $provider1->method( '__toString' )
476 ->willReturn( 'MockProvider1' );
478 $provider2 = $providerBuilder->getMock();
479 $provider2->method( 'provideSessionInfo' )
480 ->willReturn( null );
481 $provider2->method( 'newSessionInfo' )
482 ->with( $this->callback( static function ( $id ) use ( &$expectId ) {
483 return $id === $expectId;
484 } ) )
485 ->willReturnCallback( static function () use ( &$info2 ) {
486 return $info2;
487 } );
488 $provider1->method( '__toString' )
489 ->willReturn( 'MockProvider2' );
491 $this->config->set( MainConfigNames::SessionProviders, [
492 $this->objectCacheDef( $provider1 ),
493 $this->objectCacheDef( $provider2 ),
494 ] );
496 // No info
497 $expectId = null;
498 $info1 = null;
499 $info2 = null;
500 try {
501 $manager->getEmptySession();
502 $this->fail( 'Expected exception not thrown' );
503 } catch ( UnexpectedValueException $ex ) {
504 $this->assertSame(
505 'No provider could provide an empty session!',
506 $ex->getMessage()
510 // Info
511 $expectId = null;
512 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
513 'provider' => $provider1,
514 'id' => 'empty---------------------------',
515 'persisted' => true,
516 'idIsSafe' => true,
517 ] );
518 $info2 = null;
519 $session = $manager->getEmptySession();
520 $this->assertInstanceOf( Session::class, $session );
521 $this->assertSame( 'empty---------------------------', $session->getId() );
523 // Info, explicitly
524 $expectId = 'expected------------------------';
525 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
526 'provider' => $provider1,
527 'id' => $expectId,
528 'persisted' => true,
529 'idIsSafe' => true,
530 ] );
531 $info2 = null;
532 $session = $pmanager->getEmptySessionInternal( null, $expectId );
533 $this->assertInstanceOf( Session::class, $session );
534 $this->assertSame( $expectId, $session->getId() );
536 // Wrong ID
537 $expectId = 'expected-----------------------2';
538 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
539 'provider' => $provider1,
540 'id' => "un$expectId",
541 'persisted' => true,
542 'idIsSafe' => true,
543 ] );
544 $info2 = null;
545 try {
546 $pmanager->getEmptySessionInternal( null, $expectId );
547 $this->fail( 'Expected exception not thrown' );
548 } catch ( UnexpectedValueException $ex ) {
549 $this->assertSame(
550 'MockProvider1 returned empty session info with a wrong id: ' .
551 "un$expectId != $expectId",
552 $ex->getMessage()
556 // Unsafe ID
557 $expectId = 'expected-----------------------2';
558 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
559 'provider' => $provider1,
560 'id' => $expectId,
561 'persisted' => true,
562 ] );
563 $info2 = null;
564 try {
565 $pmanager->getEmptySessionInternal( null, $expectId );
566 $this->fail( 'Expected exception not thrown' );
567 } catch ( UnexpectedValueException $ex ) {
568 $this->assertSame(
569 'MockProvider1 returned empty session info with id flagged unsafe',
570 $ex->getMessage()
574 // Wrong provider
575 $expectId = null;
576 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
577 'provider' => $provider2,
578 'id' => 'empty---------------------------',
579 'persisted' => true,
580 'idIsSafe' => true,
581 ] );
582 $info2 = null;
583 try {
584 $manager->getEmptySession();
585 $this->fail( 'Expected exception not thrown' );
586 } catch ( UnexpectedValueException $ex ) {
587 $this->assertSame(
588 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
589 $ex->getMessage()
593 // Highest priority wins
594 $expectId = null;
595 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
596 'provider' => $provider1,
597 'id' => 'empty1--------------------------',
598 'persisted' => true,
599 'idIsSafe' => true,
600 ] );
601 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
602 'provider' => $provider2,
603 'id' => 'empty2--------------------------',
604 'persisted' => true,
605 'idIsSafe' => true,
606 ] );
607 $session = $manager->getEmptySession();
608 $this->assertInstanceOf( Session::class, $session );
609 $this->assertSame( 'empty1--------------------------', $session->getId() );
611 $expectId = null;
612 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
613 'provider' => $provider1,
614 'id' => 'empty1--------------------------',
615 'persisted' => true,
616 'idIsSafe' => true,
617 ] );
618 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
619 'provider' => $provider2,
620 'id' => 'empty2--------------------------',
621 'persisted' => true,
622 'idIsSafe' => true,
623 ] );
624 $session = $manager->getEmptySession();
625 $this->assertInstanceOf( Session::class, $session );
626 $this->assertSame( 'empty2--------------------------', $session->getId() );
628 // Tied priorities throw an exception
629 $expectId = null;
630 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
631 'provider' => $provider1,
632 'id' => 'empty1--------------------------',
633 'persisted' => true,
634 'userInfo' => UserInfo::newAnonymous(),
635 'idIsSafe' => true,
636 ] );
637 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
638 'provider' => $provider2,
639 'id' => 'empty2--------------------------',
640 'persisted' => true,
641 'userInfo' => UserInfo::newAnonymous(),
642 'idIsSafe' => true,
643 ] );
644 try {
645 $manager->getEmptySession();
646 $this->fail( 'Expected exception not thrown' );
647 } catch ( UnexpectedValueException $ex ) {
648 $this->assertStringStartsWith(
649 'Multiple empty sessions tied for top priority: ',
650 $ex->getMessage()
654 // Bad id
655 try {
656 $pmanager->getEmptySessionInternal( null, 'bad' );
657 $this->fail( 'Expected exception not thrown' );
658 } catch ( InvalidArgumentException $ex ) {
659 $this->assertSame( 'Invalid session ID', $ex->getMessage() );
662 // Session already exists
663 $expectId = 'expected-----------------------3';
664 $this->store->setSessionMeta( $expectId, [
665 'provider' => 'MockProvider2',
666 'userId' => 0,
667 'userName' => null,
668 'userToken' => null,
669 ] );
670 try {
671 $pmanager->getEmptySessionInternal( null, $expectId );
672 $this->fail( 'Expected exception not thrown' );
673 } catch ( InvalidArgumentException $ex ) {
674 $this->assertSame( 'Session ID already exists', $ex->getMessage() );
678 public function testInvalidateSessionsForUser() {
679 $user = $this->getTestSysop()->getUser();
680 $manager = $this->getManager();
682 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
683 ->onlyMethods( [ 'invalidateSessionsForUser', '__toString' ] );
685 $provider1 = $providerBuilder->getMock();
686 $provider1->expects( $this->once() )->method( 'invalidateSessionsForUser' )
687 ->with( $this->identicalTo( $user ) );
688 $provider1->method( '__toString' )
689 ->willReturn( 'MockProvider1' );
691 $provider2 = $providerBuilder->getMock();
692 $provider2->expects( $this->once() )->method( 'invalidateSessionsForUser' )
693 ->with( $this->identicalTo( $user ) );
694 $provider2->method( '__toString' )
695 ->willReturn( 'MockProvider2' );
697 $this->config->set( MainConfigNames::SessionProviders, [
698 $this->objectCacheDef( $provider1 ),
699 $this->objectCacheDef( $provider2 ),
700 ] );
702 $oldToken = $user->getToken( true );
703 $manager->invalidateSessionsForUser( $user );
704 $this->assertNotEquals( $oldToken, $user->getToken() );
707 public function testGetVaryHeaders() {
708 $manager = $this->getManager();
710 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
711 ->onlyMethods( [ 'getVaryHeaders', '__toString' ] );
713 $provider1 = $providerBuilder->getMock();
714 $provider1->expects( $this->once() )->method( 'getVaryHeaders' )
715 ->willReturn( [
716 'Foo' => null,
717 'Bar' => [ 'X', 'Bar1' ],
718 'Quux' => null,
719 ] );
720 $provider1->method( '__toString' )
721 ->willReturn( 'MockProvider1' );
723 $provider2 = $providerBuilder->getMock();
724 $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
725 ->willReturn( [
726 'Baz' => null,
727 'Bar' => [ 'X', 'Bar2' ],
728 'Quux' => [ 'Quux' ],
729 ] );
730 $provider2->method( '__toString' )
731 ->willReturn( 'MockProvider2' );
733 $this->config->set( MainConfigNames::SessionProviders, [
734 $this->objectCacheDef( $provider1 ),
735 $this->objectCacheDef( $provider2 ),
736 ] );
738 $expect = [
739 'Foo' => null,
740 'Bar' => null,
741 'Quux' => null,
742 'Baz' => null,
745 $this->assertEquals( $expect, $manager->getVaryHeaders() );
747 // Again, to ensure it's cached
748 $this->assertEquals( $expect, $manager->getVaryHeaders() );
751 public function testGetVaryCookies() {
752 $manager = $this->getManager();
754 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
755 ->onlyMethods( [ 'getVaryCookies', '__toString' ] );
757 $provider1 = $providerBuilder->getMock();
758 $provider1->expects( $this->once() )->method( 'getVaryCookies' )
759 ->willReturn( [ 'Foo', 'Bar' ] );
760 $provider1->method( '__toString' )
761 ->willReturn( 'MockProvider1' );
763 $provider2 = $providerBuilder->getMock();
764 $provider2->expects( $this->once() )->method( 'getVaryCookies' )
765 ->willReturn( [ 'Foo', 'Baz' ] );
766 $provider2->method( '__toString' )
767 ->willReturn( 'MockProvider2' );
769 $this->config->set( MainConfigNames::SessionProviders, [
770 $this->objectCacheDef( $provider1 ),
771 $this->objectCacheDef( $provider2 ),
772 ] );
774 $expect = [ 'Foo', 'Bar', 'Baz' ];
776 $this->assertEquals( $expect, $manager->getVaryCookies() );
778 // Again, to ensure it's cached
779 $this->assertEquals( $expect, $manager->getVaryCookies() );
782 public function testGetProviders() {
783 $realManager = $this->getManager();
784 $manager = TestingAccessWrapper::newFromObject( $realManager );
786 $this->config->set( MainConfigNames::SessionProviders, [
787 [ 'class' => DummySessionProvider::class ],
788 ] );
789 $providers = $manager->getProviders();
790 $this->assertArrayHasKey( 'DummySessionProvider', $providers );
791 $provider = TestingAccessWrapper::newFromObject( $providers['DummySessionProvider'] );
792 $this->assertSame( $manager->logger, $provider->logger );
793 $this->assertSame( $manager->config, $provider->getConfig() );
794 $this->assertSame( $realManager, $provider->getManager() );
796 $this->config->set( MainConfigNames::SessionProviders, [
797 [ 'class' => DummySessionProvider::class ],
798 [ 'class' => DummySessionProvider::class ],
799 ] );
800 $manager->sessionProviders = null;
801 try {
802 $manager->getProviders();
803 $this->fail( 'Expected exception not thrown' );
804 } catch ( UnexpectedValueException $ex ) {
805 $this->assertSame(
806 'Duplicate provider name "DummySessionProvider"',
807 $ex->getMessage()
812 public function testShutdown() {
813 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
814 $manager->setLogger( new NullLogger() );
816 $mock = $this->getMockBuilder( stdClass::class )
817 ->addMethods( [ 'shutdown' ] )->getMock();
818 $mock->expects( $this->once() )->method( 'shutdown' );
820 $manager->allSessionBackends = [ $mock ];
821 $manager->shutdown();
824 public function testGetSessionFromInfo() {
825 $manager = TestingAccessWrapper::newFromObject( $this->getManager() );
826 $request = new FauxRequest();
828 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
830 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
831 'provider' => $manager->getProvider( 'DummySessionProvider' ),
832 'id' => $id,
833 'persisted' => true,
834 'userInfo' => UserInfo::newFromName( 'TestGetSessionFromInfo', true ),
835 'idIsSafe' => true,
836 ] );
837 TestingAccessWrapper::newFromObject( $info )->idIsSafe = true;
838 $session1 = TestingAccessWrapper::newFromObject(
839 $manager->getSessionFromInfo( $info, $request )
841 $session2 = TestingAccessWrapper::newFromObject(
842 $manager->getSessionFromInfo( $info, $request )
845 $this->assertSame( $session1->backend, $session2->backend );
846 $this->assertNotEquals( $session1->index, $session2->index );
847 $this->assertSame( $session1->getSessionId(), $session2->getSessionId() );
848 $this->assertSame( $id, $session1->getId() );
850 TestingAccessWrapper::newFromObject( $info )->idIsSafe = false;
851 $session3 = $manager->getSessionFromInfo( $info, $request );
852 $this->assertNotSame( $id, $session3->getId() );
855 public function testBackendRegistration() {
856 $manager = $this->getManager();
858 $session = $manager->getSessionForRequest( new FauxRequest );
859 $backend = TestingAccessWrapper::newFromObject( $session )->backend;
860 $sessionId = $session->getSessionId();
861 $id = (string)$sessionId;
863 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
865 $manager->changeBackendId( $backend );
866 $this->assertSame( $sessionId, $session->getSessionId() );
867 $this->assertNotEquals( $id, (string)$sessionId );
868 $id = (string)$sessionId;
870 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
872 // Destruction of the session here causes the backend to be deregistered
873 $session = null;
875 try {
876 $manager->changeBackendId( $backend );
877 $this->fail( 'Expected exception not thrown' );
878 } catch ( InvalidArgumentException $ex ) {
879 $this->assertSame(
880 'Backend was not registered with this SessionManager', $ex->getMessage()
884 try {
885 $manager->deregisterSessionBackend( $backend );
886 $this->fail( 'Expected exception not thrown' );
887 } catch ( InvalidArgumentException $ex ) {
888 $this->assertSame(
889 'Backend was not registered with this SessionManager', $ex->getMessage()
893 $session = $manager->getSessionById( $id, true );
894 $this->assertSame( $sessionId, $session->getSessionId() );
897 public function testGenerateSessionId() {
898 $manager = $this->getManager();
900 $id = $manager->generateSessionId();
901 $this->assertTrue( SessionManager::validateSessionId( $id ), "Generated ID: $id" );
904 public function testPreventSessionsForUser() {
905 $manager = $this->getManager();
907 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
908 ->onlyMethods( [ 'preventSessionsForUser', '__toString' ] );
910 $username = 'TestPreventSessionsForUser';
911 $provider1 = $providerBuilder->getMock();
912 $provider1->expects( $this->once() )->method( 'preventSessionsForUser' )
913 ->with( $username );
914 $provider1->method( '__toString' )
915 ->willReturn( 'MockProvider1' );
917 $this->config->set( MainConfigNames::SessionProviders, [
918 $this->objectCacheDef( $provider1 ),
919 ] );
921 $this->assertFalse( $manager->isUserSessionPrevented( $username ) );
922 $manager->preventSessionsForUser( $username );
923 $this->assertTrue( $manager->isUserSessionPrevented( $username ) );
926 public function testLoadSessionInfoFromStore() {
927 $manager = $this->getManager();
928 $logger = new TestLogger( true );
929 $manager->setLogger( $logger );
930 $request = new FauxRequest();
932 // TestingAccessWrapper can't handle methods with reference arguments, sigh.
933 $rClass = new ReflectionClass( $manager );
934 $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
935 $rMethod->setAccessible( true );
936 $loadSessionInfoFromStore = static function ( &$info ) use ( $rMethod, $manager, $request ) {
937 return $rMethod->invokeArgs( $manager, [ &$info, $request ] );
940 $username = $this->getTestSysop()->getUserIdentity()->getName();
941 $userInfo = UserInfo::newFromName( $username, true );
942 $unverifiedUserInfo = UserInfo::newFromName( $username, false );
944 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
945 $metadata = [
946 'userId' => $userInfo->getId(),
947 'userName' => $userInfo->getName(),
948 'userToken' => $userInfo->getToken( true ),
949 'provider' => 'Mock',
952 $builder = $this->getMockBuilder( SessionProvider::class )
953 ->onlyMethods( [ '__toString', 'mergeMetadata', 'refreshSessionInfo' ] );
955 $provider = $builder->getMockForAbstractClass();
956 $this->initProvider( $provider, null, null, $manager );
957 $provider->method( 'persistsSessionId' )
958 ->willReturn( true );
959 $provider->method( 'canChangeUser' )
960 ->willReturn( true );
961 $provider->method( 'refreshSessionInfo' )
962 ->willReturn( true );
963 $provider->method( '__toString' )
964 ->willReturn( 'Mock' );
965 $provider->method( 'mergeMetadata' )
966 ->willReturnCallback( static function ( $a, $b ) {
967 if ( $b === [ 'Throw' ] ) {
968 throw new MetadataMergeException( 'no merge!' );
970 return [ 'Merged' ];
971 } );
973 $provider2 = $builder->getMockForAbstractClass();
974 $this->initProvider( $provider2, null, null, $manager );
975 $provider2->method( 'persistsSessionId' )
976 ->willReturn( false );
977 $provider2->method( 'canChangeUser' )
978 ->willReturn( false );
979 $provider2->method( '__toString' )
980 ->willReturn( 'Mock2' );
981 $provider2->method( 'refreshSessionInfo' )
982 ->willReturnCallback( static function ( $info, $request, &$metadata ) {
983 $metadata['changed'] = true;
984 return true;
985 } );
987 $provider3 = $builder->getMockForAbstractClass();
988 $this->initProvider( $provider3, null, null, $manager );
989 $provider3->method( 'persistsSessionId' )
990 ->willReturn( true );
991 $provider3->method( 'canChangeUser' )
992 ->willReturn( true );
993 $provider3->expects( $this->once() )->method( 'refreshSessionInfo' )
994 ->willReturn( false );
995 $provider3->method( '__toString' )
996 ->willReturn( 'Mock3' );
998 TestingAccessWrapper::newFromObject( $manager )->sessionProviders = [
999 (string)$provider => $provider,
1000 (string)$provider2 => $provider2,
1001 (string)$provider3 => $provider3,
1004 // No metadata, basic usage
1005 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1006 'provider' => $provider,
1007 'id' => $id,
1008 'userInfo' => $userInfo
1009 ] );
1010 $this->assertFalse( $info->isIdSafe() );
1011 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1012 $this->assertFalse( $info->isIdSafe() );
1013 $this->assertSame( [], $logger->getBuffer() );
1015 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1016 'provider' => $provider,
1017 'userInfo' => $userInfo
1018 ] );
1019 $this->assertTrue( $info->isIdSafe() );
1020 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1021 $this->assertTrue( $info->isIdSafe() );
1022 $this->assertSame( [], $logger->getBuffer() );
1024 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1025 'provider' => $provider2,
1026 'id' => $id,
1027 'userInfo' => $userInfo
1028 ] );
1029 $this->assertFalse( $info->isIdSafe() );
1030 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1031 $this->assertTrue( $info->isIdSafe() );
1032 $this->assertSame( [], $logger->getBuffer() );
1034 // Unverified user, no metadata
1035 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1036 'provider' => $provider,
1037 'id' => $id,
1038 'userInfo' => $unverifiedUserInfo
1039 ] );
1040 $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
1041 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1042 $this->assertSame( [
1044 LogLevel::INFO,
1045 'Session "{session}": Unverified user provided and no metadata to auth it',
1047 ], $logger->getBuffer() );
1048 $logger->clearBuffer();
1050 // No metadata, missing data
1051 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1052 'id' => $id,
1053 'userInfo' => $userInfo
1054 ] );
1055 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1056 $this->assertSame( [
1057 [ LogLevel::WARNING, 'Session "{session}": Null provider and no metadata' ],
1058 ], $logger->getBuffer() );
1059 $logger->clearBuffer();
1061 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1062 'provider' => $provider,
1063 'id' => $id,
1064 ] );
1065 $this->assertFalse( $info->isIdSafe() );
1066 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1067 $this->assertInstanceOf( UserInfo::class, $info->getUserInfo() );
1068 $this->assertTrue( $info->getUserInfo()->isVerified() );
1069 $this->assertTrue( $info->getUserInfo()->isAnon() );
1070 $this->assertFalse( $info->isIdSafe() );
1071 $this->assertSame( [], $logger->getBuffer() );
1073 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1074 'provider' => $provider2,
1075 'id' => $id,
1076 ] );
1077 $this->assertFalse( $info->isIdSafe() );
1078 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1079 $this->assertSame( [
1080 [ LogLevel::INFO, 'Session "{session}": No user provided and provider cannot set user' ]
1081 ], $logger->getBuffer() );
1082 $logger->clearBuffer();
1084 // Incomplete/bad metadata
1085 $this->store->setRawSession( $id, true );
1086 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1087 $this->assertSame( [
1088 [ LogLevel::WARNING, 'Session "{session}": Bad data' ],
1089 ], $logger->getBuffer() );
1090 $logger->clearBuffer();
1092 $this->store->setRawSession( $id, [ 'data' => [] ] );
1093 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1094 $this->assertSame( [
1095 [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1096 ], $logger->getBuffer() );
1097 $logger->clearBuffer();
1099 $this->store->deleteSession( $id );
1100 $this->store->setRawSession( $id, [ 'metadata' => $metadata ] );
1101 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1102 $this->assertSame( [
1103 [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1104 ], $logger->getBuffer() );
1105 $logger->clearBuffer();
1107 $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => true ] );
1108 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1109 $this->assertSame( [
1110 [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1111 ], $logger->getBuffer() );
1112 $logger->clearBuffer();
1114 $this->store->setRawSession( $id, [ 'metadata' => true, 'data' => [] ] );
1115 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1116 $this->assertSame( [
1117 [ LogLevel::WARNING, 'Session "{session}": Bad data structure' ],
1118 ], $logger->getBuffer() );
1119 $logger->clearBuffer();
1121 foreach ( $metadata as $key => $dummy ) {
1122 $tmp = $metadata;
1123 unset( $tmp[$key] );
1124 $this->store->setRawSession( $id, [ 'metadata' => $tmp, 'data' => [] ] );
1125 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1126 $this->assertSame( [
1127 [ LogLevel::WARNING, 'Session "{session}": Bad metadata' ],
1128 ], $logger->getBuffer() );
1129 $logger->clearBuffer();
1132 // Basic usage with metadata
1133 $this->store->setRawSession( $id, [ 'metadata' => $metadata, 'data' => [] ] );
1134 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1135 'provider' => $provider,
1136 'id' => $id,
1137 'userInfo' => $userInfo
1138 ] );
1139 $this->assertFalse( $info->isIdSafe() );
1140 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1141 $this->assertTrue( $info->isIdSafe() );
1142 $this->assertSame( [], $logger->getBuffer() );
1144 // Mismatched provider
1145 $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata );
1146 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1147 'provider' => $provider,
1148 'id' => $id,
1149 'userInfo' => $userInfo
1150 ] );
1151 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1152 $this->assertSame( [
1153 [ LogLevel::WARNING, 'Session "{session}": Wrong provider Bad !== Mock' ],
1154 ], $logger->getBuffer() );
1155 $logger->clearBuffer();
1157 // Unknown provider
1158 $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata );
1159 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1160 'id' => $id,
1161 'userInfo' => $userInfo
1162 ] );
1163 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1164 $this->assertSame( [
1165 [ LogLevel::WARNING, 'Session "{session}": Unknown provider Bad' ],
1166 ], $logger->getBuffer() );
1167 $logger->clearBuffer();
1169 // Fill in provider
1170 $this->store->setSessionMeta( $id, $metadata );
1171 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1172 'id' => $id,
1173 'userInfo' => $userInfo
1174 ] );
1175 $this->assertFalse( $info->isIdSafe() );
1176 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1177 $this->assertTrue( $info->isIdSafe() );
1178 $this->assertSame( [], $logger->getBuffer() );
1180 // Bad user metadata
1181 $this->store->setSessionMeta( $id, [ 'userId' => -1, 'userToken' => null ] + $metadata );
1182 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1183 'provider' => $provider,
1184 'id' => $id,
1185 ] );
1186 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1187 $this->assertSame( [
1188 [ LogLevel::ERROR, 'Session "{session}": {exception}' ],
1189 ], $logger->getBuffer() );
1190 $logger->clearBuffer();
1192 $this->store->setSessionMeta(
1193 $id, [ 'userId' => 0, 'userName' => '<X>', 'userToken' => null ] + $metadata
1195 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1196 'provider' => $provider,
1197 'id' => $id,
1198 ] );
1199 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1200 $this->assertSame( [
1201 [ LogLevel::ERROR, 'Session "{session}": {exception}', ],
1202 ], $logger->getBuffer() );
1203 $logger->clearBuffer();
1205 // Mismatched user by ID
1206 $this->store->setSessionMeta(
1207 $id, [ 'userId' => $userInfo->getId() + 1, 'userToken' => null ] + $metadata
1209 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1210 'provider' => $provider,
1211 'id' => $id,
1212 'userInfo' => $userInfo
1213 ] );
1214 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1215 $this->assertSame( [
1216 [ LogLevel::WARNING, 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}' ],
1217 ], $logger->getBuffer() );
1218 $logger->clearBuffer();
1220 // Mismatched user by name
1221 $this->store->setSessionMeta(
1222 $id, [ 'userId' => 0, 'userName' => 'X', 'userToken' => null ] + $metadata
1224 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1225 'provider' => $provider,
1226 'id' => $id,
1227 'userInfo' => $userInfo
1228 ] );
1229 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1230 $this->assertSame( [
1231 [ LogLevel::WARNING, 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}' ],
1232 ], $logger->getBuffer() );
1233 $logger->clearBuffer();
1235 // ID matches, name doesn't
1236 $this->store->setSessionMeta(
1237 $id, [ 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ] + $metadata
1239 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1240 'provider' => $provider,
1241 'id' => $id,
1242 'userInfo' => $userInfo
1243 ] );
1244 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1245 $this->assertSame( [
1247 LogLevel::WARNING,
1248 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}'
1250 ], $logger->getBuffer() );
1251 $logger->clearBuffer();
1253 // Mismatched anon user
1254 $this->store->setSessionMeta(
1255 $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata
1257 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1258 'provider' => $provider,
1259 'id' => $id,
1260 'userInfo' => $userInfo
1261 ] );
1262 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1263 $this->assertSame( [
1265 LogLevel::WARNING,
1266 'Session "{session}": the session store entry is for an anonymous user, ' .
1267 'but the session metadata indicates a non-anonynmous user',
1269 ], $logger->getBuffer() );
1270 $logger->clearBuffer();
1272 // Lookup user by ID
1273 $this->store->setSessionMeta( $id, [ 'userToken' => null ] + $metadata );
1274 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1275 'provider' => $provider,
1276 'id' => $id,
1277 ] );
1278 $this->assertFalse( $info->isIdSafe() );
1279 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1280 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1281 $this->assertTrue( $info->isIdSafe() );
1282 $this->assertSame( [], $logger->getBuffer() );
1284 // Lookup user by name
1285 $this->store->setSessionMeta(
1286 $id, [ 'userId' => 0, 'userName' => $username, 'userToken' => null ] + $metadata
1288 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1289 'provider' => $provider,
1290 'id' => $id,
1291 ] );
1292 $this->assertFalse( $info->isIdSafe() );
1293 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1294 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1295 $this->assertTrue( $info->isIdSafe() );
1296 $this->assertSame( [], $logger->getBuffer() );
1298 // Lookup anonymous user
1299 $this->store->setSessionMeta(
1300 $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] + $metadata
1302 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1303 'provider' => $provider,
1304 'id' => $id,
1305 ] );
1306 $this->assertFalse( $info->isIdSafe() );
1307 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1308 $this->assertTrue( $info->getUserInfo()->isAnon() );
1309 $this->assertTrue( $info->isIdSafe() );
1310 $this->assertSame( [], $logger->getBuffer() );
1312 // Unverified user with metadata
1313 $this->store->setSessionMeta( $id, $metadata );
1314 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1315 'provider' => $provider,
1316 'id' => $id,
1317 'userInfo' => $unverifiedUserInfo
1318 ] );
1319 $this->assertFalse( $info->isIdSafe() );
1320 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1321 $this->assertTrue( $info->getUserInfo()->isVerified() );
1322 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1323 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1324 $this->assertTrue( $info->isIdSafe() );
1325 $this->assertSame( [], $logger->getBuffer() );
1327 // Unverified user with metadata
1328 $this->store->setSessionMeta( $id, $metadata );
1329 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1330 'provider' => $provider,
1331 'id' => $id,
1332 'userInfo' => $unverifiedUserInfo
1333 ] );
1334 $this->assertFalse( $info->isIdSafe() );
1335 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1336 $this->assertTrue( $info->getUserInfo()->isVerified() );
1337 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1338 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1339 $this->assertTrue( $info->isIdSafe() );
1340 $this->assertSame( [], $logger->getBuffer() );
1342 // Wrong token
1343 $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
1344 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1345 'provider' => $provider,
1346 'id' => $id,
1347 'userInfo' => $userInfo
1348 ] );
1349 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1350 $this->assertSame( [
1351 [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ],
1352 ], $logger->getBuffer() );
1353 $logger->clearBuffer();
1355 // Provider metadata
1356 $this->store->setSessionMeta( $id, [ 'provider' => 'Mock2' ] + $metadata );
1357 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1358 'provider' => $provider2,
1359 'id' => $id,
1360 'userInfo' => $userInfo,
1361 'metadata' => [ 'Info' ],
1362 ] );
1363 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1364 $this->assertSame( [ 'Info', 'changed' => true ], $info->getProviderMetadata() );
1365 $this->assertSame( [], $logger->getBuffer() );
1367 $this->store->setSessionMeta( $id, [ 'providerMetadata' => [ 'Saved' ] ] + $metadata );
1368 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1369 'provider' => $provider,
1370 'id' => $id,
1371 'userInfo' => $userInfo,
1372 ] );
1373 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1374 $this->assertSame( [ 'Saved' ], $info->getProviderMetadata() );
1375 $this->assertSame( [], $logger->getBuffer() );
1377 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1378 'provider' => $provider,
1379 'id' => $id,
1380 'userInfo' => $userInfo,
1381 'metadata' => [ 'Info' ],
1382 ] );
1383 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1384 $this->assertSame( [ 'Merged' ], $info->getProviderMetadata() );
1385 $this->assertSame( [], $logger->getBuffer() );
1387 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1388 'provider' => $provider,
1389 'id' => $id,
1390 'userInfo' => $userInfo,
1391 'metadata' => [ 'Throw' ],
1392 ] );
1393 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1394 $this->assertSame( [
1396 LogLevel::WARNING,
1397 'Session "{session}": Metadata merge failed: {exception}',
1399 ], $logger->getBuffer() );
1400 $logger->clearBuffer();
1402 // Remember from session
1403 $this->store->setSessionMeta( $id, $metadata );
1404 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1405 'provider' => $provider,
1406 'id' => $id,
1407 ] );
1408 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1409 $this->assertFalse( $info->wasRemembered() );
1410 $this->assertSame( [], $logger->getBuffer() );
1412 $this->store->setSessionMeta( $id, [ 'remember' => true ] + $metadata );
1413 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1414 'provider' => $provider,
1415 'id' => $id,
1416 ] );
1417 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1418 $this->assertTrue( $info->wasRemembered() );
1419 $this->assertSame( [], $logger->getBuffer() );
1421 $this->store->setSessionMeta( $id, [ 'remember' => false ] + $metadata );
1422 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1423 'provider' => $provider,
1424 'id' => $id,
1425 'userInfo' => $userInfo
1426 ] );
1427 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1428 $this->assertTrue( $info->wasRemembered() );
1429 $this->assertSame( [], $logger->getBuffer() );
1431 // forceHTTPS from session
1432 $this->store->setSessionMeta( $id, $metadata );
1433 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1434 'provider' => $provider,
1435 'id' => $id,
1436 'userInfo' => $userInfo
1437 ] );
1438 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1439 $this->assertFalse( $info->forceHTTPS() );
1440 $this->assertSame( [], $logger->getBuffer() );
1442 $this->store->setSessionMeta( $id, [ 'forceHTTPS' => true ] + $metadata );
1443 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1444 'provider' => $provider,
1445 'id' => $id,
1446 'userInfo' => $userInfo
1447 ] );
1448 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1449 $this->assertTrue( $info->forceHTTPS() );
1450 $this->assertSame( [], $logger->getBuffer() );
1452 $this->store->setSessionMeta( $id, [ 'forceHTTPS' => false ] + $metadata );
1453 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1454 'provider' => $provider,
1455 'id' => $id,
1456 'userInfo' => $userInfo,
1457 'forceHTTPS' => true
1458 ] );
1459 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1460 $this->assertTrue( $info->forceHTTPS() );
1461 $this->assertSame( [], $logger->getBuffer() );
1463 // "Persist" flag from session
1464 $this->store->setSessionMeta( $id, $metadata );
1465 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1466 'provider' => $provider,
1467 'id' => $id,
1468 'userInfo' => $userInfo
1469 ] );
1470 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1471 $this->assertFalse( $info->wasPersisted() );
1472 $this->assertSame( [], $logger->getBuffer() );
1474 $this->store->setSessionMeta( $id, [ 'persisted' => true ] + $metadata );
1475 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1476 'provider' => $provider,
1477 'id' => $id,
1478 'userInfo' => $userInfo
1479 ] );
1480 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1481 $this->assertTrue( $info->wasPersisted() );
1482 $this->assertSame( [], $logger->getBuffer() );
1484 $this->store->setSessionMeta( $id, [ 'persisted' => false ] + $metadata );
1485 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1486 'provider' => $provider,
1487 'id' => $id,
1488 'userInfo' => $userInfo,
1489 'persisted' => true
1490 ] );
1491 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1492 $this->assertTrue( $info->wasPersisted() );
1493 $this->assertSame( [], $logger->getBuffer() );
1495 // Provider refreshSessionInfo() returning false
1496 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1497 'provider' => $provider3,
1498 ] );
1499 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1500 $this->assertSame( [], $logger->getBuffer() );
1502 // Hook
1503 $called = false;
1504 $data = [ 'foo' => 1 ];
1505 $this->store->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] );
1506 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1507 'provider' => $provider,
1508 'id' => $id,
1509 'userInfo' => $userInfo
1510 ] );
1511 $manager->setHookContainer( $this->createHookContainer( [
1512 'SessionCheckInfo' => function ( &$reason, $i, $r, $m, $d ) use (
1513 $info, $metadata, $data, $request, &$called
1515 $this->assertSame( $info->getId(), $i->getId() );
1516 $this->assertSame( $info->getProvider(), $i->getProvider() );
1517 $this->assertSame( $info->getUserInfo(), $i->getUserInfo() );
1518 $this->assertSame( $request, $r );
1519 $this->assertEquals( $metadata, $m );
1520 $this->assertEquals( $data, $d );
1521 $called = true;
1522 return false;
1524 ] ) );
1525 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1526 $this->assertTrue( $called );
1527 $this->assertSame( [
1528 [ LogLevel::WARNING, 'Session "{session}": Hook aborted' ],
1529 ], $logger->getBuffer() );
1530 $logger->clearBuffer();
1531 $manager->setHookContainer( $this->createHookContainer() );
1533 // forceUse deletes bad backend data
1534 $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
1535 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1536 'provider' => $provider,
1537 'id' => $id,
1538 'userInfo' => $userInfo,
1539 'forceUse' => true,
1540 ] );
1541 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1542 $this->assertFalse( $this->store->getSession( $id ) );
1543 $this->assertSame( [
1544 [ LogLevel::WARNING, 'Session "{session}": User token mismatch' ],
1545 ], $logger->getBuffer() );
1546 $logger->clearBuffer();
1550 * @dataProvider provideLogPotentialSessionLeakage
1552 public function testLogPotentialSessionLeakage(
1553 $ip, $mwuser, $sessionData, $expectedSessionData, $expectedLogLevel
1555 MWTimestamp::setFakeTime( 1234567 );
1556 $this->overrideConfigValue( MainConfigNames::SuspiciousIpExpiry, 600 );
1557 $manager = new SessionManager();
1558 $logger = $this->createMock( LoggerInterface::class );
1559 $this->setLogger( 'session-ip', $logger );
1560 $request = new FauxRequest();
1561 $request->setIP( $ip );
1562 $request->setCookie( 'mwuser-sessionId', $mwuser );
1564 $proxyLookup = $this->createMock( ProxyLookup::class );
1565 $proxyLookup->method( 'isConfiguredProxy' )->willReturnCallback( static function ( $ip ) {
1566 return $ip === '11.22.33.44';
1567 } );
1568 $this->setService( 'ProxyLookup', $proxyLookup );
1570 $session = $this->createMock( Session::class );
1571 $session->method( 'isPersistent' )->willReturn( true );
1572 $session->method( 'getUser' )->willReturn( $this->getTestSysop()->getUser() );
1573 $session->method( 'getRequest' )->willReturn( $request );
1574 $session->method( 'getProvider' )->willReturn(
1575 $this->createMock( CookieSessionProvider::class ) );
1576 $session->method( 'get' )
1577 ->with( 'SessionManager-logPotentialSessionLeakage' )
1578 ->willReturn( $sessionData );
1579 $session->expects( $this->exactly( isset( $expectedSessionData ) ) )->method( 'set' )
1580 ->with( 'SessionManager-logPotentialSessionLeakage', $expectedSessionData );
1582 $logger->expects( $this->exactly( isset( $expectedLogLevel ) ) )->method( 'log' )
1583 ->with( $expectedLogLevel );
1585 $manager->logPotentialSessionLeakage( $session );
1588 public static function provideLogPotentialSessionLeakage() {
1589 $now = 1234567;
1590 $valid = $now - 100;
1591 $expired = $now - 1000;
1592 return [
1593 'no log for new IP' => [
1594 'ip' => '1.2.3.4',
1595 'mwuser' => null,
1596 'sessionData' => [],
1597 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1598 'expectedLogLevel' => null,
1600 'no log for same IP' => [
1601 'ip' => '1.2.3.4',
1602 'mwuser' => null,
1603 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $valid ],
1604 'expectedSessionData' => null,
1605 'expectedLogLevel' => null,
1607 'no log for expired IP' => [
1608 'ip' => '1.2.3.4',
1609 'mwuser' => null,
1610 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => null, 'timestamp' => $expired ],
1611 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1612 'expectedLogLevel' => null,
1614 'INFO log for changed IP' => [
1615 'ip' => '1.2.3.4',
1616 'mwuser' => null,
1617 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => null, 'timestamp' => $valid ],
1618 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1619 'expectedLogLevel' => LogLevel::INFO,
1622 'no log for new mwuser' => [
1623 'ip' => '1.2.3.4',
1624 'mwuser' => 'new',
1625 'sessionData' => [],
1626 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1627 'expectedLogLevel' => null,
1629 'no log for same mwuser' => [
1630 'ip' => '1.2.3.4',
1631 'mwuser' => 'old',
1632 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1633 'expectedSessionData' => null,
1634 'expectedLogLevel' => null,
1636 'NOTICE log for changed mwuser' => [
1637 'ip' => '1.2.3.4',
1638 'mwuser' => 'new',
1639 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1640 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1641 'expectedLogLevel' => LogLevel::NOTICE,
1643 'no expiration for mwuser' => [
1644 'ip' => '1.2.3.4',
1645 'mwuser' => 'new',
1646 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $expired ],
1647 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1648 'expectedLogLevel' => LogLevel::NOTICE,
1650 'WARNING log for changed IP + mwuser' => [
1651 'ip' => '1.2.3.4',
1652 'mwuser' => 'new',
1653 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1654 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1655 'expectedLogLevel' => LogLevel::WARNING,
1658 'special IPs are ignored (1)' => [
1659 'ip' => '127.0.0.1',
1660 'mwuser' => 'new',
1661 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1662 'expectedSessionData' => null,
1663 'expectedLogLevel' => null,
1665 'special IPs are ignored (2)' => [
1666 'ip' => '11.22.33.44',
1667 'mwuser' => 'new',
1668 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1669 'expectedSessionData' => null,
1670 'expectedLogLevel' => null,