3 namespace MediaWiki\Session
;
8 use Wikimedia\TestingAccessWrapper
;
13 * @covers MediaWiki\Session\SessionBackend
15 class SessionBackendTest
extends MediaWikiTestCase
{
16 const SESSIONID
= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
18 /** @var SessionManager */
24 /** @var SessionProvider */
27 /** @var TestBagOStuff */
30 protected $onSessionMetadataCalled = false;
33 * Returns a non-persistent backend that thinks it has at least one session active
34 * @param User|null $user
36 * @return SessionBackend
38 protected function getBackend( User
$user = null, $id = null ) {
39 if ( !$this->config
) {
40 $this->config
= new \
HashConfig();
41 $this->manager
= null;
43 if ( !$this->store
) {
44 $this->store
= new TestBagOStuff();
45 $this->manager
= null;
48 $logger = new \Psr\Log\
NullLogger();
49 if ( !$this->manager
) {
50 $this->manager
= new SessionManager( [
51 'store' => $this->store
,
53 'config' => $this->config
,
57 if ( !$this->provider
) {
58 $this->provider
= new \
DummySessionProvider();
60 $this->provider
->setLogger( $logger );
61 $this->provider
->setConfig( $this->config
);
62 $this->provider
->setManager( $this->manager
);
64 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
65 'provider' => $this->provider
,
66 'id' => $id ?
: self
::SESSIONID
,
68 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
71 $id = new SessionId( $info->getId() );
73 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
74 $priv = TestingAccessWrapper
::newFromObject( $backend );
75 $priv->persist
= false;
76 $priv->requests
= [ 100 => new \
FauxRequest() ];
77 $priv->requests
[100]->setSessionId( $id );
78 $priv->usePhpSessionHandling
= false;
80 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
81 $manager->allSessionBackends
= [ $backend->getId() => $backend ] +
$manager->allSessionBackends
;
82 $manager->allSessionIds
= [ $backend->getId() => $id ] +
$manager->allSessionIds
;
83 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
88 public function testConstructor() {
92 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
93 'provider' => $this->provider
,
94 'id' => self
::SESSIONID
,
96 'userInfo' => UserInfo
::newFromName( 'UTSysop', false ),
99 $id = new SessionId( $info->getId() );
100 $logger = new \Psr\Log\
NullLogger();
102 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
103 $this->fail( 'Expected exception not thrown' );
104 } catch ( \InvalidArgumentException
$ex ) {
106 "Refusing to create session for unverified user {$info->getUserInfo()}",
111 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
112 'id' => self
::SESSIONID
,
113 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
116 $id = new SessionId( $info->getId() );
118 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
119 $this->fail( 'Expected exception not thrown' );
120 } catch ( \InvalidArgumentException
$ex ) {
121 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
124 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
125 'provider' => $this->provider
,
126 'id' => self
::SESSIONID
,
128 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
131 $id = new SessionId( '!' . $info->getId() );
133 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
134 $this->fail( 'Expected exception not thrown' );
135 } catch ( \InvalidArgumentException
$ex ) {
137 'SessionId and SessionInfo don\'t match',
142 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
143 'provider' => $this->provider
,
144 'id' => self
::SESSIONID
,
146 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
149 $id = new SessionId( $info->getId() );
150 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
151 $this->assertSame( self
::SESSIONID
, $backend->getId() );
152 $this->assertSame( $id, $backend->getSessionId() );
153 $this->assertSame( $this->provider
, $backend->getProvider() );
154 $this->assertInstanceOf( User
::class, $backend->getUser() );
155 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
156 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
157 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
158 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
160 $expire = time() +
100;
161 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ] );
163 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
164 'provider' => $this->provider
,
165 'id' => self
::SESSIONID
,
167 'forceHTTPS' => true,
168 'metadata' => [ 'foo' ],
171 $id = new SessionId( $info->getId() );
172 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
173 $this->assertSame( self
::SESSIONID
, $backend->getId() );
174 $this->assertSame( $id, $backend->getSessionId() );
175 $this->assertSame( $this->provider
, $backend->getProvider() );
176 $this->assertInstanceOf( User
::class, $backend->getUser() );
177 $this->assertTrue( $backend->getUser()->isAnon() );
178 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
179 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
180 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
181 $this->assertSame( $expire, TestingAccessWrapper
::newFromObject( $backend )->expires
);
182 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
185 public function testSessionStuff() {
186 $backend = $this->getBackend();
187 $priv = TestingAccessWrapper
::newFromObject( $backend );
188 $priv->requests
= []; // Remove dummy session
190 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
192 $request1 = new \
FauxRequest();
193 $session1 = $backend->getSession( $request1 );
194 $request2 = new \
FauxRequest();
195 $session2 = $backend->getSession( $request2 );
197 $this->assertInstanceOf( Session
::class, $session1 );
198 $this->assertInstanceOf( Session
::class, $session2 );
199 $this->assertSame( 2, count( $priv->requests
) );
201 $index = TestingAccessWrapper
::newFromObject( $session1 )->index
;
203 $this->assertSame( $request1, $backend->getRequest( $index ) );
204 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
205 $request1->setCookie( 'UserName', 'Example' );
206 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
209 $this->assertSame( 1, count( $priv->requests
) );
210 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
211 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
213 $backend->getRequest( $index );
214 $this->fail( 'Expected exception not thrown' );
215 } catch ( \InvalidArgumentException
$ex ) {
216 $this->assertSame( 'Invalid session index', $ex->getMessage() );
219 $backend->suggestLoginUsername( $index );
220 $this->fail( 'Expected exception not thrown' );
221 } catch ( \InvalidArgumentException
$ex ) {
222 $this->assertSame( 'Invalid session index', $ex->getMessage() );
226 $this->assertSame( 0, count( $priv->requests
) );
227 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
228 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
231 public function testSetProviderMetadata() {
232 $backend = $this->getBackend();
233 $priv = TestingAccessWrapper
::newFromObject( $backend );
234 $priv->providerMetadata
= [ 'dummy' ];
237 $backend->setProviderMetadata( 'foo' );
238 $this->fail( 'Expected exception not thrown' );
239 } catch ( \InvalidArgumentException
$ex ) {
240 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
244 $backend->setProviderMetadata( (object)[] );
245 $this->fail( 'Expected exception not thrown' );
246 } catch ( \InvalidArgumentException
$ex ) {
247 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
250 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
251 $backend->setProviderMetadata( [ 'dummy' ] );
252 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
254 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
255 $backend->setProviderMetadata( [ 'test' ] );
256 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
257 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
258 $this->store
->deleteSession( self
::SESSIONID
);
260 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
261 $backend->setProviderMetadata( null );
262 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
263 $this->assertSame( null, $backend->getProviderMetadata() );
264 $this->store
->deleteSession( self
::SESSIONID
);
267 public function testResetId() {
270 $builder = $this->getMockBuilder( \DummySessionProvider
::class )
271 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
273 $this->provider
= $builder->getMock();
274 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
275 ->will( $this->returnValue( false ) );
276 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
277 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
278 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
279 $sessionId = $backend->getSessionId();
281 $this->assertSame( self
::SESSIONID
, $backend->getId() );
282 $this->assertSame( $backend->getId(), $sessionId->getId() );
283 $this->assertSame( $id, session_id() );
284 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
286 $this->provider
= $builder->getMock();
287 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
288 ->will( $this->returnValue( true ) );
289 $backend = $this->getBackend();
290 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
291 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
292 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
293 $sessionId = $backend->getSessionId();
295 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
296 $this->assertSame( $backend->getId(), $sessionId->getId() );
297 $this->assertInternalType( 'array', $this->store
->getSession( $backend->getId() ) );
298 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
299 $this->assertSame( $id, session_id() );
300 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
301 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
302 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
305 public function testPersist() {
306 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
307 ->setMethods( [ 'persistSession' ] )->getMock();
308 $this->provider
->expects( $this->once() )->method( 'persistSession' );
309 $backend = $this->getBackend();
310 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
311 $backend->save(); // This one shouldn't call $provider->persistSession()
314 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
316 $this->provider
= null;
317 $backend = $this->getBackend();
318 $wrap = TestingAccessWrapper
::newFromObject( $backend );
319 $wrap->persist
= true;
322 $this->assertNotEquals( 0, $wrap->expires
);
325 public function testUnpersist() {
326 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
327 ->setMethods( [ 'unpersistSession' ] )->getMock();
328 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
329 $backend = $this->getBackend();
330 $wrap = TestingAccessWrapper
::newFromObject( $backend );
331 $wrap->store
= new \
CachedBagOStuff( $this->store
);
332 $wrap->persist
= true;
333 $wrap->dataDirty
= true;
335 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
336 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
337 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
339 $backend->unpersist();
340 $this->assertFalse( $backend->isPersistent() );
341 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
342 $this->assertNotFalse(
343 $wrap->store
->get( $wrap->store
->makeKey( 'MWSession', self
::SESSIONID
) )
347 public function testRememberUser() {
348 $backend = $this->getBackend();
350 $remembered = $backend->shouldRememberUser();
351 $backend->setRememberUser( !$remembered );
352 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
353 $backend->setRememberUser( $remembered );
354 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
357 public function testForceHTTPS() {
358 $backend = $this->getBackend();
360 $force = $backend->shouldForceHTTPS();
361 $backend->setForceHTTPS( !$force );
362 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
363 $backend->setForceHTTPS( $force );
364 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
367 public function testLoggedOutTimestamp() {
368 $backend = $this->getBackend();
370 $backend->setLoggedOutTimestamp( 42 );
371 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
372 $backend->setLoggedOutTimestamp( '123' );
373 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
376 public function testSetUser() {
377 $user = static::getTestSysop()->getUser();
379 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
380 ->setMethods( [ 'canChangeUser' ] )->getMock();
381 $this->provider
->expects( $this->any() )->method( 'canChangeUser' )
382 ->will( $this->returnValue( false ) );
383 $backend = $this->getBackend();
384 $this->assertFalse( $backend->canSetUser() );
386 $backend->setUser( $user );
387 $this->fail( 'Expected exception not thrown' );
388 } catch ( \BadMethodCallException
$ex ) {
390 'Cannot set user on this session; check $session->canSetUser() first',
394 $this->assertNotSame( $user, $backend->getUser() );
396 $this->provider
= null;
397 $backend = $this->getBackend();
398 $this->assertTrue( $backend->canSetUser() );
399 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
400 $backend->setUser( $user );
401 $this->assertSame( $user, $backend->getUser() );
404 public function testDirty() {
405 $backend = $this->getBackend();
406 $priv = TestingAccessWrapper
::newFromObject( $backend );
407 $priv->dataDirty
= false;
409 $this->assertTrue( $priv->dataDirty
);
412 public function testGetData() {
413 $backend = $this->getBackend();
414 $data = $backend->getData();
415 $this->assertSame( [], $data );
416 $this->assertTrue( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
417 $data['???'] = '!!!';
418 $this->assertSame( [ '???' => '!!!' ], $data );
420 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
421 $this->store
->setSessionData( self
::SESSIONID
, $testData );
422 $backend = $this->getBackend();
423 $this->assertSame( $testData, $backend->getData() );
424 $this->assertFalse( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
427 public function testAddData() {
428 $backend = $this->getBackend();
429 $priv = TestingAccessWrapper
::newFromObject( $backend );
431 $priv->data
= [ 'foo' => 1 ];
432 $priv->dataDirty
= false;
433 $backend->addData( [ 'foo' => 1 ] );
434 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
435 $this->assertFalse( $priv->dataDirty
);
437 $priv->data
= [ 'foo' => 1 ];
438 $priv->dataDirty
= false;
439 $backend->addData( [ 'foo' => '1' ] );
440 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
441 $this->assertTrue( $priv->dataDirty
);
443 $priv->data
= [ 'foo' => 1 ];
444 $priv->dataDirty
= false;
445 $backend->addData( [ 'bar' => 2 ] );
446 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
447 $this->assertTrue( $priv->dataDirty
);
450 public function testDelaySave() {
451 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
452 $backend = $this->getBackend();
453 $priv = TestingAccessWrapper
::newFromObject( $backend );
454 $priv->persist
= true;
456 // Saves happen normally when no delay is in effect
457 $this->onSessionMetadataCalled
= false;
458 $priv->metaDirty
= true;
460 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
462 $this->onSessionMetadataCalled
= false;
463 $priv->metaDirty
= true;
465 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
467 $delay = $backend->delaySave();
469 // Autosave doesn't happen when no delay is in effect
470 $this->onSessionMetadataCalled
= false;
471 $priv->metaDirty
= true;
473 $this->assertFalse( $this->onSessionMetadataCalled
);
475 // Save still does happen when no delay is in effect
477 $this->assertTrue( $this->onSessionMetadataCalled
);
479 // Save happens when delay is consumed
480 $this->onSessionMetadataCalled
= false;
481 $priv->metaDirty
= true;
482 \Wikimedia\ScopedCallback
::consume( $delay );
483 $this->assertTrue( $this->onSessionMetadataCalled
);
485 // Test multiple delays
486 $delay1 = $backend->delaySave();
487 $delay2 = $backend->delaySave();
488 $delay3 = $backend->delaySave();
489 $this->onSessionMetadataCalled
= false;
490 $priv->metaDirty
= true;
492 $this->assertFalse( $this->onSessionMetadataCalled
);
493 \Wikimedia\ScopedCallback
::consume( $delay3 );
494 $this->assertFalse( $this->onSessionMetadataCalled
);
495 \Wikimedia\ScopedCallback
::consume( $delay1 );
496 $this->assertFalse( $this->onSessionMetadataCalled
);
497 \Wikimedia\ScopedCallback
::consume( $delay2 );
498 $this->assertTrue( $this->onSessionMetadataCalled
);
501 public function testSave() {
502 $user = static::getTestSysop()->getUser();
503 $this->store
= new TestBagOStuff();
504 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
506 $neverHook = $this->getMockBuilder( __CLASS__
)
507 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
508 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
510 $builder = $this->getMockBuilder( \DummySessionProvider
::class )
511 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
513 $neverProvider = $builder->getMock();
514 $neverProvider->expects( $this->never() )->method( 'persistSession' );
515 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
517 // Not persistent or dirty
518 $this->provider
= $neverProvider;
519 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
520 $this->store
->setSessionData( self
::SESSIONID
, $testData );
521 $backend = $this->getBackend( $user );
522 $this->store
->deleteSession( self
::SESSIONID
);
523 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
524 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
525 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
527 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
529 // (but does unpersist if forced)
530 $this->provider
= $builder->getMock();
531 $this->provider
->expects( $this->never() )->method( 'persistSession' );
532 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
533 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
534 $this->store
->setSessionData( self
::SESSIONID
, $testData );
535 $backend = $this->getBackend( $user );
536 $this->store
->deleteSession( self
::SESSIONID
);
537 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
538 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
539 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
540 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
541 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
543 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
545 // (but not to a WebRequest associated with a different session)
546 $this->provider
= $neverProvider;
547 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
548 $this->store
->setSessionData( self
::SESSIONID
, $testData );
549 $backend = $this->getBackend( $user );
550 TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
551 ->setSessionId( new SessionId( 'x' ) );
552 $this->store
->deleteSession( self
::SESSIONID
);
553 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
554 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
555 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
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 // Not persistent, but dirty
562 $this->provider
= $neverProvider;
563 $this->onSessionMetadataCalled
= false;
564 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
565 $this->store
->setSessionData( self
::SESSIONID
, $testData );
566 $backend = $this->getBackend( $user );
567 $this->store
->deleteSession( self
::SESSIONID
);
568 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
569 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
570 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
572 $this->assertTrue( $this->onSessionMetadataCalled
);
573 $blob = $this->store
->getSession( self
::SESSIONID
);
574 $this->assertInternalType( 'array', $blob );
575 $this->assertArrayHasKey( 'metadata', $blob );
576 $metadata = $blob['metadata'];
577 $this->assertInternalType( 'array', $metadata );
578 $this->assertArrayHasKey( '???', $metadata );
579 $this->assertSame( '!!!', $metadata['???'] );
580 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
581 'making sure it didn\'t save to backend' );
583 // Persistent, not dirty
584 $this->provider
= $neverProvider;
585 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
586 $this->store
->setSessionData( self
::SESSIONID
, $testData );
587 $backend = $this->getBackend( $user );
588 $this->store
->deleteSession( self
::SESSIONID
);
589 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
590 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
591 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
592 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
594 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
596 // (but will persist if forced)
597 $this->provider
= $builder->getMock();
598 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
599 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
600 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
601 $this->store
->setSessionData( self
::SESSIONID
, $testData );
602 $backend = $this->getBackend( $user );
603 $this->store
->deleteSession( self
::SESSIONID
);
604 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
605 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
606 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
607 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
608 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
610 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
612 // Persistent and dirty
613 $this->provider
= $neverProvider;
614 $this->onSessionMetadataCalled
= false;
615 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
616 $this->store
->setSessionData( self
::SESSIONID
, $testData );
617 $backend = $this->getBackend( $user );
618 $this->store
->deleteSession( self
::SESSIONID
);
619 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
620 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
621 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
622 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
624 $this->assertTrue( $this->onSessionMetadataCalled
);
625 $blob = $this->store
->getSession( self
::SESSIONID
);
626 $this->assertInternalType( 'array', $blob );
627 $this->assertArrayHasKey( 'metadata', $blob );
628 $metadata = $blob['metadata'];
629 $this->assertInternalType( 'array', $metadata );
630 $this->assertArrayHasKey( '???', $metadata );
631 $this->assertSame( '!!!', $metadata['???'] );
632 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
633 'making sure it did save to backend' );
635 // (also persists if forced)
636 $this->provider
= $builder->getMock();
637 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
638 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
639 $this->onSessionMetadataCalled
= false;
640 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
641 $this->store
->setSessionData( self
::SESSIONID
, $testData );
642 $backend = $this->getBackend( $user );
643 $this->store
->deleteSession( self
::SESSIONID
);
644 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
645 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
646 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
647 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
648 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
650 $this->assertTrue( $this->onSessionMetadataCalled
);
651 $blob = $this->store
->getSession( self
::SESSIONID
);
652 $this->assertInternalType( 'array', $blob );
653 $this->assertArrayHasKey( 'metadata', $blob );
654 $metadata = $blob['metadata'];
655 $this->assertInternalType( 'array', $metadata );
656 $this->assertArrayHasKey( '???', $metadata );
657 $this->assertSame( '!!!', $metadata['???'] );
658 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
659 'making sure it did save to backend' );
661 // (also persists if metadata dirty)
662 $this->provider
= $builder->getMock();
663 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
664 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
665 $this->onSessionMetadataCalled
= false;
666 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
667 $this->store
->setSessionData( self
::SESSIONID
, $testData );
668 $backend = $this->getBackend( $user );
669 $this->store
->deleteSession( self
::SESSIONID
);
670 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
671 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
672 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
673 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
675 $this->assertTrue( $this->onSessionMetadataCalled
);
676 $blob = $this->store
->getSession( self
::SESSIONID
);
677 $this->assertInternalType( 'array', $blob );
678 $this->assertArrayHasKey( 'metadata', $blob );
679 $metadata = $blob['metadata'];
680 $this->assertInternalType( 'array', $metadata );
681 $this->assertArrayHasKey( '???', $metadata );
682 $this->assertSame( '!!!', $metadata['???'] );
683 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
684 'making sure it did save to backend' );
686 // Not marked dirty, but dirty data
687 // (e.g. indirect modification from ArrayAccess::offsetGet)
688 $this->provider
= $neverProvider;
689 $this->onSessionMetadataCalled
= false;
690 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
691 $this->store
->setSessionData( self
::SESSIONID
, $testData );
692 $backend = $this->getBackend( $user );
693 $this->store
->deleteSession( self
::SESSIONID
);
694 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
695 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
696 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
697 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
698 TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
700 $this->assertTrue( $this->onSessionMetadataCalled
);
701 $blob = $this->store
->getSession( self
::SESSIONID
);
702 $this->assertInternalType( 'array', $blob );
703 $this->assertArrayHasKey( 'metadata', $blob );
704 $metadata = $blob['metadata'];
705 $this->assertInternalType( 'array', $metadata );
706 $this->assertArrayHasKey( '???', $metadata );
707 $this->assertSame( '!!!', $metadata['???'] );
708 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
709 'making sure it did save to backend' );
712 $this->provider
= null;
713 $mockHook = $this->getMockBuilder( __CLASS__
)
714 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
715 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
716 ->will( $this->returnCallback(
717 function ( SessionBackend
$backend, array &$metadata, array $requests ) {
718 $metadata['userId']++
;
721 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
722 $this->store
->setSessionData( self
::SESSIONID
, $testData );
723 $backend = $this->getBackend( $user );
727 $this->fail( 'Expected exception not thrown' );
728 } catch ( \UnexpectedValueException
$ex ) {
730 'SessionMetadata hook changed metadata key "userId"',
735 // SessionManager::preventSessionsForUser
736 TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
737 $user->getName() => true,
739 $this->provider
= $neverProvider;
740 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
741 $this->store
->setSessionData( self
::SESSIONID
, $testData );
742 $backend = $this->getBackend( $user );
743 $this->store
->deleteSession( self
::SESSIONID
);
744 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
745 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
746 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
747 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
749 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
752 public function testRenew() {
753 $user = static::getTestSysop()->getUser();
754 $this->store
= new TestBagOStuff();
755 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
758 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
759 ->setMethods( [ 'persistSession' ] )->getMock();
760 $this->provider
->expects( $this->never() )->method( 'persistSession' );
761 $this->onSessionMetadataCalled
= false;
762 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
763 $this->store
->setSessionData( self
::SESSIONID
, $testData );
764 $backend = $this->getBackend( $user );
765 $this->store
->deleteSession( self
::SESSIONID
);
766 $wrap = TestingAccessWrapper
::newFromObject( $backend );
767 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
768 $wrap->metaDirty
= false;
769 $wrap->dataDirty
= false;
770 $wrap->forcePersist
= false;
773 $this->assertTrue( $this->onSessionMetadataCalled
);
774 $blob = $this->store
->getSession( self
::SESSIONID
);
775 $this->assertInternalType( 'array', $blob );
776 $this->assertArrayHasKey( 'metadata', $blob );
777 $metadata = $blob['metadata'];
778 $this->assertInternalType( 'array', $metadata );
779 $this->assertArrayHasKey( '???', $metadata );
780 $this->assertSame( '!!!', $metadata['???'] );
781 $this->assertNotEquals( 0, $wrap->expires
);
784 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
785 ->setMethods( [ 'persistSession' ] )->getMock();
786 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
787 $this->onSessionMetadataCalled
= false;
788 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
789 $this->store
->setSessionData( self
::SESSIONID
, $testData );
790 $backend = $this->getBackend( $user );
791 $this->store
->deleteSession( self
::SESSIONID
);
792 $wrap = TestingAccessWrapper
::newFromObject( $backend );
793 $wrap->persist
= true;
794 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
795 $wrap->metaDirty
= false;
796 $wrap->dataDirty
= false;
797 $wrap->forcePersist
= false;
800 $this->assertTrue( $this->onSessionMetadataCalled
);
801 $blob = $this->store
->getSession( self
::SESSIONID
);
802 $this->assertInternalType( 'array', $blob );
803 $this->assertArrayHasKey( 'metadata', $blob );
804 $metadata = $blob['metadata'];
805 $this->assertInternalType( 'array', $metadata );
806 $this->assertArrayHasKey( '???', $metadata );
807 $this->assertSame( '!!!', $metadata['???'] );
808 $this->assertNotEquals( 0, $wrap->expires
);
810 // Not persistent, not expiring
811 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
812 ->setMethods( [ 'persistSession' ] )->getMock();
813 $this->provider
->expects( $this->never() )->method( 'persistSession' );
814 $this->onSessionMetadataCalled
= false;
815 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
816 $this->store
->setSessionData( self
::SESSIONID
, $testData );
817 $backend = $this->getBackend( $user );
818 $this->store
->deleteSession( self
::SESSIONID
);
819 $wrap = TestingAccessWrapper
::newFromObject( $backend );
820 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
821 $wrap->metaDirty
= false;
822 $wrap->dataDirty
= false;
823 $wrap->forcePersist
= false;
824 $expires = time() +
$wrap->lifetime +
100;
825 $wrap->expires
= $expires;
827 $this->assertFalse( $this->onSessionMetadataCalled
);
828 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
829 $this->assertEquals( $expires, $wrap->expires
);
832 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
833 $this->onSessionMetadataCalled
= true;
834 $metadata['???'] = '!!!';
837 public function testTakeOverGlobalSession() {
838 if ( !PHPSessionHandler
::isInstalled() ) {
839 PHPSessionHandler
::install( SessionManager
::singleton() );
841 if ( !PHPSessionHandler
::isEnabled() ) {
842 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
843 $rProp->setAccessible( true );
844 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
845 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
846 session_write_close();
847 $handler->enable
= false;
849 $handler->enable
= true;
852 $backend = $this->getBackend( static::getTestSysop()->getUser() );
853 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
855 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
857 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
858 $request = \RequestContext
::getMain()->getRequest();
859 $manager->globalSession
= $backend->getSession( $request );
860 $manager->globalSessionRequest
= $request;
863 TestingAccessWrapper
::newFromObject( $backend )->checkPHPSession();
864 $this->assertSame( $backend->getId(), session_id() );
865 session_write_close();
867 $backend2 = $this->getBackend(
868 User
::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
870 TestingAccessWrapper
::newFromObject( $backend2 )->usePhpSessionHandling
= true;
873 TestingAccessWrapper
::newFromObject( $backend2 )->checkPHPSession();
874 $this->assertSame( '', session_id() );
877 public function testResetIdOfGlobalSession() {
878 if ( !PHPSessionHandler
::isInstalled() ) {
879 PHPSessionHandler
::install( SessionManager
::singleton() );
881 if ( !PHPSessionHandler
::isEnabled() ) {
882 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
883 $rProp->setAccessible( true );
884 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
885 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
886 session_write_close();
887 $handler->enable
= false;
889 $handler->enable
= true;
892 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
893 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
895 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
897 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
898 $request = \RequestContext
::getMain()->getRequest();
899 $manager->globalSession
= $backend->getSession( $request );
900 $manager->globalSessionRequest
= $request;
902 session_id( self
::SESSIONID
);
903 \Wikimedia\
quietCall( 'session_start' );
904 $_SESSION['foo'] = __METHOD__
;
906 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
907 $this->assertSame( $backend->getId(), session_id() );
908 $this->assertArrayHasKey( 'foo', $_SESSION );
909 $this->assertSame( __METHOD__
, $_SESSION['foo'] );
910 session_write_close();
913 public function testUnpersistOfGlobalSession() {
914 if ( !PHPSessionHandler
::isInstalled() ) {
915 PHPSessionHandler
::install( SessionManager
::singleton() );
917 if ( !PHPSessionHandler
::isEnabled() ) {
918 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
919 $rProp->setAccessible( true );
920 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
921 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
922 session_write_close();
923 $handler->enable
= false;
925 $handler->enable
= true;
928 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
929 $wrap = TestingAccessWrapper
::newFromObject( $backend );
930 $wrap->usePhpSessionHandling
= true;
931 $wrap->persist
= true;
933 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
935 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
936 $request = \RequestContext
::getMain()->getRequest();
937 $manager->globalSession
= $backend->getSession( $request );
938 $manager->globalSessionRequest
= $request;
940 session_id( self
::SESSIONID
. 'x' );
941 \Wikimedia\
quietCall( 'session_start' );
942 $backend->unpersist();
943 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
944 session_write_close();
946 session_id( self
::SESSIONID
);
947 $wrap->persist
= true;
948 $backend->unpersist();
949 $this->assertSame( '', session_id() );
952 public function testGetAllowedUserRights() {
953 $this->provider
= $this->getMockBuilder( \DummySessionProvider
::class )
954 ->setMethods( [ 'getAllowedUserRights' ] )
956 $this->provider
->expects( $this->any() )->method( 'getAllowedUserRights' )
957 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
959 $backend = $this->getBackend();
960 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );