3 namespace MediaWiki\Session
;
5 use Wikimedia\AtEase\AtEase
;
9 use Wikimedia\TestingAccessWrapper
;
14 * @covers MediaWiki\Session\SessionBackend
16 class SessionBackendTest
extends MediaWikiTestCase
{
17 const SESSIONID
= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
19 /** @var SessionManager */
25 /** @var SessionProvider */
28 /** @var TestBagOStuff */
31 protected $onSessionMetadataCalled = false;
34 * Returns a non-persistent backend that thinks it has at least one session active
35 * @param User|null $user
37 * @return SessionBackend
39 protected function getBackend( User
$user = null, $id = null ) {
40 if ( !$this->config
) {
41 $this->config
= new \
HashConfig();
42 $this->manager
= null;
44 if ( !$this->store
) {
45 $this->store
= new TestBagOStuff();
46 $this->manager
= null;
49 $logger = new \Psr\Log\
NullLogger();
50 if ( !$this->manager
) {
51 $this->manager
= new SessionManager( [
52 'store' => $this->store
,
54 'config' => $this->config
,
58 if ( !$this->provider
) {
59 $this->provider
= new \
DummySessionProvider();
61 $this->provider
->setLogger( $logger );
62 $this->provider
->setConfig( $this->config
);
63 $this->provider
->setManager( $this->manager
);
65 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
66 'provider' => $this->provider
,
67 'id' => $id ?
: self
::SESSIONID
,
69 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
72 $id = new SessionId( $info->getId() );
74 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
75 $priv = TestingAccessWrapper
::newFromObject( $backend );
76 $priv->persist
= false;
77 $priv->requests
= [ 100 => new \
FauxRequest() ];
78 $priv->requests
[100]->setSessionId( $id );
79 $priv->usePhpSessionHandling
= false;
81 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
82 $manager->allSessionBackends
= [ $backend->getId() => $backend ] +
$manager->allSessionBackends
;
83 $manager->allSessionIds
= [ $backend->getId() => $id ] +
$manager->allSessionIds
;
84 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
89 public function testConstructor() {
93 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
94 'provider' => $this->provider
,
95 'id' => self
::SESSIONID
,
97 'userInfo' => UserInfo
::newFromName( 'UTSysop', false ),
100 $id = new SessionId( $info->getId() );
101 $logger = new \Psr\Log\
NullLogger();
103 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
104 $this->fail( 'Expected exception not thrown' );
105 } catch ( \InvalidArgumentException
$ex ) {
107 "Refusing to create session for unverified user {$info->getUserInfo()}",
112 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
113 'id' => self
::SESSIONID
,
114 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
117 $id = new SessionId( $info->getId() );
119 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
120 $this->fail( 'Expected exception not thrown' );
121 } catch ( \InvalidArgumentException
$ex ) {
122 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
125 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
126 'provider' => $this->provider
,
127 'id' => self
::SESSIONID
,
129 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
132 $id = new SessionId( '!' . $info->getId() );
134 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
135 $this->fail( 'Expected exception not thrown' );
136 } catch ( \InvalidArgumentException
$ex ) {
138 'SessionId and SessionInfo don\'t match',
143 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
144 'provider' => $this->provider
,
145 'id' => self
::SESSIONID
,
147 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
150 $id = new SessionId( $info->getId() );
151 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
152 $this->assertSame( self
::SESSIONID
, $backend->getId() );
153 $this->assertSame( $id, $backend->getSessionId() );
154 $this->assertSame( $this->provider
, $backend->getProvider() );
155 $this->assertInstanceOf( User
::class, $backend->getUser() );
156 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
157 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
158 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
159 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
161 $expire = time() +
100;
162 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ] );
164 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
165 'provider' => $this->provider
,
166 'id' => self
::SESSIONID
,
168 'forceHTTPS' => true,
169 'metadata' => [ 'foo' ],
172 $id = new SessionId( $info->getId() );
173 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
174 $this->assertSame( self
::SESSIONID
, $backend->getId() );
175 $this->assertSame( $id, $backend->getSessionId() );
176 $this->assertSame( $this->provider
, $backend->getProvider() );
177 $this->assertInstanceOf( User
::class, $backend->getUser() );
178 $this->assertTrue( $backend->getUser()->isAnon() );
179 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
180 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
181 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
182 $this->assertSame( $expire, TestingAccessWrapper
::newFromObject( $backend )->expires
);
183 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
186 public function testSessionStuff() {
187 $backend = $this->getBackend();
188 $priv = TestingAccessWrapper
::newFromObject( $backend );
189 $priv->requests
= []; // Remove dummy session
191 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
193 $request1 = new \
FauxRequest();
194 $session1 = $backend->getSession( $request1 );
195 $request2 = new \
FauxRequest();
196 $session2 = $backend->getSession( $request2 );
198 $this->assertInstanceOf( Session
::class, $session1 );
199 $this->assertInstanceOf( Session
::class, $session2 );
200 $this->assertSame( 2, count( $priv->requests
) );
202 $index = TestingAccessWrapper
::newFromObject( $session1 )->index
;
204 $this->assertSame( $request1, $backend->getRequest( $index ) );
205 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
206 $request1->setCookie( 'UserName', 'Example' );
207 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
210 $this->assertSame( 1, count( $priv->requests
) );
211 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
212 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
214 $backend->getRequest( $index );
215 $this->fail( 'Expected exception not thrown' );
216 } catch ( \InvalidArgumentException
$ex ) {
217 $this->assertSame( 'Invalid session index', $ex->getMessage() );
220 $backend->suggestLoginUsername( $index );
221 $this->fail( 'Expected exception not thrown' );
222 } catch ( \InvalidArgumentException
$ex ) {
223 $this->assertSame( 'Invalid session index', $ex->getMessage() );
227 $this->assertSame( 0, count( $priv->requests
) );
228 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
229 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
232 public function testSetProviderMetadata() {
233 $backend = $this->getBackend();
234 $priv = TestingAccessWrapper
::newFromObject( $backend );
235 $priv->providerMetadata
= [ 'dummy' ];
238 $backend->setProviderMetadata( 'foo' );
239 $this->fail( 'Expected exception not thrown' );
240 } catch ( \InvalidArgumentException
$ex ) {
241 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
245 $backend->setProviderMetadata( (object)[] );
246 $this->fail( 'Expected exception not thrown' );
247 } catch ( \InvalidArgumentException
$ex ) {
248 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
251 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
252 $backend->setProviderMetadata( [ 'dummy' ] );
253 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
255 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
256 $backend->setProviderMetadata( [ 'test' ] );
257 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
258 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
259 $this->store
->deleteSession( self
::SESSIONID
);
261 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
262 $backend->setProviderMetadata( null );
263 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
264 $this->assertSame( null, $backend->getProviderMetadata() );
265 $this->store
->deleteSession( self
::SESSIONID
);
268 public function testResetId() {
271 $builder = $this->getMockBuilder( \DummySessionProvider
::class )
272 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
274 $this->provider
= $builder->getMock();
275 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
276 ->will( $this->returnValue( false ) );
277 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
278 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
279 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
280 $sessionId = $backend->getSessionId();
282 $this->assertSame( self
::SESSIONID
, $backend->getId() );
283 $this->assertSame( $backend->getId(), $sessionId->getId() );
284 $this->assertSame( $id, session_id() );
285 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
287 $this->provider
= $builder->getMock();
288 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
289 ->will( $this->returnValue( true ) );
290 $backend = $this->getBackend();
291 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
292 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
293 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
294 $sessionId = $backend->getSessionId();
296 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
297 $this->assertSame( $backend->getId(), $sessionId->getId() );
298 $this->assertInternalType( 'array', $this->store
->getSession( $backend->getId() ) );
299 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
300 $this->assertSame( $id, session_id() );
301 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
302 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
303 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
306 public function testPersist() {
307 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
308 ->setMethods( [ 'persistSession' ] )->getMock();
309 $this->provider
->expects( $this->once() )->method( 'persistSession' );
310 $backend = $this->getBackend();
311 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
312 $backend->save(); // This one shouldn't call $provider->persistSession()
315 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
317 $this->provider
= null;
318 $backend = $this->getBackend();
319 $wrap = TestingAccessWrapper
::newFromObject( $backend );
320 $wrap->persist
= true;
323 $this->assertNotEquals( 0, $wrap->expires
);
326 public function testUnpersist() {
327 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
328 ->setMethods( [ 'unpersistSession' ] )->getMock();
329 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
330 $backend = $this->getBackend();
331 $wrap = TestingAccessWrapper
::newFromObject( $backend );
332 $wrap->store
= new \
CachedBagOStuff( $this->store
);
333 $wrap->persist
= true;
334 $wrap->dataDirty
= true;
336 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
337 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
338 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
340 $backend->unpersist();
341 $this->assertFalse( $backend->isPersistent() );
342 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
343 $this->assertNotFalse(
344 $wrap->store
->get( $wrap->store
->makeKey( 'MWSession', self
::SESSIONID
) )
348 public function testRememberUser() {
349 $backend = $this->getBackend();
351 $remembered = $backend->shouldRememberUser();
352 $backend->setRememberUser( !$remembered );
353 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
354 $backend->setRememberUser( $remembered );
355 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
358 public function testForceHTTPS() {
359 $backend = $this->getBackend();
361 $force = $backend->shouldForceHTTPS();
362 $backend->setForceHTTPS( !$force );
363 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
364 $backend->setForceHTTPS( $force );
365 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
368 public function testLoggedOutTimestamp() {
369 $backend = $this->getBackend();
371 $backend->setLoggedOutTimestamp( 42 );
372 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
373 $backend->setLoggedOutTimestamp( '123' );
374 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
377 public function testSetUser() {
378 $user = static::getTestSysop()->getUser();
380 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
381 ->setMethods( [ 'canChangeUser' ] )->getMock();
382 $this->provider
->expects( $this->any() )->method( 'canChangeUser' )
383 ->will( $this->returnValue( false ) );
384 $backend = $this->getBackend();
385 $this->assertFalse( $backend->canSetUser() );
387 $backend->setUser( $user );
388 $this->fail( 'Expected exception not thrown' );
389 } catch ( \BadMethodCallException
$ex ) {
391 'Cannot set user on this session; check $session->canSetUser() first',
395 $this->assertNotSame( $user, $backend->getUser() );
397 $this->provider
= null;
398 $backend = $this->getBackend();
399 $this->assertTrue( $backend->canSetUser() );
400 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
401 $backend->setUser( $user );
402 $this->assertSame( $user, $backend->getUser() );
405 public function testDirty() {
406 $backend = $this->getBackend();
407 $priv = TestingAccessWrapper
::newFromObject( $backend );
408 $priv->dataDirty
= false;
410 $this->assertTrue( $priv->dataDirty
);
413 public function testGetData() {
414 $backend = $this->getBackend();
415 $data = $backend->getData();
416 $this->assertSame( [], $data );
417 $this->assertTrue( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
418 $data['???'] = '!!!';
419 $this->assertSame( [ '???' => '!!!' ], $data );
421 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
422 $this->store
->setSessionData( self
::SESSIONID
, $testData );
423 $backend = $this->getBackend();
424 $this->assertSame( $testData, $backend->getData() );
425 $this->assertFalse( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
428 public function testAddData() {
429 $backend = $this->getBackend();
430 $priv = TestingAccessWrapper
::newFromObject( $backend );
432 $priv->data
= [ 'foo' => 1 ];
433 $priv->dataDirty
= false;
434 $backend->addData( [ 'foo' => 1 ] );
435 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
436 $this->assertFalse( $priv->dataDirty
);
438 $priv->data
= [ 'foo' => 1 ];
439 $priv->dataDirty
= false;
440 $backend->addData( [ 'foo' => '1' ] );
441 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
442 $this->assertTrue( $priv->dataDirty
);
444 $priv->data
= [ 'foo' => 1 ];
445 $priv->dataDirty
= false;
446 $backend->addData( [ 'bar' => 2 ] );
447 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
448 $this->assertTrue( $priv->dataDirty
);
451 public function testDelaySave() {
452 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
453 $backend = $this->getBackend();
454 $priv = TestingAccessWrapper
::newFromObject( $backend );
455 $priv->persist
= true;
457 // Saves happen normally when no delay is in effect
458 $this->onSessionMetadataCalled
= false;
459 $priv->metaDirty
= true;
461 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
463 $this->onSessionMetadataCalled
= false;
464 $priv->metaDirty
= true;
466 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
468 $delay = $backend->delaySave();
470 // Autosave doesn't happen when no delay is in effect
471 $this->onSessionMetadataCalled
= false;
472 $priv->metaDirty
= true;
474 $this->assertFalse( $this->onSessionMetadataCalled
);
476 // Save still does happen when no delay is in effect
478 $this->assertTrue( $this->onSessionMetadataCalled
);
480 // Save happens when delay is consumed
481 $this->onSessionMetadataCalled
= false;
482 $priv->metaDirty
= true;
483 \Wikimedia\ScopedCallback
::consume( $delay );
484 $this->assertTrue( $this->onSessionMetadataCalled
);
486 // Test multiple delays
487 $delay1 = $backend->delaySave();
488 $delay2 = $backend->delaySave();
489 $delay3 = $backend->delaySave();
490 $this->onSessionMetadataCalled
= false;
491 $priv->metaDirty
= true;
493 $this->assertFalse( $this->onSessionMetadataCalled
);
494 \Wikimedia\ScopedCallback
::consume( $delay3 );
495 $this->assertFalse( $this->onSessionMetadataCalled
);
496 \Wikimedia\ScopedCallback
::consume( $delay1 );
497 $this->assertFalse( $this->onSessionMetadataCalled
);
498 \Wikimedia\ScopedCallback
::consume( $delay2 );
499 $this->assertTrue( $this->onSessionMetadataCalled
);
502 public function testSave() {
503 $user = static::getTestSysop()->getUser();
504 $this->store
= new TestBagOStuff();
505 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
507 $neverHook = $this->getMockBuilder( __CLASS__
)
508 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
509 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
511 $builder = $this->getMockBuilder( \DummySessionProvider
::class )
512 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
514 $neverProvider = $builder->getMock();
515 $neverProvider->expects( $this->never() )->method( 'persistSession' );
516 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
518 // Not persistent or dirty
519 $this->provider
= $neverProvider;
520 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
521 $this->store
->setSessionData( self
::SESSIONID
, $testData );
522 $backend = $this->getBackend( $user );
523 $this->store
->deleteSession( self
::SESSIONID
);
524 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
525 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
526 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
528 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
530 // (but does unpersist if forced)
531 $this->provider
= $builder->getMock();
532 $this->provider
->expects( $this->never() )->method( 'persistSession' );
533 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
534 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
535 $this->store
->setSessionData( self
::SESSIONID
, $testData );
536 $backend = $this->getBackend( $user );
537 $this->store
->deleteSession( self
::SESSIONID
);
538 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
539 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
540 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
541 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
542 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
544 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
546 // (but not to a WebRequest associated with a different session)
547 $this->provider
= $neverProvider;
548 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
549 $this->store
->setSessionData( self
::SESSIONID
, $testData );
550 $backend = $this->getBackend( $user );
551 TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
552 ->setSessionId( new SessionId( 'x' ) );
553 $this->store
->deleteSession( self
::SESSIONID
);
554 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
555 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
556 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
557 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
558 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
560 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
562 // Not persistent, but dirty
563 $this->provider
= $neverProvider;
564 $this->onSessionMetadataCalled
= false;
565 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
566 $this->store
->setSessionData( self
::SESSIONID
, $testData );
567 $backend = $this->getBackend( $user );
568 $this->store
->deleteSession( self
::SESSIONID
);
569 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
570 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
571 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
573 $this->assertTrue( $this->onSessionMetadataCalled
);
574 $blob = $this->store
->getSession( self
::SESSIONID
);
575 $this->assertInternalType( 'array', $blob );
576 $this->assertArrayHasKey( 'metadata', $blob );
577 $metadata = $blob['metadata'];
578 $this->assertInternalType( 'array', $metadata );
579 $this->assertArrayHasKey( '???', $metadata );
580 $this->assertSame( '!!!', $metadata['???'] );
581 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
582 'making sure it didn\'t save to backend' );
584 // Persistent, not dirty
585 $this->provider
= $neverProvider;
586 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
587 $this->store
->setSessionData( self
::SESSIONID
, $testData );
588 $backend = $this->getBackend( $user );
589 $this->store
->deleteSession( self
::SESSIONID
);
590 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
591 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
592 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
593 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
595 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
597 // (but will persist if forced)
598 $this->provider
= $builder->getMock();
599 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
600 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
601 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
602 $this->store
->setSessionData( self
::SESSIONID
, $testData );
603 $backend = $this->getBackend( $user );
604 $this->store
->deleteSession( self
::SESSIONID
);
605 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
606 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
607 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
608 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
609 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
611 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
613 // Persistent and dirty
614 $this->provider
= $neverProvider;
615 $this->onSessionMetadataCalled
= false;
616 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
617 $this->store
->setSessionData( self
::SESSIONID
, $testData );
618 $backend = $this->getBackend( $user );
619 $this->store
->deleteSession( self
::SESSIONID
);
620 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
621 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
622 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
623 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
625 $this->assertTrue( $this->onSessionMetadataCalled
);
626 $blob = $this->store
->getSession( self
::SESSIONID
);
627 $this->assertInternalType( 'array', $blob );
628 $this->assertArrayHasKey( 'metadata', $blob );
629 $metadata = $blob['metadata'];
630 $this->assertInternalType( 'array', $metadata );
631 $this->assertArrayHasKey( '???', $metadata );
632 $this->assertSame( '!!!', $metadata['???'] );
633 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
634 'making sure it did save to backend' );
636 // (also persists if forced)
637 $this->provider
= $builder->getMock();
638 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
639 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
640 $this->onSessionMetadataCalled
= false;
641 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
642 $this->store
->setSessionData( self
::SESSIONID
, $testData );
643 $backend = $this->getBackend( $user );
644 $this->store
->deleteSession( self
::SESSIONID
);
645 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
646 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
647 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
648 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
649 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
651 $this->assertTrue( $this->onSessionMetadataCalled
);
652 $blob = $this->store
->getSession( self
::SESSIONID
);
653 $this->assertInternalType( 'array', $blob );
654 $this->assertArrayHasKey( 'metadata', $blob );
655 $metadata = $blob['metadata'];
656 $this->assertInternalType( 'array', $metadata );
657 $this->assertArrayHasKey( '???', $metadata );
658 $this->assertSame( '!!!', $metadata['???'] );
659 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
660 'making sure it did save to backend' );
662 // (also persists if metadata dirty)
663 $this->provider
= $builder->getMock();
664 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
665 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
666 $this->onSessionMetadataCalled
= false;
667 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
668 $this->store
->setSessionData( self
::SESSIONID
, $testData );
669 $backend = $this->getBackend( $user );
670 $this->store
->deleteSession( self
::SESSIONID
);
671 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
672 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
673 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
674 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
676 $this->assertTrue( $this->onSessionMetadataCalled
);
677 $blob = $this->store
->getSession( self
::SESSIONID
);
678 $this->assertInternalType( 'array', $blob );
679 $this->assertArrayHasKey( 'metadata', $blob );
680 $metadata = $blob['metadata'];
681 $this->assertInternalType( 'array', $metadata );
682 $this->assertArrayHasKey( '???', $metadata );
683 $this->assertSame( '!!!', $metadata['???'] );
684 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
685 'making sure it did save to backend' );
687 // Not marked dirty, but dirty data
688 // (e.g. indirect modification from ArrayAccess::offsetGet)
689 $this->provider
= $neverProvider;
690 $this->onSessionMetadataCalled
= false;
691 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
692 $this->store
->setSessionData( self
::SESSIONID
, $testData );
693 $backend = $this->getBackend( $user );
694 $this->store
->deleteSession( self
::SESSIONID
);
695 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
696 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
697 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
698 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
699 TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
701 $this->assertTrue( $this->onSessionMetadataCalled
);
702 $blob = $this->store
->getSession( self
::SESSIONID
);
703 $this->assertInternalType( 'array', $blob );
704 $this->assertArrayHasKey( 'metadata', $blob );
705 $metadata = $blob['metadata'];
706 $this->assertInternalType( 'array', $metadata );
707 $this->assertArrayHasKey( '???', $metadata );
708 $this->assertSame( '!!!', $metadata['???'] );
709 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
710 'making sure it did save to backend' );
713 $this->provider
= null;
714 $mockHook = $this->getMockBuilder( __CLASS__
)
715 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
716 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
717 ->will( $this->returnCallback(
718 function ( SessionBackend
$backend, array &$metadata, array $requests ) {
719 $metadata['userId']++
;
722 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
723 $this->store
->setSessionData( self
::SESSIONID
, $testData );
724 $backend = $this->getBackend( $user );
728 $this->fail( 'Expected exception not thrown' );
729 } catch ( \UnexpectedValueException
$ex ) {
731 'SessionMetadata hook changed metadata key "userId"',
736 // SessionManager::preventSessionsForUser
737 TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
738 $user->getName() => true,
740 $this->provider
= $neverProvider;
741 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
742 $this->store
->setSessionData( self
::SESSIONID
, $testData );
743 $backend = $this->getBackend( $user );
744 $this->store
->deleteSession( self
::SESSIONID
);
745 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
746 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
747 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
748 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
750 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
753 public function testRenew() {
754 $user = static::getTestSysop()->getUser();
755 $this->store
= new TestBagOStuff();
756 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
759 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
760 ->setMethods( [ 'persistSession' ] )->getMock();
761 $this->provider
->expects( $this->never() )->method( 'persistSession' );
762 $this->onSessionMetadataCalled
= false;
763 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
764 $this->store
->setSessionData( self
::SESSIONID
, $testData );
765 $backend = $this->getBackend( $user );
766 $this->store
->deleteSession( self
::SESSIONID
);
767 $wrap = TestingAccessWrapper
::newFromObject( $backend );
768 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
769 $wrap->metaDirty
= false;
770 $wrap->dataDirty
= false;
771 $wrap->forcePersist
= false;
774 $this->assertTrue( $this->onSessionMetadataCalled
);
775 $blob = $this->store
->getSession( self
::SESSIONID
);
776 $this->assertInternalType( 'array', $blob );
777 $this->assertArrayHasKey( 'metadata', $blob );
778 $metadata = $blob['metadata'];
779 $this->assertInternalType( 'array', $metadata );
780 $this->assertArrayHasKey( '???', $metadata );
781 $this->assertSame( '!!!', $metadata['???'] );
782 $this->assertNotEquals( 0, $wrap->expires
);
785 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
786 ->setMethods( [ 'persistSession' ] )->getMock();
787 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
788 $this->onSessionMetadataCalled
= false;
789 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
790 $this->store
->setSessionData( self
::SESSIONID
, $testData );
791 $backend = $this->getBackend( $user );
792 $this->store
->deleteSession( self
::SESSIONID
);
793 $wrap = TestingAccessWrapper
::newFromObject( $backend );
794 $wrap->persist
= true;
795 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
796 $wrap->metaDirty
= false;
797 $wrap->dataDirty
= false;
798 $wrap->forcePersist
= false;
801 $this->assertTrue( $this->onSessionMetadataCalled
);
802 $blob = $this->store
->getSession( self
::SESSIONID
);
803 $this->assertInternalType( 'array', $blob );
804 $this->assertArrayHasKey( 'metadata', $blob );
805 $metadata = $blob['metadata'];
806 $this->assertInternalType( 'array', $metadata );
807 $this->assertArrayHasKey( '???', $metadata );
808 $this->assertSame( '!!!', $metadata['???'] );
809 $this->assertNotEquals( 0, $wrap->expires
);
811 // Not persistent, not expiring
812 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
813 ->setMethods( [ 'persistSession' ] )->getMock();
814 $this->provider
->expects( $this->never() )->method( 'persistSession' );
815 $this->onSessionMetadataCalled
= false;
816 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
817 $this->store
->setSessionData( self
::SESSIONID
, $testData );
818 $backend = $this->getBackend( $user );
819 $this->store
->deleteSession( self
::SESSIONID
);
820 $wrap = TestingAccessWrapper
::newFromObject( $backend );
821 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
822 $wrap->metaDirty
= false;
823 $wrap->dataDirty
= false;
824 $wrap->forcePersist
= false;
825 $expires = time() +
$wrap->lifetime +
100;
826 $wrap->expires
= $expires;
828 $this->assertFalse( $this->onSessionMetadataCalled
);
829 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
830 $this->assertEquals( $expires, $wrap->expires
);
833 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
834 $this->onSessionMetadataCalled
= true;
835 $metadata['???'] = '!!!';
838 public function testTakeOverGlobalSession() {
839 if ( !PHPSessionHandler
::isInstalled() ) {
840 PHPSessionHandler
::install( SessionManager
::singleton() );
842 if ( !PHPSessionHandler
::isEnabled() ) {
843 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
844 $rProp->setAccessible( true );
845 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
846 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
847 session_write_close();
848 $handler->enable
= false;
850 $handler->enable
= true;
853 $backend = $this->getBackend( static::getTestSysop()->getUser() );
854 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
856 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
858 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
859 $request = \RequestContext
::getMain()->getRequest();
860 $manager->globalSession
= $backend->getSession( $request );
861 $manager->globalSessionRequest
= $request;
864 TestingAccessWrapper
::newFromObject( $backend )->checkPHPSession();
865 $this->assertSame( $backend->getId(), session_id() );
866 session_write_close();
868 $backend2 = $this->getBackend(
869 User
::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
871 TestingAccessWrapper
::newFromObject( $backend2 )->usePhpSessionHandling
= true;
874 TestingAccessWrapper
::newFromObject( $backend2 )->checkPHPSession();
875 $this->assertSame( '', session_id() );
878 public function testResetIdOfGlobalSession() {
879 if ( !PHPSessionHandler
::isInstalled() ) {
880 PHPSessionHandler
::install( SessionManager
::singleton() );
882 if ( !PHPSessionHandler
::isEnabled() ) {
883 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
884 $rProp->setAccessible( true );
885 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
886 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
887 session_write_close();
888 $handler->enable
= false;
890 $handler->enable
= true;
893 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
894 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
896 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
898 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
899 $request = \RequestContext
::getMain()->getRequest();
900 $manager->globalSession
= $backend->getSession( $request );
901 $manager->globalSessionRequest
= $request;
903 session_id( self
::SESSIONID
);
904 AtEase
::quietCall( 'session_start' );
905 $_SESSION['foo'] = __METHOD__
;
907 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
908 $this->assertSame( $backend->getId(), session_id() );
909 $this->assertArrayHasKey( 'foo', $_SESSION );
910 $this->assertSame( __METHOD__
, $_SESSION['foo'] );
911 session_write_close();
914 public function testUnpersistOfGlobalSession() {
915 if ( !PHPSessionHandler
::isInstalled() ) {
916 PHPSessionHandler
::install( SessionManager
::singleton() );
918 if ( !PHPSessionHandler
::isEnabled() ) {
919 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
920 $rProp->setAccessible( true );
921 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
922 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
923 session_write_close();
924 $handler->enable
= false;
926 $handler->enable
= true;
929 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
930 $wrap = TestingAccessWrapper
::newFromObject( $backend );
931 $wrap->usePhpSessionHandling
= true;
932 $wrap->persist
= true;
934 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
936 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
937 $request = \RequestContext
::getMain()->getRequest();
938 $manager->globalSession
= $backend->getSession( $request );
939 $manager->globalSessionRequest
= $request;
941 session_id( self
::SESSIONID
. 'x' );
942 AtEase
::quietCall( 'session_start' );
943 $backend->unpersist();
944 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
945 session_write_close();
947 session_id( self
::SESSIONID
);
948 $wrap->persist
= true;
949 $backend->unpersist();
950 $this->assertSame( '', session_id() );
953 public function testGetAllowedUserRights() {
954 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
955 ->setMethods( [ 'getAllowedUserRights' ] )
957 $this->provider
->expects( $this->any() )->method( 'getAllowedUserRights' )
958 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
960 $backend = $this->getBackend();
961 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );