3 namespace MediaWiki\Tests\Session
;
5 use BadMethodCallException
;
6 use DummySessionProvider
;
7 use InvalidArgumentException
;
8 use MediaWiki\Config\Config
;
9 use MediaWiki\Config\HashConfig
;
10 use MediaWiki\Context\RequestContext
;
11 use MediaWiki\HookContainer\HookContainer
;
12 use MediaWiki\Request\FauxRequest
;
13 use MediaWiki\Session\PHPSessionHandler
;
14 use MediaWiki\Session\Session
;
15 use MediaWiki\Session\SessionBackend
;
16 use MediaWiki\Session\SessionId
;
17 use MediaWiki\Session\SessionInfo
;
18 use MediaWiki\Session\SessionManager
;
19 use MediaWiki\Session\SessionProvider
;
20 use MediaWiki\Session\UserInfo
;
21 use MediaWiki\User\User
;
22 use MediaWikiIntegrationTestCase
;
23 use Psr\Log\NullLogger
;
24 use UnexpectedValueException
;
25 use Wikimedia\ObjectCache\CachedBagOStuff
;
26 use Wikimedia\ScopedCallback
;
27 use Wikimedia\TestingAccessWrapper
;
32 * @covers \MediaWiki\Session\SessionBackend
34 class SessionBackendTest
extends MediaWikiIntegrationTestCase
{
35 use SessionProviderTestTrait
;
37 private const SESSIONID
= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
39 /** @var SessionManager */
45 /** @var SessionProvider */
48 /** @var TestBagOStuff */
52 protected $onSessionMetadataCalled = false;
55 * @return HookContainer
57 private function getHookContainer() {
58 // Need a real HookContainer to support modification of $wgHooks in the test
59 return $this->getServiceContainer()->getHookContainer();
63 * Returns a non-persistent backend that thinks it has at least one session active
64 * @param User|null $user
65 * @param string|null $id
66 * @return SessionBackend
68 protected function getBackend( ?User
$user = null, $id = null ) {
69 if ( !$this->config
) {
70 $this->config
= new HashConfig();
71 $this->manager
= null;
73 if ( !$this->store
) {
74 $this->store
= new TestBagOStuff();
75 $this->manager
= null;
78 $logger = new NullLogger();
79 if ( !$this->manager
) {
80 $this->manager
= new SessionManager( [
81 'store' => $this->store
,
83 'config' => $this->config
,
87 $hookContainer = $this->getHookContainer();
89 if ( !$this->provider
) {
90 $this->provider
= new DummySessionProvider();
92 $this->initProvider( $this->provider
, null, $this->config
, $this->manager
, $hookContainer );
94 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
95 'provider' => $this->provider
,
96 'id' => $id ?
: self
::SESSIONID
,
98 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
101 $id = new SessionId( $info->getId() );
103 $backend = new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
104 $priv = TestingAccessWrapper
::newFromObject( $backend );
105 $priv->persist
= false;
106 $priv->requests
= [ 100 => new FauxRequest() ];
107 $priv->requests
[100]->setSessionId( $id );
108 $priv->usePhpSessionHandling
= false;
110 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
111 $manager->allSessionBackends
= [ $backend->getId() => $backend ] +
$manager->allSessionBackends
;
112 $manager->allSessionIds
= [ $backend->getId() => $id ] +
$manager->allSessionIds
;
113 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
118 public function testConstructor() {
119 $username = 'TestConstructor';
123 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
124 'provider' => $this->provider
,
125 'id' => self
::SESSIONID
,
127 'userInfo' => UserInfo
::newFromName( $username, false ),
130 $id = new SessionId( $info->getId() );
131 $logger = new NullLogger();
132 $hookContainer = $this->getHookContainer();
134 new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
135 $this->fail( 'Expected exception not thrown' );
136 } catch ( InvalidArgumentException
$ex ) {
138 'Refusing to create session for unverified user ' . $info->getUserInfo(),
143 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
144 'id' => self
::SESSIONID
,
145 'userInfo' => UserInfo
::newFromName( $username, true ),
148 $id = new SessionId( $info->getId() );
150 new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
151 $this->fail( 'Expected exception not thrown' );
152 } catch ( InvalidArgumentException
$ex ) {
153 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
156 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
157 'provider' => $this->provider
,
158 'id' => self
::SESSIONID
,
160 'userInfo' => UserInfo
::newFromName( $username, true ),
163 $id = new SessionId( '!' . $info->getId() );
165 new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
166 $this->fail( 'Expected exception not thrown' );
167 } catch ( InvalidArgumentException
$ex ) {
169 'SessionId and SessionInfo don\'t match',
174 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
175 'provider' => $this->provider
,
176 'id' => self
::SESSIONID
,
178 'userInfo' => UserInfo
::newFromName( $username, true ),
181 $id = new SessionId( $info->getId() );
182 $backend = new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
183 $this->assertSame( self
::SESSIONID
, $backend->getId() );
184 $this->assertSame( $id, $backend->getSessionId() );
185 $this->assertSame( $this->provider
, $backend->getProvider() );
186 $this->assertInstanceOf( User
::class, $backend->getUser() );
187 $this->assertSame( $username, $backend->getUser()->getName() );
188 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
189 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
190 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
192 $expire = time() +
100;
193 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ] );
195 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
196 'provider' => $this->provider
,
197 'id' => self
::SESSIONID
,
199 'forceHTTPS' => true,
200 'metadata' => [ 'foo' ],
203 $id = new SessionId( $info->getId() );
204 $backend = new SessionBackend( $id, $info, $this->store
, $logger, $hookContainer, 10 );
205 $this->assertSame( self
::SESSIONID
, $backend->getId() );
206 $this->assertSame( $id, $backend->getSessionId() );
207 $this->assertSame( $this->provider
, $backend->getProvider() );
208 $this->assertInstanceOf( User
::class, $backend->getUser() );
209 $this->assertTrue( $backend->getUser()->isAnon() );
210 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
211 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
212 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
213 $this->assertSame( $expire, TestingAccessWrapper
::newFromObject( $backend )->expires
);
214 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
217 public function testSessionStuff() {
218 $backend = $this->getBackend();
219 $priv = TestingAccessWrapper
::newFromObject( $backend );
220 $priv->requests
= []; // Remove dummy session
222 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
224 $request1 = new FauxRequest();
225 $session1 = $backend->getSession( $request1 );
226 $request2 = new FauxRequest();
227 $session2 = $backend->getSession( $request2 );
229 $this->assertInstanceOf( Session
::class, $session1 );
230 $this->assertInstanceOf( Session
::class, $session2 );
231 $this->assertCount( 2, $priv->requests
);
233 $index = TestingAccessWrapper
::newFromObject( $session1 )->index
;
235 $this->assertSame( $request1, $backend->getRequest( $index ) );
236 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
237 $request1->setCookie( 'UserName', 'Example' );
238 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
241 $this->assertCount( 1, $priv->requests
);
242 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
243 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
245 $backend->getRequest( $index );
246 $this->fail( 'Expected exception not thrown' );
247 } catch ( InvalidArgumentException
$ex ) {
248 $this->assertSame( 'Invalid session index', $ex->getMessage() );
251 $backend->suggestLoginUsername( $index );
252 $this->fail( 'Expected exception not thrown' );
253 } catch ( InvalidArgumentException
$ex ) {
254 $this->assertSame( 'Invalid session index', $ex->getMessage() );
258 $this->assertSame( [], $priv->requests
);
259 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
260 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
263 public function testSetProviderMetadata() {
264 $backend = $this->getBackend();
265 $priv = TestingAccessWrapper
::newFromObject( $backend );
266 $priv->providerMetadata
= [ 'dummy' ];
269 $backend->setProviderMetadata( 'foo' );
270 $this->fail( 'Expected exception not thrown' );
271 } catch ( InvalidArgumentException
$ex ) {
272 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
276 $backend->setProviderMetadata( (object)[] );
277 $this->fail( 'Expected exception not thrown' );
278 } catch ( InvalidArgumentException
$ex ) {
279 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
282 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
283 $backend->setProviderMetadata( [ 'dummy' ] );
284 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
286 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
287 $backend->setProviderMetadata( [ 'test' ] );
288 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
289 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
290 $this->store
->deleteSession( self
::SESSIONID
);
292 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
293 $backend->setProviderMetadata( null );
294 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
295 $this->assertSame( null, $backend->getProviderMetadata() );
296 $this->store
->deleteSession( self
::SESSIONID
);
299 public function testResetId() {
302 $builder = $this->getMockBuilder( DummySessionProvider
::class )
303 ->onlyMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
305 $this->provider
= $builder->getMock();
306 $this->provider
->method( 'persistsSessionId' )
307 ->willReturn( false );
308 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
309 $backend = $this->getBackend( User
::newFromName( 'TestResetId' ) );
310 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
311 $sessionId = $backend->getSessionId();
313 $this->assertSame( self
::SESSIONID
, $backend->getId() );
314 $this->assertSame( $backend->getId(), $sessionId->getId() );
315 $this->assertSame( $id, session_id() );
316 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
318 $this->provider
= $builder->getMock();
319 $this->provider
->method( 'persistsSessionId' )
320 ->willReturn( true );
321 $backend = $this->getBackend();
322 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
323 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
324 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
325 $sessionId = $backend->getSessionId();
327 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
328 $this->assertSame( $backend->getId(), $sessionId->getId() );
329 $this->assertIsArray( $this->store
->getSession( $backend->getId() ) );
330 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
331 $this->assertSame( $id, session_id() );
332 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
333 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
334 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
337 public function testPersist() {
338 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
339 ->onlyMethods( [ 'persistSession' ] )->getMock();
340 $this->provider
->expects( $this->once() )->method( 'persistSession' );
341 $backend = $this->getBackend();
342 $this->assertFalse( $backend->isPersistent() );
343 $backend->save(); // This one shouldn't call $provider->persistSession()
346 $this->assertTrue( $backend->isPersistent() );
348 $this->provider
= null;
349 $backend = $this->getBackend();
350 $wrap = TestingAccessWrapper
::newFromObject( $backend );
351 $wrap->persist
= true;
354 $this->assertNotEquals( 0, $wrap->expires
);
357 public function testUnpersist() {
358 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
359 ->onlyMethods( [ 'unpersistSession' ] )->getMock();
360 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
361 $backend = $this->getBackend();
362 $wrap = TestingAccessWrapper
::newFromObject( $backend );
363 $wrap->store
= new CachedBagOStuff( $this->store
);
364 $wrap->persist
= true;
365 $wrap->dataDirty
= true;
367 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
368 $this->assertTrue( $backend->isPersistent() );
369 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
371 $backend->unpersist();
372 $this->assertFalse( $backend->isPersistent() );
373 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
374 $this->assertNotFalse(
375 $wrap->store
->get( $wrap->store
->makeKey( 'MWSession', self
::SESSIONID
) )
379 public function testRememberUser() {
380 $backend = $this->getBackend();
382 $remembered = $backend->shouldRememberUser();
383 $backend->setRememberUser( !$remembered );
384 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
385 $backend->setRememberUser( $remembered );
386 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
389 public function testForceHTTPS() {
390 $backend = $this->getBackend();
392 $force = $backend->shouldForceHTTPS();
393 $backend->setForceHTTPS( !$force );
394 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
395 $backend->setForceHTTPS( $force );
396 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
399 public function testLoggedOutTimestamp() {
400 $backend = $this->getBackend();
402 $backend->setLoggedOutTimestamp( 42 );
403 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
404 $backend->setLoggedOutTimestamp( '123' );
405 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
408 public function testSetUser() {
409 $user = static::getTestSysop()->getUser();
411 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
412 ->onlyMethods( [ 'canChangeUser' ] )->getMock();
413 $this->provider
->method( 'canChangeUser' )
414 ->willReturn( false );
415 $backend = $this->getBackend();
416 $this->assertFalse( $backend->canSetUser() );
418 $backend->setUser( $user );
419 $this->fail( 'Expected exception not thrown' );
420 } catch ( BadMethodCallException
$ex ) {
422 'Cannot set user on this session; check $session->canSetUser() first',
426 $this->assertNotSame( $user, $backend->getUser() );
428 $this->provider
= null;
429 $backend = $this->getBackend();
430 $this->assertTrue( $backend->canSetUser() );
431 $this->assertNotSame( $user, $backend->getUser() );
432 $backend->setUser( $user );
433 $this->assertSame( $user, $backend->getUser() );
436 public function testDirty() {
437 $backend = $this->getBackend();
438 $priv = TestingAccessWrapper
::newFromObject( $backend );
439 $priv->dataDirty
= false;
441 $this->assertTrue( $priv->dataDirty
);
444 public function testGetData() {
445 $backend = $this->getBackend();
446 $data = $backend->getData();
447 $this->assertSame( [], $data );
448 $this->assertTrue( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
449 $data['???'] = '!!!';
450 $this->assertSame( [ '???' => '!!!' ], $data );
452 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
453 $this->store
->setSessionData( self
::SESSIONID
, $testData );
454 $backend = $this->getBackend();
455 $this->assertSame( $testData, $backend->getData() );
456 $this->assertFalse( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
459 public function testAddData() {
460 $backend = $this->getBackend();
461 $priv = TestingAccessWrapper
::newFromObject( $backend );
463 $priv->data
= [ 'foo' => 1 ];
464 $priv->dataDirty
= false;
465 $backend->addData( [ 'foo' => 1 ] );
466 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
467 $this->assertFalse( $priv->dataDirty
);
469 $priv->data
= [ 'foo' => 1 ];
470 $priv->dataDirty
= false;
471 $backend->addData( [ 'foo' => '1' ] );
472 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
473 $this->assertTrue( $priv->dataDirty
);
475 $priv->data
= [ 'foo' => 1 ];
476 $priv->dataDirty
= false;
477 $backend->addData( [ 'bar' => 2 ] );
478 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
479 $this->assertTrue( $priv->dataDirty
);
482 public function testDelaySave() {
483 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
484 $backend = $this->getBackend();
485 $priv = TestingAccessWrapper
::newFromObject( $backend );
486 $priv->persist
= true;
488 // Saves happen normally when no delay is in effect
489 $this->onSessionMetadataCalled
= false;
490 $priv->metaDirty
= true;
492 $this->assertTrue( $this->onSessionMetadataCalled
);
494 $this->onSessionMetadataCalled
= false;
495 $priv->metaDirty
= true;
497 $this->assertTrue( $this->onSessionMetadataCalled
);
499 $delay = $backend->delaySave();
501 // Autosave doesn't happen when no delay is in effect
502 $this->onSessionMetadataCalled
= false;
503 $priv->metaDirty
= true;
505 $this->assertFalse( $this->onSessionMetadataCalled
);
507 // Save still does happen when no delay is in effect
509 $this->assertTrue( $this->onSessionMetadataCalled
);
511 // Save happens when delay is consumed
512 $this->onSessionMetadataCalled
= false;
513 $priv->metaDirty
= true;
514 ScopedCallback
::consume( $delay );
515 $this->assertTrue( $this->onSessionMetadataCalled
);
517 // Test multiple delays
518 $delay1 = $backend->delaySave();
519 $delay2 = $backend->delaySave();
520 $delay3 = $backend->delaySave();
521 $this->onSessionMetadataCalled
= false;
522 $priv->metaDirty
= true;
524 $this->assertFalse( $this->onSessionMetadataCalled
);
525 ScopedCallback
::consume( $delay3 );
526 $this->assertFalse( $this->onSessionMetadataCalled
);
527 ScopedCallback
::consume( $delay1 );
528 $this->assertFalse( $this->onSessionMetadataCalled
);
529 ScopedCallback
::consume( $delay2 );
530 $this->assertTrue( $this->onSessionMetadataCalled
);
533 public function testSave() {
534 $user = static::getTestSysop()->getUser();
535 $this->store
= new TestBagOStuff();
536 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
538 $neverHook = $this->getMockBuilder( __CLASS__
)
539 ->onlyMethods( [ 'onSessionMetadata' ] )->getMock();
540 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
542 $builder = $this->getMockBuilder( DummySessionProvider
::class )
543 ->onlyMethods( [ 'persistSession', 'unpersistSession' ] );
545 $neverProvider = $builder->getMock();
546 $neverProvider->expects( $this->never() )->method( 'persistSession' );
547 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
549 // Not persistent or dirty
550 $this->provider
= $neverProvider;
551 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
552 $this->store
->setSessionData( self
::SESSIONID
, $testData );
553 $backend = $this->getBackend( $user );
554 $this->store
->deleteSession( self
::SESSIONID
);
555 $this->assertFalse( $backend->isPersistent() );
556 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
557 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
559 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
561 // (but does unpersist if forced)
562 $this->provider
= $builder->getMock();
563 $this->provider
->expects( $this->never() )->method( 'persistSession' );
564 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
565 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
566 $this->store
->setSessionData( self
::SESSIONID
, $testData );
567 $backend = $this->getBackend( $user );
568 $this->store
->deleteSession( self
::SESSIONID
);
569 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
570 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
571 $this->assertFalse( $backend->isPersistent() );
572 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
573 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
575 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
577 // (but not to a WebRequest associated with a different session)
578 $this->provider
= $neverProvider;
579 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
580 $this->store
->setSessionData( self
::SESSIONID
, $testData );
581 $backend = $this->getBackend( $user );
582 TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
583 ->setSessionId( new SessionId( 'x' ) );
584 $this->store
->deleteSession( self
::SESSIONID
);
585 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
586 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
587 $this->assertFalse( $backend->isPersistent() );
588 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
589 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
591 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
593 // Not persistent, but dirty
594 $this->provider
= $neverProvider;
595 $this->onSessionMetadataCalled
= false;
596 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
597 $this->store
->setSessionData( self
::SESSIONID
, $testData );
598 $backend = $this->getBackend( $user );
599 $this->store
->deleteSession( self
::SESSIONID
);
600 $this->assertFalse( $backend->isPersistent() );
601 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
602 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
604 $this->assertTrue( $this->onSessionMetadataCalled
);
605 $blob = $this->store
->getSession( self
::SESSIONID
);
606 $this->assertIsArray( $blob );
607 $this->assertArrayHasKey( 'metadata', $blob );
608 $metadata = $blob['metadata'];
609 $this->assertIsArray( $metadata );
610 $this->assertArrayHasKey( '???', $metadata );
611 $this->assertSame( '!!!', $metadata['???'] );
612 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
613 'making sure it didn\'t save to backend' );
615 // Persistent, not dirty
616 $this->provider
= $neverProvider;
617 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
618 $this->store
->setSessionData( self
::SESSIONID
, $testData );
619 $backend = $this->getBackend( $user );
620 $this->store
->deleteSession( self
::SESSIONID
);
621 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
622 $this->assertTrue( $backend->isPersistent() );
623 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
624 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
626 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
628 // (but will persist if forced)
629 $this->provider
= $builder->getMock();
630 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
631 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
632 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
633 $this->store
->setSessionData( self
::SESSIONID
, $testData );
634 $backend = $this->getBackend( $user );
635 $this->store
->deleteSession( self
::SESSIONID
);
636 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
637 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
638 $this->assertTrue( $backend->isPersistent() );
639 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
640 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
642 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
644 // Persistent and dirty
645 $this->provider
= $neverProvider;
646 $this->onSessionMetadataCalled
= false;
647 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
648 $this->store
->setSessionData( self
::SESSIONID
, $testData );
649 $backend = $this->getBackend( $user );
650 $this->store
->deleteSession( self
::SESSIONID
);
651 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
652 $this->assertTrue( $backend->isPersistent() );
653 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
654 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
656 $this->assertTrue( $this->onSessionMetadataCalled
);
657 $blob = $this->store
->getSession( self
::SESSIONID
);
658 $this->assertIsArray( $blob );
659 $this->assertArrayHasKey( 'metadata', $blob );
660 $metadata = $blob['metadata'];
661 $this->assertIsArray( $metadata );
662 $this->assertArrayHasKey( '???', $metadata );
663 $this->assertSame( '!!!', $metadata['???'] );
664 $this->assertIsArray( $this->store
->getSessionFromBackend( self
::SESSIONID
),
665 'making sure it did save to backend' );
667 // (also persists if forced)
668 $this->provider
= $builder->getMock();
669 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
670 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
671 $this->onSessionMetadataCalled
= false;
672 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
673 $this->store
->setSessionData( self
::SESSIONID
, $testData );
674 $backend = $this->getBackend( $user );
675 $this->store
->deleteSession( self
::SESSIONID
);
676 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
677 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
678 $this->assertTrue( $backend->isPersistent() );
679 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
680 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
682 $this->assertTrue( $this->onSessionMetadataCalled
);
683 $blob = $this->store
->getSession( self
::SESSIONID
);
684 $this->assertIsArray( $blob );
685 $this->assertArrayHasKey( 'metadata', $blob );
686 $metadata = $blob['metadata'];
687 $this->assertIsArray( $metadata );
688 $this->assertArrayHasKey( '???', $metadata );
689 $this->assertSame( '!!!', $metadata['???'] );
690 $this->assertIsArray( $this->store
->getSessionFromBackend( self
::SESSIONID
),
691 'making sure it did save to backend' );
693 // (also persists if metadata dirty)
694 $this->provider
= $builder->getMock();
695 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
696 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
697 $this->onSessionMetadataCalled
= false;
698 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
699 $this->store
->setSessionData( self
::SESSIONID
, $testData );
700 $backend = $this->getBackend( $user );
701 $this->store
->deleteSession( self
::SESSIONID
);
702 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
703 $this->assertTrue( $backend->isPersistent() );
704 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
705 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
707 $this->assertTrue( $this->onSessionMetadataCalled
);
708 $blob = $this->store
->getSession( self
::SESSIONID
);
709 $this->assertIsArray( $blob );
710 $this->assertArrayHasKey( 'metadata', $blob );
711 $metadata = $blob['metadata'];
712 $this->assertIsArray( $metadata );
713 $this->assertArrayHasKey( '???', $metadata );
714 $this->assertSame( '!!!', $metadata['???'] );
715 $this->assertIsArray( $this->store
->getSessionFromBackend( self
::SESSIONID
),
716 'making sure it did save to backend' );
718 // Not marked dirty, but dirty data
719 // (e.g. indirect modification from ArrayAccess::offsetGet)
720 $this->provider
= $neverProvider;
721 $this->onSessionMetadataCalled
= false;
722 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
723 $this->store
->setSessionData( self
::SESSIONID
, $testData );
724 $backend = $this->getBackend( $user );
725 $this->store
->deleteSession( self
::SESSIONID
);
726 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
727 $this->assertTrue( $backend->isPersistent() );
728 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
729 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
730 TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
732 $this->assertTrue( $this->onSessionMetadataCalled
);
733 $blob = $this->store
->getSession( self
::SESSIONID
);
734 $this->assertIsArray( $blob );
735 $this->assertArrayHasKey( 'metadata', $blob );
736 $metadata = $blob['metadata'];
737 $this->assertIsArray( $metadata );
738 $this->assertArrayHasKey( '???', $metadata );
739 $this->assertSame( '!!!', $metadata['???'] );
740 $this->assertIsArray( $this->store
->getSessionFromBackend( self
::SESSIONID
),
741 'making sure it did save to backend' );
744 $this->provider
= null;
745 $mockHook = $this->getMockBuilder( __CLASS__
)
746 ->onlyMethods( [ 'onSessionMetadata' ] )->getMock();
747 $mockHook->method( 'onSessionMetadata' )
748 ->willReturnCallback(
749 static function ( SessionBackend
$backend, array &$metadata, array $requests ) {
750 $metadata['userId']++
;
753 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
754 $this->store
->setSessionData( self
::SESSIONID
, $testData );
755 $backend = $this->getBackend( $user );
759 $this->fail( 'Expected exception not thrown' );
760 } catch ( UnexpectedValueException
$ex ) {
762 'SessionMetadata hook changed metadata key "userId"',
767 // SessionManager::preventSessionsForUser
768 TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
769 $user->getName() => true,
771 $this->provider
= $neverProvider;
772 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
773 $this->store
->setSessionData( self
::SESSIONID
, $testData );
774 $backend = $this->getBackend( $user );
775 $this->store
->deleteSession( self
::SESSIONID
);
776 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
777 $this->assertTrue( $backend->isPersistent() );
778 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
779 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
781 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
784 public function testRenew() {
785 $user = static::getTestSysop()->getUser();
786 $this->store
= new TestBagOStuff();
787 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
790 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
791 ->onlyMethods( [ 'persistSession' ] )->getMock();
792 $this->provider
->expects( $this->never() )->method( 'persistSession' );
793 $this->onSessionMetadataCalled
= false;
794 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
795 $this->store
->setSessionData( self
::SESSIONID
, $testData );
796 $backend = $this->getBackend( $user );
797 $this->store
->deleteSession( self
::SESSIONID
);
798 $wrap = TestingAccessWrapper
::newFromObject( $backend );
799 $this->assertFalse( $backend->isPersistent() );
800 $wrap->metaDirty
= false;
801 $wrap->dataDirty
= false;
802 $wrap->forcePersist
= false;
805 $this->assertTrue( $this->onSessionMetadataCalled
);
806 $blob = $this->store
->getSession( self
::SESSIONID
);
807 $this->assertIsArray( $blob );
808 $this->assertArrayHasKey( 'metadata', $blob );
809 $metadata = $blob['metadata'];
810 $this->assertIsArray( $metadata );
811 $this->assertArrayHasKey( '???', $metadata );
812 $this->assertSame( '!!!', $metadata['???'] );
813 $this->assertNotEquals( 0, $wrap->expires
);
816 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
817 ->onlyMethods( [ 'persistSession' ] )->getMock();
818 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
819 $this->onSessionMetadataCalled
= false;
820 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
821 $this->store
->setSessionData( self
::SESSIONID
, $testData );
822 $backend = $this->getBackend( $user );
823 $this->store
->deleteSession( self
::SESSIONID
);
824 $wrap = TestingAccessWrapper
::newFromObject( $backend );
825 $wrap->persist
= true;
826 $this->assertTrue( $backend->isPersistent() );
827 $wrap->metaDirty
= false;
828 $wrap->dataDirty
= false;
829 $wrap->forcePersist
= false;
832 $this->assertTrue( $this->onSessionMetadataCalled
);
833 $blob = $this->store
->getSession( self
::SESSIONID
);
834 $this->assertIsArray( $blob );
835 $this->assertArrayHasKey( 'metadata', $blob );
836 $metadata = $blob['metadata'];
837 $this->assertIsArray( $metadata );
838 $this->assertArrayHasKey( '???', $metadata );
839 $this->assertSame( '!!!', $metadata['???'] );
840 $this->assertNotEquals( 0, $wrap->expires
);
842 // Not persistent, not expiring
843 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
844 ->onlyMethods( [ 'persistSession' ] )->getMock();
845 $this->provider
->expects( $this->never() )->method( 'persistSession' );
846 $this->onSessionMetadataCalled
= false;
847 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
848 $this->store
->setSessionData( self
::SESSIONID
, $testData );
849 $backend = $this->getBackend( $user );
850 $this->store
->deleteSession( self
::SESSIONID
);
851 $wrap = TestingAccessWrapper
::newFromObject( $backend );
852 $this->assertFalse( $backend->isPersistent() );
853 $wrap->metaDirty
= false;
854 $wrap->dataDirty
= false;
855 $wrap->forcePersist
= false;
856 $expires = time() +
$wrap->lifetime +
100;
857 $wrap->expires
= $expires;
859 $this->assertFalse( $this->onSessionMetadataCalled
);
860 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
861 $this->assertEquals( $expires, $wrap->expires
);
864 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
865 $this->onSessionMetadataCalled
= true;
866 $metadata['???'] = '!!!';
869 public function testTakeOverGlobalSession() {
870 if ( !PHPSessionHandler
::isInstalled() ) {
871 PHPSessionHandler
::install( SessionManager
::singleton() );
873 if ( !PHPSessionHandler
::isEnabled() ) {
874 $staticAccess = TestingAccessWrapper
::newFromClass( PHPSessionHandler
::class );
875 $handler = TestingAccessWrapper
::newFromObject( $staticAccess->instance
);
876 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
877 session_write_close();
878 $handler->enable
= false;
880 $handler->enable
= true;
883 $backend = $this->getBackend( static::getTestSysop()->getUser() );
884 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
886 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
888 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
889 $request = RequestContext
::getMain()->getRequest();
890 $manager->globalSession
= $backend->getSession( $request );
891 $manager->globalSessionRequest
= $request;
894 TestingAccessWrapper
::newFromObject( $backend )->checkPHPSession();
895 $this->assertSame( $backend->getId(), session_id() );
896 session_write_close();
898 $backend2 = $this->getBackend(
899 User
::newFromName( 'TestTakeOverGlobalSession' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
901 TestingAccessWrapper
::newFromObject( $backend2 )->usePhpSessionHandling
= true;
904 TestingAccessWrapper
::newFromObject( $backend2 )->checkPHPSession();
905 $this->assertSame( '', session_id() );
908 public function testResetIdOfGlobalSession() {
909 if ( !PHPSessionHandler
::isInstalled() ) {
910 PHPSessionHandler
::install( SessionManager
::singleton() );
912 if ( !PHPSessionHandler
::isEnabled() ) {
913 $staticAccess = TestingAccessWrapper
::newFromClass( PHPSessionHandler
::class );
914 $handler = TestingAccessWrapper
::newFromObject( $staticAccess->instance
);
915 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
916 session_write_close();
917 $handler->enable
= false;
919 $handler->enable
= true;
922 $backend = $this->getBackend( User
::newFromName( 'TestResetIdOfGlobalSession' ) );
923 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
925 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
927 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
928 $request = RequestContext
::getMain()->getRequest();
929 $manager->globalSession
= $backend->getSession( $request );
930 $manager->globalSessionRequest
= $request;
932 session_id( self
::SESSIONID
);
934 $_SESSION['foo'] = __METHOD__
;
936 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
937 $this->assertSame( $backend->getId(), session_id() );
938 $this->assertArrayHasKey( 'foo', $_SESSION );
939 $this->assertSame( __METHOD__
, $_SESSION['foo'] );
940 session_write_close();
943 public function testUnpersistOfGlobalSession() {
944 if ( !PHPSessionHandler
::isInstalled() ) {
945 PHPSessionHandler
::install( SessionManager
::singleton() );
947 if ( !PHPSessionHandler
::isEnabled() ) {
948 $staticAccess = TestingAccessWrapper
::newFromClass( PHPSessionHandler
::class );
949 $handler = TestingAccessWrapper
::newFromObject( $staticAccess->instance
);
950 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
951 session_write_close();
952 $handler->enable
= false;
954 $handler->enable
= true;
957 $backend = $this->getBackend( User
::newFromName( 'TestUnpersistOfGlobalSession' ) );
958 $wrap = TestingAccessWrapper
::newFromObject( $backend );
959 $wrap->usePhpSessionHandling
= true;
960 $wrap->persist
= true;
962 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
964 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
965 $request = RequestContext
::getMain()->getRequest();
966 $manager->globalSession
= $backend->getSession( $request );
967 $manager->globalSessionRequest
= $request;
969 session_id( self
::SESSIONID
. 'x' );
971 $backend->unpersist();
972 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
973 session_write_close();
975 session_id( self
::SESSIONID
);
976 $wrap->persist
= true;
977 $backend->unpersist();
978 $this->assertSame( '', session_id() );
981 public function testGetAllowedUserRights() {
982 $this->provider
= $this->getMockBuilder( DummySessionProvider
::class )
983 ->onlyMethods( [ 'getAllowedUserRights' ] )
985 $this->provider
->method( 'getAllowedUserRights' )
986 ->willReturn( [ 'foo', 'bar' ] );
988 $backend = $this->getBackend();
989 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );