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
;
25 use Psr\Log\NullLogger
;
30 use UnexpectedValueException
;
31 use Wikimedia\ScopedCallback
;
32 use Wikimedia\TestingAccessWrapper
;
37 * @covers \MediaWiki\Session\SessionManager
39 class SessionManagerTest
extends MediaWikiIntegrationTestCase
{
40 use SessionProviderTestTrait
;
42 private HashConfig
$config;
43 private TestLogger
$logger;
44 private TestBagOStuff
$store;
46 protected function getManager() {
47 $this->store
= new TestBagOStuff();
48 $cacheType = $this->setMainCache( $this->store
);
50 $this->config
= new HashConfig( [
51 MainConfigNames
::LanguageCode
=> 'en',
52 MainConfigNames
::SessionCacheType
=> $cacheType,
53 MainConfigNames
::ObjectCacheSessionExpiry
=> 100,
54 MainConfigNames
::SessionProviders
=> [
55 [ 'class' => DummySessionProvider
::class ],
58 $this->logger
= new TestLogger( false, static function ( $m ) {
59 return ( str_starts_with( $m, 'SessionBackend ' )
60 ||
str_starts_with( $m, 'SessionManager using store ' )
61 // These were added for T264793 and behave somewhat erratically, not worth testing
62 ||
str_starts_with( $m, 'Failed to load session, unpersisting' )
63 ||
preg_match( '/^(Persisting|Unpersisting) session (for|due to)/', $m )
67 return new SessionManager( [
68 'config' => $this->config
,
69 'logger' => $this->logger
,
70 'store' => $this->store
,
74 protected function objectCacheDef( $object ) {
75 return [ 'factory' => static function () use ( $object ) {
80 public function testSingleton() {
81 $reset = TestUtils
::setSessionManagerSingleton( null );
83 $singleton = SessionManager
::singleton();
84 $this->assertInstanceOf( SessionManager
::class, $singleton );
85 $this->assertSame( $singleton, SessionManager
::singleton() );
88 public function testGetGlobalSession() {
89 $context = RequestContext
::getMain();
91 if ( !PHPSessionHandler
::isInstalled() ) {
92 PHPSessionHandler
::install( SessionManager
::singleton() );
94 $staticAccess = TestingAccessWrapper
::newFromClass( PHPSessionHandler
::class );
95 $handler = TestingAccessWrapper
::newFromObject( $staticAccess->instance
);
96 $oldEnable = $handler->enable
;
97 $reset[] = new ScopedCallback( static function () use ( $handler, $oldEnable ) {
98 if ( $handler->enable
) {
99 session_write_close();
101 $handler->enable
= $oldEnable;
103 $reset[] = TestUtils
::setSessionManagerSingleton( $this->getManager() );
105 $handler->enable
= true;
106 $request = new FauxRequest();
107 $context->setRequest( $request );
108 $id = $request->getSession()->getId();
110 session_write_close();
112 $session = SessionManager
::getGlobalSession();
113 $this->assertSame( $id, $session->getId() );
115 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
116 $session = SessionManager
::getGlobalSession();
117 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $session->getId() );
118 $this->assertSame( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', $request->getSession()->getId() );
120 session_write_close();
121 $handler->enable
= false;
122 $request = new FauxRequest();
123 $context->setRequest( $request );
124 $id = $request->getSession()->getId();
127 $session = SessionManager
::getGlobalSession();
128 $this->assertSame( $id, $session->getId() );
130 session_id( 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' );
131 $session = SessionManager
::getGlobalSession();
132 $this->assertSame( $id, $session->getId() );
133 $this->assertSame( $id, $request->getSession()->getId() );
136 public function testConstructor() {
137 $manager = TestingAccessWrapper
::newFromObject( $this->getManager() );
138 $this->assertSame( $this->config
, $manager->config
);
139 $this->assertSame( $this->logger
, $manager->logger
);
140 $this->assertSame( $this->store
, $manager->store
);
142 $manager = TestingAccessWrapper
::newFromObject( new SessionManager() );
143 $this->assertSame( $this->getServiceContainer()->getMainConfig(), $manager->config
);
145 $manager = TestingAccessWrapper
::newFromObject( new SessionManager( [
146 'config' => $this->config
,
147 'store' => $this->store
,
149 $this->assertSame( $this->store
, $manager->store
);
152 'config' => 'MediaWiki\Config\Config',
153 'logger' => 'Psr\Log\LoggerInterface',
154 'store' => 'Wikimedia\ObjectCache\BagOStuff',
155 ] as $key => $error ) {
157 new SessionManager( [ $key => new stdClass
] );
158 $this->fail( 'Expected exception not thrown' );
159 } catch ( TypeError
$ex ) {
160 $this->assertStringContainsString( $error, $ex->getMessage() );
165 public function testGetSessionForRequest() {
166 $manager = $this->getManager();
167 $request = new FauxRequest();
168 $requestUnpersist1 = false;
169 $requestUnpersist2 = false;
170 $requestInfo1 = null;
171 $requestInfo2 = null;
175 $idEmpty = 'empty-session-------------------';
177 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
179 [ 'provideSessionInfo', 'newSessionInfo', '__toString', 'describe', 'unpersistSession' ]
182 $provider1 = $providerBuilder->getMock();
183 $provider1->method( 'provideSessionInfo' )
184 ->with( $this->identicalTo( $request ) )
185 ->willReturnCallback( static function ( $request ) use ( &$requestInfo1 ) {
186 return $requestInfo1;
188 $provider1->method( 'newSessionInfo' )
189 ->willReturnCallback( static function () use ( $idEmpty, $provider1 ) {
190 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
191 'provider' => $provider1,
197 $provider1->method( '__toString' )
198 ->willReturn( 'Provider1' );
199 $provider1->method( 'describe' )
200 ->willReturn( '#1 sessions' );
201 $provider1->method( 'unpersistSession' )
202 ->willReturnCallback( static function ( $request ) use ( &$requestUnpersist1 ) {
203 $requestUnpersist1 = true;
206 $provider2 = $providerBuilder->getMock();
207 $provider2->method( 'provideSessionInfo' )
208 ->with( $this->identicalTo( $request ) )
209 ->willReturnCallback( static function ( $request ) use ( &$requestInfo2 ) {
210 return $requestInfo2;
212 $provider2->method( '__toString' )
213 ->willReturn( 'Provider2' );
214 $provider2->method( 'describe' )
215 ->willReturn( '#2 sessions' );
216 $provider2->method( 'unpersistSession' )
217 ->willReturnCallback( static function ( $request ) use ( &$requestUnpersist2 ) {
218 $requestUnpersist2 = true;
221 $this->config
->set( MainConfigNames
::SessionProviders
, [
222 $this->objectCacheDef( $provider1 ),
223 $this->objectCacheDef( $provider2 ),
226 // No provider returns info
227 $session = $manager->getSessionForRequest( $request );
228 $this->assertInstanceOf( Session
::class, $session );
229 $this->assertSame( $idEmpty, $session->getId() );
230 $this->assertFalse( $requestUnpersist1 );
231 $this->assertFalse( $requestUnpersist2 );
233 // Both providers return info, picks best one
234 $requestInfo1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
235 'provider' => $provider1,
236 'id' => ( $id1 = $manager->generateSessionId() ),
240 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
2, [
241 'provider' => $provider2,
242 'id' => ( $id2 = $manager->generateSessionId() ),
246 $session = $manager->getSessionForRequest( $request );
247 $this->assertInstanceOf( Session
::class, $session );
248 $this->assertSame( $id2, $session->getId() );
249 $this->assertFalse( $requestUnpersist1 );
250 $this->assertFalse( $requestUnpersist2 );
252 $requestInfo1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
2, [
253 'provider' => $provider1,
254 'id' => ( $id1 = $manager->generateSessionId() ),
258 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
259 'provider' => $provider2,
260 'id' => ( $id2 = $manager->generateSessionId() ),
264 $session = $manager->getSessionForRequest( $request );
265 $this->assertInstanceOf( Session
::class, $session );
266 $this->assertSame( $id1, $session->getId() );
267 $this->assertFalse( $requestUnpersist1 );
268 $this->assertFalse( $requestUnpersist2 );
271 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
272 'provider' => $provider1,
273 'id' => ( $id1 = $manager->generateSessionId() ),
275 'userInfo' => UserInfo
::newAnonymous(),
278 $requestInfo2 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
279 'provider' => $provider2,
280 'id' => ( $id2 = $manager->generateSessionId() ),
282 'userInfo' => UserInfo
::newAnonymous(),
286 $manager->getSessionForRequest( $request );
287 $this->fail( 'Expcected exception not thrown' );
288 } catch ( SessionOverflowException
$ex ) {
289 $this->assertStringStartsWith(
290 'Multiple sessions for this request tied for top priority: ',
293 $this->assertCount( 2, $ex->getSessionInfos() );
294 $this->assertContains( $requestInfo1, $ex->getSessionInfos() );
295 $this->assertContains( $requestInfo2, $ex->getSessionInfos() );
297 $this->assertFalse( $requestUnpersist1 );
298 $this->assertFalse( $requestUnpersist2 );
301 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
302 'provider' => $provider2,
303 'id' => ( $id1 = $manager->generateSessionId() ),
307 $requestInfo2 = null;
309 $manager->getSessionForRequest( $request );
310 $this->fail( 'Expcected exception not thrown' );
311 } catch ( UnexpectedValueException
$ex ) {
313 'Provider1 returned session info for a different provider: ' . $requestInfo1,
317 $this->assertFalse( $requestUnpersist1 );
318 $this->assertFalse( $requestUnpersist2 );
320 // Unusable session info
321 $this->logger
->setCollect( true );
322 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
323 'provider' => $provider1,
324 'id' => ( $id1 = $manager->generateSessionId() ),
326 'userInfo' => UserInfo
::newFromName( 'TestGetSessionForRequest', false ),
329 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
330 'provider' => $provider2,
331 'id' => ( $id2 = $manager->generateSessionId() ),
335 $session = $manager->getSessionForRequest( $request );
336 $this->assertInstanceOf( Session
::class, $session );
337 $this->assertSame( $id2, $session->getId() );
338 $this->logger
->setCollect( false );
339 $this->assertTrue( $requestUnpersist1 );
340 $this->assertFalse( $requestUnpersist2 );
341 $requestUnpersist1 = false;
343 $this->logger
->setCollect( true );
344 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
345 'provider' => $provider1,
346 'id' => ( $id1 = $manager->generateSessionId() ),
350 $requestInfo2 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
351 'provider' => $provider2,
352 'id' => ( $id2 = $manager->generateSessionId() ),
354 'userInfo' => UserInfo
::newFromName( 'TestGetSessionForRequest', false ),
357 $session = $manager->getSessionForRequest( $request );
358 $this->assertInstanceOf( Session
::class, $session );
359 $this->assertSame( $id1, $session->getId() );
360 $this->logger
->setCollect( false );
361 $this->assertFalse( $requestUnpersist1 );
362 $this->assertTrue( $requestUnpersist2 );
363 $requestUnpersist2 = false;
365 // Unpersisted session ID
366 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
367 'provider' => $provider1,
368 'id' => ( $id1 = $manager->generateSessionId() ),
369 'persisted' => false,
370 'userInfo' => UserInfo
::newFromName( 'TestGetSessionForRequest', true ),
373 $requestInfo2 = null;
374 $session = $manager->getSessionForRequest( $request );
375 $this->assertInstanceOf( Session
::class, $session );
376 $this->assertSame( $id1, $session->getId() );
377 $this->assertTrue( $requestUnpersist1 ); // The saving of the session does it
378 $this->assertFalse( $requestUnpersist2 );
380 $this->assertTrue( $session->isPersistent() );
383 public function testGetSessionById() {
384 $manager = $this->getManager();
386 $manager->getSessionById( 'bad' );
387 $this->fail( 'Expected exception not thrown' );
388 } catch ( InvalidArgumentException
$ex ) {
389 $this->assertSame( 'Invalid session ID', $ex->getMessage() );
392 // Unknown session ID
393 $id = $manager->generateSessionId();
394 $session = $manager->getSessionById( $id, true );
395 $this->assertInstanceOf( Session
::class, $session );
396 $this->assertSame( $id, $session->getId() );
398 $id = $manager->generateSessionId();
399 $this->assertNull( $manager->getSessionById( $id, false ) );
401 $userIdentity = $this->getTestSysop()->getUserIdentity();
402 // Known but unloadable session ID
403 $this->logger
->setCollect( true );
404 $id = $manager->generateSessionId();
405 $this->store
->setSession( $id, [ 'metadata' => [
406 'userId' => $userIdentity->getId(),
407 'userToken' => 'bad',
410 $this->assertNull( $manager->getSessionById( $id, true ) );
411 $this->assertNull( $manager->getSessionById( $id, false ) );
412 $this->logger
->setCollect( false );
415 $this->store
->setSession( $id, [] );
416 $session = $manager->getSessionById( $id, false );
417 $this->assertInstanceOf( Session
::class, $session );
418 $this->assertSame( $id, $session->getId() );
420 // Store isn't checked if the session is already loaded
421 $this->store
->setSession( $id, [ 'metadata' => [
422 'userId' => $userIdentity->getId(),
423 'userToken' => 'bad',
425 $session2 = $manager->getSessionById( $id, false );
426 $this->assertInstanceOf( Session
::class, $session2 );
427 $this->assertSame( $id, $session2->getId() );
428 unset( $session, $session2 );
429 $this->logger
->setCollect( true );
430 $this->assertNull( $manager->getSessionById( $id, true ) );
431 $this->logger
->setCollect( false );
433 // Failure to create an empty session
434 $manager = $this->getManager();
435 $provider = $this->getMockBuilder( DummySessionProvider
::class )
436 ->onlyMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] )
438 $provider->method( 'provideSessionInfo' )
439 ->willReturn( null );
440 $provider->method( 'newSessionInfo' )
441 ->willReturn( null );
442 $provider->method( '__toString' )
443 ->willReturn( 'MockProvider' );
444 $this->config
->set( MainConfigNames
::SessionProviders
, [
445 $this->objectCacheDef( $provider ),
447 $this->logger
->setCollect( true );
448 $this->assertNull( $manager->getSessionById( $id, true ) );
449 $this->logger
->setCollect( false );
451 [ LogLevel
::ERROR
, 'Failed to create empty session: {exception}' ]
452 ], $this->logger
->getBuffer() );
455 public function testGetEmptySession() {
456 $manager = $this->getManager();
457 $pmanager = TestingAccessWrapper
::newFromObject( $manager );
459 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
460 ->onlyMethods( [ 'provideSessionInfo', 'newSessionInfo', '__toString' ] );
466 $provider1 = $providerBuilder->getMock();
467 $provider1->method( 'provideSessionInfo' )
468 ->willReturn( null );
469 $provider1->method( 'newSessionInfo' )
470 ->with( $this->callback( static function ( $id ) use ( &$expectId ) {
471 return $id === $expectId;
473 ->willReturnCallback( static function () use ( &$info1 ) {
476 $provider1->method( '__toString' )
477 ->willReturn( 'MockProvider1' );
479 $provider2 = $providerBuilder->getMock();
480 $provider2->method( 'provideSessionInfo' )
481 ->willReturn( null );
482 $provider2->method( 'newSessionInfo' )
483 ->with( $this->callback( static function ( $id ) use ( &$expectId ) {
484 return $id === $expectId;
486 ->willReturnCallback( static function () use ( &$info2 ) {
489 $provider1->method( '__toString' )
490 ->willReturn( 'MockProvider2' );
492 $this->config
->set( MainConfigNames
::SessionProviders
, [
493 $this->objectCacheDef( $provider1 ),
494 $this->objectCacheDef( $provider2 ),
502 $manager->getEmptySession();
503 $this->fail( 'Expected exception not thrown' );
504 } catch ( UnexpectedValueException
$ex ) {
506 'No provider could provide an empty session!',
513 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
514 'provider' => $provider1,
515 'id' => 'empty---------------------------',
520 $session = $manager->getEmptySession();
521 $this->assertInstanceOf( Session
::class, $session );
522 $this->assertSame( 'empty---------------------------', $session->getId() );
525 $expectId = 'expected------------------------';
526 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
527 'provider' => $provider1,
533 $session = $pmanager->getEmptySessionInternal( null, $expectId );
534 $this->assertInstanceOf( Session
::class, $session );
535 $this->assertSame( $expectId, $session->getId() );
538 $expectId = 'expected-----------------------2';
539 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
540 'provider' => $provider1,
541 'id' => "un$expectId",
547 $pmanager->getEmptySessionInternal( null, $expectId );
548 $this->fail( 'Expected exception not thrown' );
549 } catch ( UnexpectedValueException
$ex ) {
551 'MockProvider1 returned empty session info with a wrong id: ' .
552 "un$expectId != $expectId",
558 $expectId = 'expected-----------------------2';
559 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
560 'provider' => $provider1,
566 $pmanager->getEmptySessionInternal( null, $expectId );
567 $this->fail( 'Expected exception not thrown' );
568 } catch ( UnexpectedValueException
$ex ) {
570 'MockProvider1 returned empty session info with id flagged unsafe',
577 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
578 'provider' => $provider2,
579 'id' => 'empty---------------------------',
585 $manager->getEmptySession();
586 $this->fail( 'Expected exception not thrown' );
587 } catch ( UnexpectedValueException
$ex ) {
589 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
594 // Highest priority wins
596 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
597 'provider' => $provider1,
598 'id' => 'empty1--------------------------',
602 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
603 'provider' => $provider2,
604 'id' => 'empty2--------------------------',
608 $session = $manager->getEmptySession();
609 $this->assertInstanceOf( Session
::class, $session );
610 $this->assertSame( 'empty1--------------------------', $session->getId() );
613 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
614 'provider' => $provider1,
615 'id' => 'empty1--------------------------',
619 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
2, [
620 'provider' => $provider2,
621 'id' => 'empty2--------------------------',
625 $session = $manager->getEmptySession();
626 $this->assertInstanceOf( Session
::class, $session );
627 $this->assertSame( 'empty2--------------------------', $session->getId() );
629 // Tied priorities throw an exception
631 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
632 'provider' => $provider1,
633 'id' => 'empty1--------------------------',
635 'userInfo' => UserInfo
::newAnonymous(),
638 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
639 'provider' => $provider2,
640 'id' => 'empty2--------------------------',
642 'userInfo' => UserInfo
::newAnonymous(),
646 $manager->getEmptySession();
647 $this->fail( 'Expected exception not thrown' );
648 } catch ( UnexpectedValueException
$ex ) {
649 $this->assertStringStartsWith(
650 'Multiple empty sessions tied for top priority: ',
657 $pmanager->getEmptySessionInternal( null, 'bad' );
658 $this->fail( 'Expected exception not thrown' );
659 } catch ( InvalidArgumentException
$ex ) {
660 $this->assertSame( 'Invalid session ID', $ex->getMessage() );
663 // Session already exists
664 $expectId = 'expected-----------------------3';
665 $this->store
->setSessionMeta( $expectId, [
666 'provider' => 'MockProvider2',
672 $pmanager->getEmptySessionInternal( null, $expectId );
673 $this->fail( 'Expected exception not thrown' );
674 } catch ( InvalidArgumentException
$ex ) {
675 $this->assertSame( 'Session ID already exists', $ex->getMessage() );
679 public function testInvalidateSessionsForUser() {
680 $user = $this->getTestSysop()->getUser();
681 $manager = $this->getManager();
683 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
684 ->onlyMethods( [ 'invalidateSessionsForUser', '__toString' ] );
686 $provider1 = $providerBuilder->getMock();
687 $provider1->expects( $this->once() )->method( 'invalidateSessionsForUser' )
688 ->with( $this->identicalTo( $user ) );
689 $provider1->method( '__toString' )
690 ->willReturn( 'MockProvider1' );
692 $provider2 = $providerBuilder->getMock();
693 $provider2->expects( $this->once() )->method( 'invalidateSessionsForUser' )
694 ->with( $this->identicalTo( $user ) );
695 $provider2->method( '__toString' )
696 ->willReturn( 'MockProvider2' );
698 $this->config
->set( MainConfigNames
::SessionProviders
, [
699 $this->objectCacheDef( $provider1 ),
700 $this->objectCacheDef( $provider2 ),
703 $oldToken = $user->getToken( true );
704 $manager->invalidateSessionsForUser( $user );
705 $this->assertNotEquals( $oldToken, $user->getToken() );
708 public function testGetVaryHeaders() {
709 $manager = $this->getManager();
711 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
712 ->onlyMethods( [ 'getVaryHeaders', '__toString' ] );
714 $provider1 = $providerBuilder->getMock();
715 $provider1->expects( $this->once() )->method( 'getVaryHeaders' )
718 'Bar' => [ 'X', 'Bar1' ],
721 $provider1->method( '__toString' )
722 ->willReturn( 'MockProvider1' );
724 $provider2 = $providerBuilder->getMock();
725 $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
728 'Bar' => [ 'X', 'Bar2' ],
729 'Quux' => [ 'Quux' ],
731 $provider2->method( '__toString' )
732 ->willReturn( 'MockProvider2' );
734 $this->config
->set( MainConfigNames
::SessionProviders
, [
735 $this->objectCacheDef( $provider1 ),
736 $this->objectCacheDef( $provider2 ),
746 $this->assertEquals( $expect, $manager->getVaryHeaders() );
748 // Again, to ensure it's cached
749 $this->assertEquals( $expect, $manager->getVaryHeaders() );
752 public function testGetVaryCookies() {
753 $manager = $this->getManager();
755 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
756 ->onlyMethods( [ 'getVaryCookies', '__toString' ] );
758 $provider1 = $providerBuilder->getMock();
759 $provider1->expects( $this->once() )->method( 'getVaryCookies' )
760 ->willReturn( [ 'Foo', 'Bar' ] );
761 $provider1->method( '__toString' )
762 ->willReturn( 'MockProvider1' );
764 $provider2 = $providerBuilder->getMock();
765 $provider2->expects( $this->once() )->method( 'getVaryCookies' )
766 ->willReturn( [ 'Foo', 'Baz' ] );
767 $provider2->method( '__toString' )
768 ->willReturn( 'MockProvider2' );
770 $this->config
->set( MainConfigNames
::SessionProviders
, [
771 $this->objectCacheDef( $provider1 ),
772 $this->objectCacheDef( $provider2 ),
775 $expect = [ 'Foo', 'Bar', 'Baz' ];
777 $this->assertEquals( $expect, $manager->getVaryCookies() );
779 // Again, to ensure it's cached
780 $this->assertEquals( $expect, $manager->getVaryCookies() );
783 public function testGetProviders() {
784 $realManager = $this->getManager();
785 $manager = TestingAccessWrapper
::newFromObject( $realManager );
787 $this->config
->set( MainConfigNames
::SessionProviders
, [
788 [ 'class' => DummySessionProvider
::class ],
790 $providers = $manager->getProviders();
791 $this->assertArrayHasKey( 'DummySessionProvider', $providers );
792 $provider = TestingAccessWrapper
::newFromObject( $providers['DummySessionProvider'] );
793 $this->assertSame( $manager->logger
, $provider->logger
);
794 $this->assertSame( $manager->config
, $provider->getConfig() );
795 $this->assertSame( $realManager, $provider->getManager() );
797 $this->config
->set( MainConfigNames
::SessionProviders
, [
798 [ 'class' => DummySessionProvider
::class ],
799 [ 'class' => DummySessionProvider
::class ],
801 $manager->sessionProviders
= null;
803 $manager->getProviders();
804 $this->fail( 'Expected exception not thrown' );
805 } catch ( UnexpectedValueException
$ex ) {
807 'Duplicate provider name "DummySessionProvider"',
813 public function testShutdown() {
814 $manager = TestingAccessWrapper
::newFromObject( $this->getManager() );
815 $manager->setLogger( new NullLogger() );
817 $mock = $this->getMockBuilder( stdClass
::class )
818 ->addMethods( [ 'shutdown' ] )->getMock();
819 $mock->expects( $this->once() )->method( 'shutdown' );
821 $manager->allSessionBackends
= [ $mock ];
822 $manager->shutdown();
825 public function testGetSessionFromInfo() {
826 $manager = TestingAccessWrapper
::newFromObject( $this->getManager() );
827 $request = new FauxRequest();
829 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
831 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
832 'provider' => $manager->getProvider( 'DummySessionProvider' ),
835 'userInfo' => UserInfo
::newFromName( 'TestGetSessionFromInfo', true ),
838 TestingAccessWrapper
::newFromObject( $info )->idIsSafe
= true;
839 $session1 = TestingAccessWrapper
::newFromObject(
840 $manager->getSessionFromInfo( $info, $request )
842 $session2 = TestingAccessWrapper
::newFromObject(
843 $manager->getSessionFromInfo( $info, $request )
846 $this->assertSame( $session1->backend
, $session2->backend
);
847 $this->assertNotEquals( $session1->index
, $session2->index
);
848 $this->assertSame( $session1->getSessionId(), $session2->getSessionId() );
849 $this->assertSame( $id, $session1->getId() );
851 TestingAccessWrapper
::newFromObject( $info )->idIsSafe
= false;
852 $session3 = $manager->getSessionFromInfo( $info, $request );
853 $this->assertNotSame( $id, $session3->getId() );
856 public function testBackendRegistration() {
857 $manager = $this->getManager();
859 $session = $manager->getSessionForRequest( new FauxRequest
);
860 $backend = TestingAccessWrapper
::newFromObject( $session )->backend
;
861 $sessionId = $session->getSessionId();
862 $id = (string)$sessionId;
864 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
866 $manager->changeBackendId( $backend );
867 $this->assertSame( $sessionId, $session->getSessionId() );
868 $this->assertNotEquals( $id, (string)$sessionId );
869 $id = (string)$sessionId;
871 $this->assertSame( $sessionId, $manager->getSessionById( $id, true )->getSessionId() );
873 // Destruction of the session here causes the backend to be deregistered
877 $manager->changeBackendId( $backend );
878 $this->fail( 'Expected exception not thrown' );
879 } catch ( InvalidArgumentException
$ex ) {
881 'Backend was not registered with this SessionManager', $ex->getMessage()
886 $manager->deregisterSessionBackend( $backend );
887 $this->fail( 'Expected exception not thrown' );
888 } catch ( InvalidArgumentException
$ex ) {
890 'Backend was not registered with this SessionManager', $ex->getMessage()
894 $session = $manager->getSessionById( $id, true );
895 $this->assertSame( $sessionId, $session->getSessionId() );
898 public function testGenerateSessionId() {
899 $manager = $this->getManager();
901 $id = $manager->generateSessionId();
902 $this->assertTrue( SessionManager
::validateSessionId( $id ), "Generated ID: $id" );
905 public function testPreventSessionsForUser() {
906 $manager = $this->getManager();
908 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
909 ->onlyMethods( [ 'preventSessionsForUser', '__toString' ] );
911 $username = 'TestPreventSessionsForUser';
912 $provider1 = $providerBuilder->getMock();
913 $provider1->expects( $this->once() )->method( 'preventSessionsForUser' )
915 $provider1->method( '__toString' )
916 ->willReturn( 'MockProvider1' );
918 $this->config
->set( MainConfigNames
::SessionProviders
, [
919 $this->objectCacheDef( $provider1 ),
922 $this->assertFalse( $manager->isUserSessionPrevented( $username ) );
923 $manager->preventSessionsForUser( $username );
924 $this->assertTrue( $manager->isUserSessionPrevented( $username ) );
927 public function testLoadSessionInfoFromStore() {
928 $manager = $this->getManager();
929 $logger = new TestLogger( true );
930 $manager->setLogger( $logger );
931 $request = new FauxRequest();
933 // TestingAccessWrapper can't handle methods with reference arguments, sigh.
934 $rClass = new ReflectionClass( $manager );
935 $rMethod = $rClass->getMethod( 'loadSessionInfoFromStore' );
936 $rMethod->setAccessible( true );
937 $loadSessionInfoFromStore = static function ( &$info ) use ( $rMethod, $manager, $request ) {
938 return $rMethod->invokeArgs( $manager, [ &$info, $request ] );
941 $username = $this->getTestSysop()->getUserIdentity()->getName();
942 $userInfo = UserInfo
::newFromName( $username, true );
943 $unverifiedUserInfo = UserInfo
::newFromName( $username, false );
945 $id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
947 'userId' => $userInfo->getId(),
948 'userName' => $userInfo->getName(),
949 'userToken' => $userInfo->getToken( true ),
950 'provider' => 'Mock',
953 $builder = $this->getMockBuilder( SessionProvider
::class )
954 ->onlyMethods( [ '__toString', 'mergeMetadata', 'refreshSessionInfo' ] );
956 $provider = $builder->getMockForAbstractClass();
957 $this->initProvider( $provider, null, null, $manager );
958 $provider->method( 'persistsSessionId' )
959 ->willReturn( true );
960 $provider->method( 'canChangeUser' )
961 ->willReturn( true );
962 $provider->method( 'refreshSessionInfo' )
963 ->willReturn( true );
964 $provider->method( '__toString' )
965 ->willReturn( 'Mock' );
966 $provider->method( 'mergeMetadata' )
967 ->willReturnCallback( static function ( $a, $b ) {
968 if ( $b === [ 'Throw' ] ) {
969 throw new MetadataMergeException( 'no merge!' );
974 $provider2 = $builder->getMockForAbstractClass();
975 $this->initProvider( $provider2, null, null, $manager );
976 $provider2->method( 'persistsSessionId' )
977 ->willReturn( false );
978 $provider2->method( 'canChangeUser' )
979 ->willReturn( false );
980 $provider2->method( '__toString' )
981 ->willReturn( 'Mock2' );
982 $provider2->method( 'refreshSessionInfo' )
983 ->willReturnCallback( static function ( $info, $request, &$metadata ) {
984 $metadata['changed'] = true;
988 $provider3 = $builder->getMockForAbstractClass();
989 $this->initProvider( $provider3, null, null, $manager );
990 $provider3->method( 'persistsSessionId' )
991 ->willReturn( true );
992 $provider3->method( 'canChangeUser' )
993 ->willReturn( true );
994 $provider3->expects( $this->once() )->method( 'refreshSessionInfo' )
995 ->willReturn( false );
996 $provider3->method( '__toString' )
997 ->willReturn( 'Mock3' );
999 TestingAccessWrapper
::newFromObject( $manager )->sessionProviders
= [
1000 (string)$provider => $provider,
1001 (string)$provider2 => $provider2,
1002 (string)$provider3 => $provider3,
1005 // No metadata, basic usage
1006 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1007 'provider' => $provider,
1009 'userInfo' => $userInfo
1011 $this->assertFalse( $info->isIdSafe() );
1012 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1013 $this->assertFalse( $info->isIdSafe() );
1014 $this->assertSame( [], $logger->getBuffer() );
1016 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1017 'provider' => $provider,
1018 'userInfo' => $userInfo
1020 $this->assertTrue( $info->isIdSafe() );
1021 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1022 $this->assertTrue( $info->isIdSafe() );
1023 $this->assertSame( [], $logger->getBuffer() );
1025 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1026 'provider' => $provider2,
1028 'userInfo' => $userInfo
1030 $this->assertFalse( $info->isIdSafe() );
1031 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1032 $this->assertTrue( $info->isIdSafe() );
1033 $this->assertSame( [], $logger->getBuffer() );
1035 // Unverified user, no metadata
1036 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1037 'provider' => $provider,
1039 'userInfo' => $unverifiedUserInfo
1041 $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
1042 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1043 $this->assertSame( [
1046 'Session "{session}": Unverified user provided and no metadata to auth it',
1048 ], $logger->getBuffer() );
1049 $logger->clearBuffer();
1051 // No metadata, missing data
1052 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1054 'userInfo' => $userInfo
1056 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1057 $this->assertSame( [
1058 [ LogLevel
::WARNING
, 'Session "{session}": Null provider and no metadata' ],
1059 ], $logger->getBuffer() );
1060 $logger->clearBuffer();
1062 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1063 'provider' => $provider,
1066 $this->assertFalse( $info->isIdSafe() );
1067 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1068 $this->assertInstanceOf( UserInfo
::class, $info->getUserInfo() );
1069 $this->assertTrue( $info->getUserInfo()->isVerified() );
1070 $this->assertTrue( $info->getUserInfo()->isAnon() );
1071 $this->assertFalse( $info->isIdSafe() );
1072 $this->assertSame( [], $logger->getBuffer() );
1074 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1075 'provider' => $provider2,
1078 $this->assertFalse( $info->isIdSafe() );
1079 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1080 $this->assertSame( [
1081 [ LogLevel
::INFO
, 'Session "{session}": No user provided and provider cannot set user' ]
1082 ], $logger->getBuffer() );
1083 $logger->clearBuffer();
1085 // Incomplete/bad metadata
1086 $this->store
->setRawSession( $id, true );
1087 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1088 $this->assertSame( [
1089 [ LogLevel
::WARNING
, 'Session "{session}": Bad data' ],
1090 ], $logger->getBuffer() );
1091 $logger->clearBuffer();
1093 $this->store
->setRawSession( $id, [ 'data' => [] ] );
1094 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1095 $this->assertSame( [
1096 [ LogLevel
::WARNING
, 'Session "{session}": Bad data structure' ],
1097 ], $logger->getBuffer() );
1098 $logger->clearBuffer();
1100 $this->store
->deleteSession( $id );
1101 $this->store
->setRawSession( $id, [ 'metadata' => $metadata ] );
1102 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1103 $this->assertSame( [
1104 [ LogLevel
::WARNING
, 'Session "{session}": Bad data structure' ],
1105 ], $logger->getBuffer() );
1106 $logger->clearBuffer();
1108 $this->store
->setRawSession( $id, [ 'metadata' => $metadata, 'data' => true ] );
1109 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1110 $this->assertSame( [
1111 [ LogLevel
::WARNING
, 'Session "{session}": Bad data structure' ],
1112 ], $logger->getBuffer() );
1113 $logger->clearBuffer();
1115 $this->store
->setRawSession( $id, [ 'metadata' => true, 'data' => [] ] );
1116 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1117 $this->assertSame( [
1118 [ LogLevel
::WARNING
, 'Session "{session}": Bad data structure' ],
1119 ], $logger->getBuffer() );
1120 $logger->clearBuffer();
1122 foreach ( $metadata as $key => $dummy ) {
1124 unset( $tmp[$key] );
1125 $this->store
->setRawSession( $id, [ 'metadata' => $tmp, 'data' => [] ] );
1126 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1127 $this->assertSame( [
1128 [ LogLevel
::WARNING
, 'Session "{session}": Bad metadata' ],
1129 ], $logger->getBuffer() );
1130 $logger->clearBuffer();
1133 // Basic usage with metadata
1134 $this->store
->setRawSession( $id, [ 'metadata' => $metadata, 'data' => [] ] );
1135 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1136 'provider' => $provider,
1138 'userInfo' => $userInfo
1140 $this->assertFalse( $info->isIdSafe() );
1141 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1142 $this->assertTrue( $info->isIdSafe() );
1143 $this->assertSame( [], $logger->getBuffer() );
1145 // Mismatched provider
1146 $this->store
->setSessionMeta( $id, [ 'provider' => 'Bad' ] +
$metadata );
1147 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1148 'provider' => $provider,
1150 'userInfo' => $userInfo
1152 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1153 $this->assertSame( [
1154 [ LogLevel
::WARNING
, 'Session "{session}": Wrong provider Bad !== Mock' ],
1155 ], $logger->getBuffer() );
1156 $logger->clearBuffer();
1159 $this->store
->setSessionMeta( $id, [ 'provider' => 'Bad' ] +
$metadata );
1160 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1162 'userInfo' => $userInfo
1164 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1165 $this->assertSame( [
1166 [ LogLevel
::WARNING
, 'Session "{session}": Unknown provider Bad' ],
1167 ], $logger->getBuffer() );
1168 $logger->clearBuffer();
1171 $this->store
->setSessionMeta( $id, $metadata );
1172 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1174 'userInfo' => $userInfo
1176 $this->assertFalse( $info->isIdSafe() );
1177 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1178 $this->assertTrue( $info->isIdSafe() );
1179 $this->assertSame( [], $logger->getBuffer() );
1181 // Bad user metadata
1182 $this->store
->setSessionMeta( $id, [ 'userId' => -1, 'userToken' => null ] +
$metadata );
1183 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1184 'provider' => $provider,
1187 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1188 $this->assertSame( [
1189 [ LogLevel
::ERROR
, 'Session "{session}": {exception}' ],
1190 ], $logger->getBuffer() );
1191 $logger->clearBuffer();
1193 $this->store
->setSessionMeta(
1194 $id, [ 'userId' => 0, 'userName' => '<X>', 'userToken' => null ] +
$metadata
1196 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1197 'provider' => $provider,
1200 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1201 $this->assertSame( [
1202 [ LogLevel
::ERROR
, 'Session "{session}": {exception}', ],
1203 ], $logger->getBuffer() );
1204 $logger->clearBuffer();
1206 // Mismatched user by ID
1207 $this->store
->setSessionMeta(
1208 $id, [ 'userId' => $userInfo->getId() +
1, 'userToken' => null ] +
$metadata
1210 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1211 'provider' => $provider,
1213 'userInfo' => $userInfo
1215 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1216 $this->assertSame( [
1217 [ LogLevel
::WARNING
, 'Session "{session}": User ID mismatch, {uid_a} !== {uid_b}' ],
1218 ], $logger->getBuffer() );
1219 $logger->clearBuffer();
1221 // Mismatched user by name
1222 $this->store
->setSessionMeta(
1223 $id, [ 'userId' => 0, 'userName' => 'X', 'userToken' => null ] +
$metadata
1225 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1226 'provider' => $provider,
1228 'userInfo' => $userInfo
1230 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1231 $this->assertSame( [
1232 [ LogLevel
::WARNING
, 'Session "{session}": User name mismatch, {uname_a} !== {uname_b}' ],
1233 ], $logger->getBuffer() );
1234 $logger->clearBuffer();
1236 // ID matches, name doesn't
1237 $this->store
->setSessionMeta(
1238 $id, [ 'userId' => $userInfo->getId(), 'userName' => 'X', 'userToken' => null ] +
$metadata
1240 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1241 'provider' => $provider,
1243 'userInfo' => $userInfo
1245 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1246 $this->assertSame( [
1249 'Session "{session}": User ID matched but name didn\'t (rename?), {uname_a} !== {uname_b}'
1251 ], $logger->getBuffer() );
1252 $logger->clearBuffer();
1254 // Mismatched anon user
1255 $this->store
->setSessionMeta(
1256 $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] +
$metadata
1258 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1259 'provider' => $provider,
1261 'userInfo' => $userInfo
1263 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1264 $this->assertSame( [
1267 'Session "{session}": the session store entry is for an anonymous user, ' .
1268 'but the session metadata indicates a non-anonynmous user',
1270 ], $logger->getBuffer() );
1271 $logger->clearBuffer();
1273 // Lookup user by ID
1274 $this->store
->setSessionMeta( $id, [ 'userToken' => null ] +
$metadata );
1275 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1276 'provider' => $provider,
1279 $this->assertFalse( $info->isIdSafe() );
1280 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1281 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1282 $this->assertTrue( $info->isIdSafe() );
1283 $this->assertSame( [], $logger->getBuffer() );
1285 // Lookup user by name
1286 $this->store
->setSessionMeta(
1287 $id, [ 'userId' => 0, 'userName' => $username, 'userToken' => null ] +
$metadata
1289 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1290 'provider' => $provider,
1293 $this->assertFalse( $info->isIdSafe() );
1294 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1295 $this->assertSame( $userInfo->getId(), $info->getUserInfo()->getId() );
1296 $this->assertTrue( $info->isIdSafe() );
1297 $this->assertSame( [], $logger->getBuffer() );
1299 // Lookup anonymous user
1300 $this->store
->setSessionMeta(
1301 $id, [ 'userId' => 0, 'userName' => null, 'userToken' => null ] +
$metadata
1303 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1304 'provider' => $provider,
1307 $this->assertFalse( $info->isIdSafe() );
1308 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1309 $this->assertTrue( $info->getUserInfo()->isAnon() );
1310 $this->assertTrue( $info->isIdSafe() );
1311 $this->assertSame( [], $logger->getBuffer() );
1313 // Unverified user with metadata
1314 $this->store
->setSessionMeta( $id, $metadata );
1315 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1316 'provider' => $provider,
1318 'userInfo' => $unverifiedUserInfo
1320 $this->assertFalse( $info->isIdSafe() );
1321 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1322 $this->assertTrue( $info->getUserInfo()->isVerified() );
1323 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1324 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1325 $this->assertTrue( $info->isIdSafe() );
1326 $this->assertSame( [], $logger->getBuffer() );
1328 // Unverified user with metadata
1329 $this->store
->setSessionMeta( $id, $metadata );
1330 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1331 'provider' => $provider,
1333 'userInfo' => $unverifiedUserInfo
1335 $this->assertFalse( $info->isIdSafe() );
1336 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1337 $this->assertTrue( $info->getUserInfo()->isVerified() );
1338 $this->assertSame( $unverifiedUserInfo->getId(), $info->getUserInfo()->getId() );
1339 $this->assertSame( $unverifiedUserInfo->getName(), $info->getUserInfo()->getName() );
1340 $this->assertTrue( $info->isIdSafe() );
1341 $this->assertSame( [], $logger->getBuffer() );
1344 $this->store
->setSessionMeta( $id, [ 'userToken' => 'Bad' ] +
$metadata );
1345 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1346 'provider' => $provider,
1348 'userInfo' => $userInfo
1350 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1351 $this->assertSame( [
1352 [ LogLevel
::WARNING
, 'Session "{session}": User token mismatch' ],
1353 ], $logger->getBuffer() );
1354 $logger->clearBuffer();
1356 // Provider metadata
1357 $this->store
->setSessionMeta( $id, [ 'provider' => 'Mock2' ] +
$metadata );
1358 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1359 'provider' => $provider2,
1361 'userInfo' => $userInfo,
1362 'metadata' => [ 'Info' ],
1364 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1365 $this->assertSame( [ 'Info', 'changed' => true ], $info->getProviderMetadata() );
1366 $this->assertSame( [], $logger->getBuffer() );
1368 $this->store
->setSessionMeta( $id, [ 'providerMetadata' => [ 'Saved' ] ] +
$metadata );
1369 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1370 'provider' => $provider,
1372 'userInfo' => $userInfo,
1374 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1375 $this->assertSame( [ 'Saved' ], $info->getProviderMetadata() );
1376 $this->assertSame( [], $logger->getBuffer() );
1378 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1379 'provider' => $provider,
1381 'userInfo' => $userInfo,
1382 'metadata' => [ 'Info' ],
1384 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1385 $this->assertSame( [ 'Merged' ], $info->getProviderMetadata() );
1386 $this->assertSame( [], $logger->getBuffer() );
1388 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1389 'provider' => $provider,
1391 'userInfo' => $userInfo,
1392 'metadata' => [ 'Throw' ],
1394 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1395 $this->assertSame( [
1398 'Session "{session}": Metadata merge failed: {exception}',
1400 ], $logger->getBuffer() );
1401 $logger->clearBuffer();
1403 // Remember from session
1404 $this->store
->setSessionMeta( $id, $metadata );
1405 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1406 'provider' => $provider,
1409 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1410 $this->assertFalse( $info->wasRemembered() );
1411 $this->assertSame( [], $logger->getBuffer() );
1413 $this->store
->setSessionMeta( $id, [ 'remember' => true ] +
$metadata );
1414 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1415 'provider' => $provider,
1418 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1419 $this->assertTrue( $info->wasRemembered() );
1420 $this->assertSame( [], $logger->getBuffer() );
1422 $this->store
->setSessionMeta( $id, [ 'remember' => false ] +
$metadata );
1423 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1424 'provider' => $provider,
1426 'userInfo' => $userInfo
1428 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1429 $this->assertTrue( $info->wasRemembered() );
1430 $this->assertSame( [], $logger->getBuffer() );
1432 // forceHTTPS from session
1433 $this->store
->setSessionMeta( $id, $metadata );
1434 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1435 'provider' => $provider,
1437 'userInfo' => $userInfo
1439 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1440 $this->assertFalse( $info->forceHTTPS() );
1441 $this->assertSame( [], $logger->getBuffer() );
1443 $this->store
->setSessionMeta( $id, [ 'forceHTTPS' => true ] +
$metadata );
1444 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1445 'provider' => $provider,
1447 'userInfo' => $userInfo
1449 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1450 $this->assertTrue( $info->forceHTTPS() );
1451 $this->assertSame( [], $logger->getBuffer() );
1453 $this->store
->setSessionMeta( $id, [ 'forceHTTPS' => false ] +
$metadata );
1454 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1455 'provider' => $provider,
1457 'userInfo' => $userInfo,
1458 'forceHTTPS' => true
1460 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1461 $this->assertTrue( $info->forceHTTPS() );
1462 $this->assertSame( [], $logger->getBuffer() );
1464 // "Persist" flag from session
1465 $this->store
->setSessionMeta( $id, $metadata );
1466 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1467 'provider' => $provider,
1469 'userInfo' => $userInfo
1471 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1472 $this->assertFalse( $info->wasPersisted() );
1473 $this->assertSame( [], $logger->getBuffer() );
1475 $this->store
->setSessionMeta( $id, [ 'persisted' => true ] +
$metadata );
1476 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1477 'provider' => $provider,
1479 'userInfo' => $userInfo
1481 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1482 $this->assertTrue( $info->wasPersisted() );
1483 $this->assertSame( [], $logger->getBuffer() );
1485 $this->store
->setSessionMeta( $id, [ 'persisted' => false ] +
$metadata );
1486 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1487 'provider' => $provider,
1489 'userInfo' => $userInfo,
1492 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1493 $this->assertTrue( $info->wasPersisted() );
1494 $this->assertSame( [], $logger->getBuffer() );
1496 // Provider refreshSessionInfo() returning false
1497 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1498 'provider' => $provider3,
1500 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1501 $this->assertSame( [], $logger->getBuffer() );
1505 $data = [ 'foo' => 1 ];
1506 $this->store
->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] );
1507 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1508 'provider' => $provider,
1510 'userInfo' => $userInfo
1512 $manager->setHookContainer( $this->createHookContainer( [
1513 'SessionCheckInfo' => function ( &$reason, $i, $r, $m, $d ) use (
1514 $info, $metadata, $data, $request, &$called
1516 $this->assertSame( $info->getId(), $i->getId() );
1517 $this->assertSame( $info->getProvider(), $i->getProvider() );
1518 $this->assertSame( $info->getUserInfo(), $i->getUserInfo() );
1519 $this->assertSame( $request, $r );
1520 $this->assertEquals( $metadata, $m );
1521 $this->assertEquals( $data, $d );
1526 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1527 $this->assertTrue( $called );
1528 $this->assertSame( [
1529 [ LogLevel
::WARNING
, 'Session "{session}": Hook aborted' ],
1530 ], $logger->getBuffer() );
1531 $logger->clearBuffer();
1532 $manager->setHookContainer( $this->createHookContainer() );
1534 // forceUse deletes bad backend data
1535 $this->store
->setSessionMeta( $id, [ 'userToken' => 'Bad' ] +
$metadata );
1536 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1537 'provider' => $provider,
1539 'userInfo' => $userInfo,
1542 $this->assertTrue( $loadSessionInfoFromStore( $info ) );
1543 $this->assertFalse( $this->store
->getSession( $id ) );
1544 $this->assertSame( [
1545 [ LogLevel
::WARNING
, 'Session "{session}": User token mismatch' ],
1546 ], $logger->getBuffer() );
1547 $logger->clearBuffer();
1551 * @dataProvider provideLogPotentialSessionLeakage
1553 public function testLogPotentialSessionLeakage(
1554 $ip, $mwuser, $sessionData, $expectedSessionData, $expectedLogLevel
1556 MWTimestamp
::setFakeTime( 1234567 );
1557 $this->overrideConfigValue( MainConfigNames
::SuspiciousIpExpiry
, 600 );
1558 $manager = new SessionManager();
1559 $logger = $this->createMock( LoggerInterface
::class );
1560 $this->setLogger( 'session-ip', $logger );
1561 $request = new FauxRequest();
1562 $request->setIP( $ip );
1563 $request->setCookie( 'mwuser-sessionId', $mwuser );
1565 $proxyLookup = $this->createMock( ProxyLookup
::class );
1566 $proxyLookup->method( 'isConfiguredProxy' )->willReturnCallback( static function ( $ip ) {
1567 return $ip === '11.22.33.44';
1569 $this->setService( 'ProxyLookup', $proxyLookup );
1571 $session = $this->createMock( Session
::class );
1572 $session->method( 'isPersistent' )->willReturn( true );
1573 $session->method( 'getUser' )->willReturn( $this->getTestSysop()->getUser() );
1574 $session->method( 'getRequest' )->willReturn( $request );
1575 $session->method( 'getProvider' )->willReturn(
1576 $this->createMock( CookieSessionProvider
::class ) );
1577 $session->method( 'get' )
1578 ->with( 'SessionManager-logPotentialSessionLeakage' )
1579 ->willReturn( $sessionData );
1580 $session->expects( $this->exactly( isset( $expectedSessionData ) ) )->method( 'set' )
1581 ->with( 'SessionManager-logPotentialSessionLeakage', $expectedSessionData );
1583 $logger->expects( $this->exactly( isset( $expectedLogLevel ) ) )->method( 'log' )
1584 ->with( $expectedLogLevel );
1586 $manager->logPotentialSessionLeakage( $session );
1589 public static function provideLogPotentialSessionLeakage() {
1591 $valid = $now - 100;
1592 $expired = $now - 1000;
1594 'no log for new IP' => [
1597 'sessionData' => [],
1598 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1599 'expectedLogLevel' => null,
1601 'no log for same IP' => [
1604 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $valid ],
1605 'expectedSessionData' => null,
1606 'expectedLogLevel' => null,
1608 'no log for expired IP' => [
1611 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => null, 'timestamp' => $expired ],
1612 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1613 'expectedLogLevel' => null,
1615 'INFO log for changed IP' => [
1618 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => null, 'timestamp' => $valid ],
1619 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1620 'expectedLogLevel' => LogLevel
::INFO
,
1623 'no log for new mwuser' => [
1626 'sessionData' => [],
1627 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1628 'expectedLogLevel' => null,
1630 'no log for same mwuser' => [
1633 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1634 'expectedSessionData' => null,
1635 'expectedLogLevel' => null,
1637 'NOTICE log for changed mwuser' => [
1640 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1641 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1642 'expectedLogLevel' => LogLevel
::NOTICE
,
1644 'no expiration for mwuser' => [
1647 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $expired ],
1648 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1649 'expectedLogLevel' => LogLevel
::NOTICE
,
1651 'WARNING log for changed IP + mwuser' => [
1654 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1655 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1656 'expectedLogLevel' => LogLevel
::WARNING
,
1659 'special IPs are ignored (1)' => [
1660 'ip' => '127.0.0.1',
1662 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1663 'expectedSessionData' => null,
1664 'expectedLogLevel' => null,
1666 'special IPs are ignored (2)' => [
1667 'ip' => '11.22.33.44',
1669 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1670 'expectedSessionData' => null,
1671 'expectedLogLevel' => null,