Merge "Remove EpicPupper from en.json authors"
[mediawiki.git] / tests / phpunit / includes / session / SessionManagerTest.php
blob4ee9159888d4bfd8fc6c28cb39ddbca8ea5e1e5b
1 <?php
3 namespace MediaWiki\Tests\Session;
5 use DummySessionProvider;
6 use InvalidArgumentException;
7 use MediaWiki\Config\HashConfig;
8 use MediaWiki\Context\RequestContext;
9 use MediaWiki\MainConfigNames;
10 use MediaWiki\Request\FauxRequest;
11 use MediaWiki\Request\ProxyLookup;
12 use MediaWiki\Session\CookieSessionProvider;
13 use MediaWiki\Session\MetadataMergeException;
14 use MediaWiki\Session\PHPSessionHandler;
15 use MediaWiki\Session\Session;
16 use MediaWiki\Session\SessionInfo;
17 use MediaWiki\Session\SessionManager;
18 use MediaWiki\Session\SessionOverflowException;
19 use MediaWiki\Session\SessionProvider;
20 use MediaWiki\Session\UserInfo;
21 use MediaWiki\Utils\MWTimestamp;
22 use MediaWikiIntegrationTestCase;
23 use Psr\Log\LoggerInterface;
24 use Psr\Log\LogLevel;
25 use Psr\Log\NullLogger;
26 use ReflectionClass;
27 use stdClass;
28 use TestLogger;
29 use TypeError;
30 use UnexpectedValueException;
31 use Wikimedia\ScopedCallback;
32 use Wikimedia\TestingAccessWrapper;
34 /**
35 * @group Session
36 * @group Database
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 ],
57 ] );
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 )
64 ) ? null : $m;
65 } );
67 return new SessionManager( [
68 'config' => $this->config,
69 'logger' => $this->logger,
70 'store' => $this->store,
71 ] );
74 protected function objectCacheDef( $object ) {
75 return [ 'factory' => static function () use ( $object ) {
76 return $object;
77 } ];
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;
102 } );
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();
111 session_id( '' );
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();
126 session_id( '' );
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,
148 ] ) );
149 $this->assertSame( $this->store, $manager->store );
151 foreach ( [
152 'config' => 'MediaWiki\Config\Config',
153 'logger' => 'Psr\Log\LoggerInterface',
154 'store' => 'Wikimedia\ObjectCache\BagOStuff',
155 ] as $key => $error ) {
156 try {
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;
173 $id1 = '';
174 $id2 = '';
175 $idEmpty = 'empty-session-------------------';
177 $providerBuilder = $this->getMockBuilder( DummySessionProvider::class )
178 ->onlyMethods(
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;
187 } );
188 $provider1->method( 'newSessionInfo' )
189 ->willReturnCallback( static function () use ( $idEmpty, $provider1 ) {
190 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
191 'provider' => $provider1,
192 'id' => $idEmpty,
193 'persisted' => true,
194 'idIsSafe' => true,
195 ] );
196 } );
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;
204 } );
206 $provider2 = $providerBuilder->getMock();
207 $provider2->method( 'provideSessionInfo' )
208 ->with( $this->identicalTo( $request ) )
209 ->willReturnCallback( static function ( $request ) use ( &$requestInfo2 ) {
210 return $requestInfo2;
211 } );
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;
219 } );
221 $this->config->set( MainConfigNames::SessionProviders, [
222 $this->objectCacheDef( $provider1 ),
223 $this->objectCacheDef( $provider2 ),
224 ] );
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() ),
237 'persisted' => true,
238 'idIsSafe' => true,
239 ] );
240 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
241 'provider' => $provider2,
242 'id' => ( $id2 = $manager->generateSessionId() ),
243 'persisted' => true,
244 'idIsSafe' => true,
245 ] );
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() ),
255 'persisted' => true,
256 'idIsSafe' => true,
257 ] );
258 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
259 'provider' => $provider2,
260 'id' => ( $id2 = $manager->generateSessionId() ),
261 'persisted' => true,
262 'idIsSafe' => true,
263 ] );
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 );
270 // Tied priorities
271 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
272 'provider' => $provider1,
273 'id' => ( $id1 = $manager->generateSessionId() ),
274 'persisted' => true,
275 'userInfo' => UserInfo::newAnonymous(),
276 'idIsSafe' => true,
277 ] );
278 $requestInfo2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
279 'provider' => $provider2,
280 'id' => ( $id2 = $manager->generateSessionId() ),
281 'persisted' => true,
282 'userInfo' => UserInfo::newAnonymous(),
283 'idIsSafe' => true,
284 ] );
285 try {
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: ',
291 $ex->getMessage()
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 );
300 // Bad provider
301 $requestInfo1 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
302 'provider' => $provider2,
303 'id' => ( $id1 = $manager->generateSessionId() ),
304 'persisted' => true,
305 'idIsSafe' => true,
306 ] );
307 $requestInfo2 = null;
308 try {
309 $manager->getSessionForRequest( $request );
310 $this->fail( 'Expcected exception not thrown' );
311 } catch ( UnexpectedValueException $ex ) {
312 $this->assertSame(
313 'Provider1 returned session info for a different provider: ' . $requestInfo1,
314 $ex->getMessage()
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() ),
325 'persisted' => true,
326 'userInfo' => UserInfo::newFromName( 'TestGetSessionForRequest', false ),
327 'idIsSafe' => true,
328 ] );
329 $requestInfo2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
330 'provider' => $provider2,
331 'id' => ( $id2 = $manager->generateSessionId() ),
332 'persisted' => true,
333 'idIsSafe' => true,
334 ] );
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() ),
347 'persisted' => true,
348 'idIsSafe' => true,
349 ] );
350 $requestInfo2 = new SessionInfo( SessionInfo::MAX_PRIORITY, [
351 'provider' => $provider2,
352 'id' => ( $id2 = $manager->generateSessionId() ),
353 'persisted' => true,
354 'userInfo' => UserInfo::newFromName( 'TestGetSessionForRequest', false ),
355 'idIsSafe' => true,
356 ] );
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 ),
371 'idIsSafe' => true,
372 ] );
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 );
379 $session->persist();
380 $this->assertTrue( $session->isPersistent() );
383 public function testGetSessionById() {
384 $manager = $this->getManager();
385 try {
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',
408 ] ] );
410 $this->assertNull( $manager->getSessionById( $id, true ) );
411 $this->assertNull( $manager->getSessionById( $id, false ) );
412 $this->logger->setCollect( false );
414 // Known session ID
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',
424 ] ] );
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' ] )
437 ->getMock();
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 ),
446 ] );
447 $this->logger->setCollect( true );
448 $this->assertNull( $manager->getSessionById( $id, true ) );
449 $this->logger->setCollect( false );
450 $this->assertSame( [
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' ] );
462 $expectId = null;
463 $info1 = null;
464 $info2 = null;
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;
472 } ) )
473 ->willReturnCallback( static function () use ( &$info1 ) {
474 return $info1;
475 } );
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;
485 } ) )
486 ->willReturnCallback( static function () use ( &$info2 ) {
487 return $info2;
488 } );
489 $provider1->method( '__toString' )
490 ->willReturn( 'MockProvider2' );
492 $this->config->set( MainConfigNames::SessionProviders, [
493 $this->objectCacheDef( $provider1 ),
494 $this->objectCacheDef( $provider2 ),
495 ] );
497 // No info
498 $expectId = null;
499 $info1 = null;
500 $info2 = null;
501 try {
502 $manager->getEmptySession();
503 $this->fail( 'Expected exception not thrown' );
504 } catch ( UnexpectedValueException $ex ) {
505 $this->assertSame(
506 'No provider could provide an empty session!',
507 $ex->getMessage()
511 // Info
512 $expectId = null;
513 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
514 'provider' => $provider1,
515 'id' => 'empty---------------------------',
516 'persisted' => true,
517 'idIsSafe' => true,
518 ] );
519 $info2 = null;
520 $session = $manager->getEmptySession();
521 $this->assertInstanceOf( Session::class, $session );
522 $this->assertSame( 'empty---------------------------', $session->getId() );
524 // Info, explicitly
525 $expectId = 'expected------------------------';
526 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
527 'provider' => $provider1,
528 'id' => $expectId,
529 'persisted' => true,
530 'idIsSafe' => true,
531 ] );
532 $info2 = null;
533 $session = $pmanager->getEmptySessionInternal( null, $expectId );
534 $this->assertInstanceOf( Session::class, $session );
535 $this->assertSame( $expectId, $session->getId() );
537 // Wrong ID
538 $expectId = 'expected-----------------------2';
539 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
540 'provider' => $provider1,
541 'id' => "un$expectId",
542 'persisted' => true,
543 'idIsSafe' => true,
544 ] );
545 $info2 = null;
546 try {
547 $pmanager->getEmptySessionInternal( null, $expectId );
548 $this->fail( 'Expected exception not thrown' );
549 } catch ( UnexpectedValueException $ex ) {
550 $this->assertSame(
551 'MockProvider1 returned empty session info with a wrong id: ' .
552 "un$expectId != $expectId",
553 $ex->getMessage()
557 // Unsafe ID
558 $expectId = 'expected-----------------------2';
559 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
560 'provider' => $provider1,
561 'id' => $expectId,
562 'persisted' => true,
563 ] );
564 $info2 = null;
565 try {
566 $pmanager->getEmptySessionInternal( null, $expectId );
567 $this->fail( 'Expected exception not thrown' );
568 } catch ( UnexpectedValueException $ex ) {
569 $this->assertSame(
570 'MockProvider1 returned empty session info with id flagged unsafe',
571 $ex->getMessage()
575 // Wrong provider
576 $expectId = null;
577 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
578 'provider' => $provider2,
579 'id' => 'empty---------------------------',
580 'persisted' => true,
581 'idIsSafe' => true,
582 ] );
583 $info2 = null;
584 try {
585 $manager->getEmptySession();
586 $this->fail( 'Expected exception not thrown' );
587 } catch ( UnexpectedValueException $ex ) {
588 $this->assertSame(
589 'MockProvider1 returned an empty session info for a different provider: ' . $info1,
590 $ex->getMessage()
594 // Highest priority wins
595 $expectId = null;
596 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
597 'provider' => $provider1,
598 'id' => 'empty1--------------------------',
599 'persisted' => true,
600 'idIsSafe' => true,
601 ] );
602 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
603 'provider' => $provider2,
604 'id' => 'empty2--------------------------',
605 'persisted' => true,
606 'idIsSafe' => true,
607 ] );
608 $session = $manager->getEmptySession();
609 $this->assertInstanceOf( Session::class, $session );
610 $this->assertSame( 'empty1--------------------------', $session->getId() );
612 $expectId = null;
613 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY + 1, [
614 'provider' => $provider1,
615 'id' => 'empty1--------------------------',
616 'persisted' => true,
617 'idIsSafe' => true,
618 ] );
619 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY + 2, [
620 'provider' => $provider2,
621 'id' => 'empty2--------------------------',
622 'persisted' => true,
623 'idIsSafe' => true,
624 ] );
625 $session = $manager->getEmptySession();
626 $this->assertInstanceOf( Session::class, $session );
627 $this->assertSame( 'empty2--------------------------', $session->getId() );
629 // Tied priorities throw an exception
630 $expectId = null;
631 $info1 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
632 'provider' => $provider1,
633 'id' => 'empty1--------------------------',
634 'persisted' => true,
635 'userInfo' => UserInfo::newAnonymous(),
636 'idIsSafe' => true,
637 ] );
638 $info2 = new SessionInfo( SessionInfo::MIN_PRIORITY, [
639 'provider' => $provider2,
640 'id' => 'empty2--------------------------',
641 'persisted' => true,
642 'userInfo' => UserInfo::newAnonymous(),
643 'idIsSafe' => true,
644 ] );
645 try {
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: ',
651 $ex->getMessage()
655 // Bad id
656 try {
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',
667 'userId' => 0,
668 'userName' => null,
669 'userToken' => null,
670 ] );
671 try {
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 ),
701 ] );
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' )
716 ->willReturn( [
717 'Foo' => null,
718 'Bar' => [ 'X', 'Bar1' ],
719 'Quux' => null,
720 ] );
721 $provider1->method( '__toString' )
722 ->willReturn( 'MockProvider1' );
724 $provider2 = $providerBuilder->getMock();
725 $provider2->expects( $this->once() )->method( 'getVaryHeaders' )
726 ->willReturn( [
727 'Baz' => null,
728 'Bar' => [ 'X', 'Bar2' ],
729 'Quux' => [ 'Quux' ],
730 ] );
731 $provider2->method( '__toString' )
732 ->willReturn( 'MockProvider2' );
734 $this->config->set( MainConfigNames::SessionProviders, [
735 $this->objectCacheDef( $provider1 ),
736 $this->objectCacheDef( $provider2 ),
737 ] );
739 $expect = [
740 'Foo' => null,
741 'Bar' => null,
742 'Quux' => null,
743 'Baz' => null,
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 ),
773 ] );
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 ],
789 ] );
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 ],
800 ] );
801 $manager->sessionProviders = null;
802 try {
803 $manager->getProviders();
804 $this->fail( 'Expected exception not thrown' );
805 } catch ( UnexpectedValueException $ex ) {
806 $this->assertSame(
807 'Duplicate provider name "DummySessionProvider"',
808 $ex->getMessage()
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' ),
833 'id' => $id,
834 'persisted' => true,
835 'userInfo' => UserInfo::newFromName( 'TestGetSessionFromInfo', true ),
836 'idIsSafe' => true,
837 ] );
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
874 $session = null;
876 try {
877 $manager->changeBackendId( $backend );
878 $this->fail( 'Expected exception not thrown' );
879 } catch ( InvalidArgumentException $ex ) {
880 $this->assertSame(
881 'Backend was not registered with this SessionManager', $ex->getMessage()
885 try {
886 $manager->deregisterSessionBackend( $backend );
887 $this->fail( 'Expected exception not thrown' );
888 } catch ( InvalidArgumentException $ex ) {
889 $this->assertSame(
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' )
914 ->with( $username );
915 $provider1->method( '__toString' )
916 ->willReturn( 'MockProvider1' );
918 $this->config->set( MainConfigNames::SessionProviders, [
919 $this->objectCacheDef( $provider1 ),
920 ] );
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';
946 $metadata = [
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!' );
971 return [ 'Merged' ];
972 } );
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;
985 return true;
986 } );
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,
1008 'id' => $id,
1009 'userInfo' => $userInfo
1010 ] );
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
1019 ] );
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,
1027 'id' => $id,
1028 'userInfo' => $userInfo
1029 ] );
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,
1038 'id' => $id,
1039 'userInfo' => $unverifiedUserInfo
1040 ] );
1041 $this->assertSame( $unverifiedUserInfo, $info->getUserInfo() );
1042 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1043 $this->assertSame( [
1045 LogLevel::INFO,
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, [
1053 'id' => $id,
1054 'userInfo' => $userInfo
1055 ] );
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,
1064 'id' => $id,
1065 ] );
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,
1076 'id' => $id,
1077 ] );
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 ) {
1123 $tmp = $metadata;
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,
1137 'id' => $id,
1138 'userInfo' => $userInfo
1139 ] );
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,
1149 'id' => $id,
1150 'userInfo' => $userInfo
1151 ] );
1152 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1153 $this->assertSame( [
1154 [ LogLevel::WARNING, 'Session "{session}": Wrong provider Bad !== Mock' ],
1155 ], $logger->getBuffer() );
1156 $logger->clearBuffer();
1158 // Unknown provider
1159 $this->store->setSessionMeta( $id, [ 'provider' => 'Bad' ] + $metadata );
1160 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1161 'id' => $id,
1162 'userInfo' => $userInfo
1163 ] );
1164 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1165 $this->assertSame( [
1166 [ LogLevel::WARNING, 'Session "{session}": Unknown provider Bad' ],
1167 ], $logger->getBuffer() );
1168 $logger->clearBuffer();
1170 // Fill in provider
1171 $this->store->setSessionMeta( $id, $metadata );
1172 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1173 'id' => $id,
1174 'userInfo' => $userInfo
1175 ] );
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,
1185 'id' => $id,
1186 ] );
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,
1198 'id' => $id,
1199 ] );
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,
1212 'id' => $id,
1213 'userInfo' => $userInfo
1214 ] );
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,
1227 'id' => $id,
1228 'userInfo' => $userInfo
1229 ] );
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,
1242 'id' => $id,
1243 'userInfo' => $userInfo
1244 ] );
1245 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1246 $this->assertSame( [
1248 LogLevel::WARNING,
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,
1260 'id' => $id,
1261 'userInfo' => $userInfo
1262 ] );
1263 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1264 $this->assertSame( [
1266 LogLevel::WARNING,
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,
1277 'id' => $id,
1278 ] );
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,
1291 'id' => $id,
1292 ] );
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,
1305 'id' => $id,
1306 ] );
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,
1317 'id' => $id,
1318 'userInfo' => $unverifiedUserInfo
1319 ] );
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,
1332 'id' => $id,
1333 'userInfo' => $unverifiedUserInfo
1334 ] );
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() );
1343 // Wrong token
1344 $this->store->setSessionMeta( $id, [ 'userToken' => 'Bad' ] + $metadata );
1345 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1346 'provider' => $provider,
1347 'id' => $id,
1348 'userInfo' => $userInfo
1349 ] );
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,
1360 'id' => $id,
1361 'userInfo' => $userInfo,
1362 'metadata' => [ 'Info' ],
1363 ] );
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,
1371 'id' => $id,
1372 'userInfo' => $userInfo,
1373 ] );
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,
1380 'id' => $id,
1381 'userInfo' => $userInfo,
1382 'metadata' => [ 'Info' ],
1383 ] );
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,
1390 'id' => $id,
1391 'userInfo' => $userInfo,
1392 'metadata' => [ 'Throw' ],
1393 ] );
1394 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1395 $this->assertSame( [
1397 LogLevel::WARNING,
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,
1407 'id' => $id,
1408 ] );
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,
1416 'id' => $id,
1417 ] );
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,
1425 'id' => $id,
1426 'userInfo' => $userInfo
1427 ] );
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,
1436 'id' => $id,
1437 'userInfo' => $userInfo
1438 ] );
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,
1446 'id' => $id,
1447 'userInfo' => $userInfo
1448 ] );
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,
1456 'id' => $id,
1457 'userInfo' => $userInfo,
1458 'forceHTTPS' => true
1459 ] );
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,
1468 'id' => $id,
1469 'userInfo' => $userInfo
1470 ] );
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,
1478 'id' => $id,
1479 'userInfo' => $userInfo
1480 ] );
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,
1488 'id' => $id,
1489 'userInfo' => $userInfo,
1490 'persisted' => true
1491 ] );
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,
1499 ] );
1500 $this->assertFalse( $loadSessionInfoFromStore( $info ) );
1501 $this->assertSame( [], $logger->getBuffer() );
1503 // Hook
1504 $called = false;
1505 $data = [ 'foo' => 1 ];
1506 $this->store->setSession( $id, [ 'metadata' => $metadata, 'data' => $data ] );
1507 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
1508 'provider' => $provider,
1509 'id' => $id,
1510 'userInfo' => $userInfo
1511 ] );
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 );
1522 $called = true;
1523 return false;
1525 ] ) );
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,
1538 'id' => $id,
1539 'userInfo' => $userInfo,
1540 'forceUse' => true,
1541 ] );
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';
1568 } );
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() {
1590 $now = 1234567;
1591 $valid = $now - 100;
1592 $expired = $now - 1000;
1593 return [
1594 'no log for new IP' => [
1595 'ip' => '1.2.3.4',
1596 'mwuser' => null,
1597 'sessionData' => [],
1598 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $now ],
1599 'expectedLogLevel' => null,
1601 'no log for same IP' => [
1602 'ip' => '1.2.3.4',
1603 'mwuser' => null,
1604 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => null, 'timestamp' => $valid ],
1605 'expectedSessionData' => null,
1606 'expectedLogLevel' => null,
1608 'no log for expired IP' => [
1609 'ip' => '1.2.3.4',
1610 'mwuser' => null,
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' => [
1616 'ip' => '1.2.3.4',
1617 'mwuser' => null,
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' => [
1624 'ip' => '1.2.3.4',
1625 'mwuser' => 'new',
1626 'sessionData' => [],
1627 'expectedSessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'new', 'timestamp' => $now ],
1628 'expectedLogLevel' => null,
1630 'no log for same mwuser' => [
1631 'ip' => '1.2.3.4',
1632 'mwuser' => 'old',
1633 'sessionData' => [ 'ip' => '1.2.3.4', 'mwuser' => 'old', 'timestamp' => $valid ],
1634 'expectedSessionData' => null,
1635 'expectedLogLevel' => null,
1637 'NOTICE log for changed mwuser' => [
1638 'ip' => '1.2.3.4',
1639 'mwuser' => 'new',
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' => [
1645 'ip' => '1.2.3.4',
1646 'mwuser' => 'new',
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' => [
1652 'ip' => '1.2.3.4',
1653 'mwuser' => 'new',
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',
1661 'mwuser' => 'new',
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',
1668 'mwuser' => 'new',
1669 'sessionData' => [ 'ip' => '10.20.30.40', 'mwuser' => 'old', 'timestamp' => $valid ],
1670 'expectedSessionData' => null,
1671 'expectedLogLevel' => null,