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