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
;
29 use UnexpectedValueException
;
30 use Wikimedia\ScopedCallback
;
31 use Wikimedia\TestingAccessWrapper
;
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 ],
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 )
66 return new SessionManager( [
67 'config' => $this->config
,
68 'logger' => $this->logger
,
69 'store' => $this->store
,
73 protected function objectCacheDef( $object ) {
74 return [ 'factory' => static function () use ( $object ) {
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;
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();
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();
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
,
148 $this->assertSame( $this->store
, $manager->store
);
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 ) {
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;
174 $idEmpty = 'empty-session-------------------';
176 $providerBuilder = $this->getMockBuilder( DummySessionProvider
::class )
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;
187 $provider1->method( 'newSessionInfo' )
188 ->willReturnCallback( static function () use ( $idEmpty, $provider1 ) {
189 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
190 'provider' => $provider1,
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;
205 $provider2 = $providerBuilder->getMock();
206 $provider2->method( 'provideSessionInfo' )
207 ->with( $this->identicalTo( $request ) )
208 ->willReturnCallback( static function ( $request ) use ( &$requestInfo2 ) {
209 return $requestInfo2;
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;
220 $this->config
->set( MainConfigNames
::SessionProviders
, [
221 $this->objectCacheDef( $provider1 ),
222 $this->objectCacheDef( $provider2 ),
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() ),
239 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
2, [
240 'provider' => $provider2,
241 'id' => ( $id2 = $manager->generateSessionId() ),
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() ),
257 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
258 'provider' => $provider2,
259 'id' => ( $id2 = $manager->generateSessionId() ),
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 );
270 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
271 'provider' => $provider1,
272 'id' => ( $id1 = $manager->generateSessionId() ),
274 'userInfo' => UserInfo
::newAnonymous(),
277 $requestInfo2 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
278 'provider' => $provider2,
279 'id' => ( $id2 = $manager->generateSessionId() ),
281 'userInfo' => UserInfo
::newAnonymous(),
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: ',
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 );
300 $requestInfo1 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
301 'provider' => $provider2,
302 'id' => ( $id1 = $manager->generateSessionId() ),
306 $requestInfo2 = null;
308 $manager->getSessionForRequest( $request );
309 $this->fail( 'Expcected exception not thrown' );
310 } catch ( UnexpectedValueException
$ex ) {
312 'Provider1 returned session info for a different provider: ' . $requestInfo1,
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() ),
325 'userInfo' => UserInfo
::newFromName( 'TestGetSessionForRequest', false ),
328 $requestInfo2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
329 'provider' => $provider2,
330 'id' => ( $id2 = $manager->generateSessionId() ),
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() ),
349 $requestInfo2 = new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
350 'provider' => $provider2,
351 'id' => ( $id2 = $manager->generateSessionId() ),
353 'userInfo' => UserInfo
::newFromName( 'TestGetSessionForRequest', false ),
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 ),
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 );
379 $this->assertTrue( $session->isPersistent() );
382 public function testGetSessionById() {
383 $manager = $this->getManager();
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',
409 $this->assertNull( $manager->getSessionById( $id, true ) );
410 $this->assertNull( $manager->getSessionById( $id, false ) );
411 $this->logger
->setCollect( false );
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',
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' ] )
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 ),
446 $this->logger
->setCollect( true );
447 $this->assertNull( $manager->getSessionById( $id, true ) );
448 $this->logger
->setCollect( false );
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' ] );
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;
472 ->willReturnCallback( static function () use ( &$info1 ) {
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;
485 ->willReturnCallback( static function () use ( &$info2 ) {
488 $provider1->method( '__toString' )
489 ->willReturn( 'MockProvider2' );
491 $this->config
->set( MainConfigNames
::SessionProviders
, [
492 $this->objectCacheDef( $provider1 ),
493 $this->objectCacheDef( $provider2 ),
501 $manager->getEmptySession();
502 $this->fail( 'Expected exception not thrown' );
503 } catch ( UnexpectedValueException
$ex ) {
505 'No provider could provide an empty session!',
512 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
513 'provider' => $provider1,
514 'id' => 'empty---------------------------',
519 $session = $manager->getEmptySession();
520 $this->assertInstanceOf( Session
::class, $session );
521 $this->assertSame( 'empty---------------------------', $session->getId() );
524 $expectId = 'expected------------------------';
525 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
526 'provider' => $provider1,
532 $session = $pmanager->getEmptySessionInternal( null, $expectId );
533 $this->assertInstanceOf( Session
::class, $session );
534 $this->assertSame( $expectId, $session->getId() );
537 $expectId = 'expected-----------------------2';
538 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
539 'provider' => $provider1,
540 'id' => "un$expectId",
546 $pmanager->getEmptySessionInternal( null, $expectId );
547 $this->fail( 'Expected exception not thrown' );
548 } catch ( UnexpectedValueException
$ex ) {
550 'MockProvider1 returned empty session info with a wrong id: ' .
551 "un$expectId != $expectId",
557 $expectId = 'expected-----------------------2';
558 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
559 'provider' => $provider1,
565 $pmanager->getEmptySessionInternal( null, $expectId );
566 $this->fail( 'Expected exception not thrown' );
567 } catch ( UnexpectedValueException
$ex ) {
569 'MockProvider1 returned empty session info with id flagged unsafe',
576 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
577 'provider' => $provider2,
578 'id' => 'empty---------------------------',
584 $manager->getEmptySession();
585 $this->fail( 'Expected exception not thrown' );
586 } catch ( UnexpectedValueException
$ex ) {
588 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
593 // Highest priority wins
595 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
596 'provider' => $provider1,
597 'id' => 'empty1--------------------------',
601 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
602 'provider' => $provider2,
603 'id' => 'empty2--------------------------',
607 $session = $manager->getEmptySession();
608 $this->assertInstanceOf( Session
::class, $session );
609 $this->assertSame( 'empty1--------------------------', $session->getId() );
612 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
1, [
613 'provider' => $provider1,
614 'id' => 'empty1--------------------------',
618 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY +
2, [
619 'provider' => $provider2,
620 'id' => 'empty2--------------------------',
624 $session = $manager->getEmptySession();
625 $this->assertInstanceOf( Session
::class, $session );
626 $this->assertSame( 'empty2--------------------------', $session->getId() );
628 // Tied priorities throw an exception
630 $info1 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
631 'provider' => $provider1,
632 'id' => 'empty1--------------------------',
634 'userInfo' => UserInfo
::newAnonymous(),
637 $info2 = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
638 'provider' => $provider2,
639 'id' => 'empty2--------------------------',
641 'userInfo' => UserInfo
::newAnonymous(),
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: ',
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',
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 ),
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' )
717 'Bar' => [ 'X', 'Bar1' ],
720 $provider1->method( '__toString' )
721 ->willReturn( 'MockProvider1' );
723 $provider2 = $providerBuilder->getMock();
724 $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
727 'Bar' => [ 'X', 'Bar2' ],
728 'Quux' => [ 'Quux' ],
730 $provider2->method( '__toString' )
731 ->willReturn( 'MockProvider2' );
733 $this->config
->set( MainConfigNames
::SessionProviders
, [
734 $this->objectCacheDef( $provider1 ),
735 $this->objectCacheDef( $provider2 ),
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 ),
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 ],
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 ],
800 $manager->sessionProviders
= null;
802 $manager->getProviders();
803 $this->fail( 'Expected exception not thrown' );
804 } catch ( UnexpectedValueException
$ex ) {
806 'Duplicate provider name "DummySessionProvider"',
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' ),
834 'userInfo' => UserInfo
::newFromName( 'TestGetSessionFromInfo', true ),
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
876 $manager->changeBackendId( $backend );
877 $this->fail( 'Expected exception not thrown' );
878 } catch ( InvalidArgumentException
$ex ) {
880 'Backend was not registered with this SessionManager', $ex->getMessage()
885 $manager->deregisterSessionBackend( $backend );
886 $this->fail( 'Expected exception not thrown' );
887 } catch ( InvalidArgumentException
$ex ) {
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' )
914 $provider1->method( '__toString' )
915 ->willReturn( 'MockProvider1' );
917 $this->config
->set( MainConfigNames
::SessionProviders
, [
918 $this->objectCacheDef( $provider1 ),
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';
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!' );
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;
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,
1008 'userInfo' => $userInfo
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
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,
1027 'userInfo' => $userInfo
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,
1038 'userInfo' => $unverifiedUserInfo
1040 $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
1041 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1042 $this->assertSame( [
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
, [
1053 'userInfo' => $userInfo
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,
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,
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 ) {
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,
1137 'userInfo' => $userInfo
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,
1149 'userInfo' => $userInfo
1151 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1152 $this->assertSame( [
1153 [ LogLevel
::WARNING
, 'Session "{session}": Wrong provider Bad !== Mock' ],
1154 ], $logger->getBuffer() );
1155 $logger->clearBuffer();
1158 $this->store
->setSessionMeta( $id, [ 'provider' => 'Bad' ] +
$metadata );
1159 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1161 'userInfo' => $userInfo
1163 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1164 $this->assertSame( [
1165 [ LogLevel
::WARNING
, 'Session "{session}": Unknown provider Bad' ],
1166 ], $logger->getBuffer() );
1167 $logger->clearBuffer();
1170 $this->store
->setSessionMeta( $id, $metadata );
1171 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1173 'userInfo' => $userInfo
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,
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,
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,
1212 'userInfo' => $userInfo
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,
1227 'userInfo' => $userInfo
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,
1242 'userInfo' => $userInfo
1244 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1245 $this->assertSame( [
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,
1260 'userInfo' => $userInfo
1262 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1263 $this->assertSame( [
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,
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,
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,
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,
1317 'userInfo' => $unverifiedUserInfo
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,
1332 'userInfo' => $unverifiedUserInfo
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() );
1343 $this->store
->setSessionMeta( $id, [ 'userToken' => 'Bad' ] +
$metadata );
1344 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1345 'provider' => $provider,
1347 'userInfo' => $userInfo
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,
1360 'userInfo' => $userInfo,
1361 'metadata' => [ 'Info' ],
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,
1371 'userInfo' => $userInfo,
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,
1380 'userInfo' => $userInfo,
1381 'metadata' => [ 'Info' ],
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,
1390 'userInfo' => $userInfo,
1391 'metadata' => [ 'Throw' ],
1393 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1394 $this->assertSame( [
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,
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,
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,
1425 'userInfo' => $userInfo
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,
1436 'userInfo' => $userInfo
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,
1446 'userInfo' => $userInfo
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,
1456 'userInfo' => $userInfo,
1457 'forceHTTPS' => true
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,
1468 'userInfo' => $userInfo
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,
1478 'userInfo' => $userInfo
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,
1488 'userInfo' => $userInfo,
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,
1499 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1500 $this->assertSame( [], $logger->getBuffer() );
1504 $data = [ 'foo' => 1 ];
1505 $this->store
->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] );
1506 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
1507 'provider' => $provider,
1509 'userInfo' => $userInfo
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 );
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,
1538 'userInfo' => $userInfo,
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';
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() {
1590 $valid = $now - 100;
1591 $expired = $now - 1000;
1593 'no log for new IP' => [
1596 'sessionData' => [],
1597 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1598 'expectedLogLevel' => null,
1600 'no log for same IP' => [
1603 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $valid ],
1604 'expectedSessionData' => null,
1605 'expectedLogLevel' => null,
1607 'no log for expired IP' => [
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' => [
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' => [
1625 'sessionData' => [],
1626 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1627 'expectedLogLevel' => null,
1629 'no log for same mwuser' => [
1632 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1633 'expectedSessionData' => null,
1634 'expectedLogLevel' => null,
1636 'NOTICE log for changed mwuser' => [
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' => [
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' => [
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',
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',
1668 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1669 'expectedSessionData' => null,
1670 'expectedLogLevel' => null,