3 namespace MediaWiki\Session
;
11 * @covers MediaWiki\Session\SessionBackend
13 class SessionBackendTest
extends MediaWikiTestCase
{
14 const SESSIONID
= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
21 protected $onSessionMetadataCalled = false;
24 * Returns a non-persistent backend that thinks it has at least one session active
25 * @param User|null $user
28 protected function getBackend( User
$user = null, $id = null ) {
29 if ( !$this->config
) {
30 $this->config
= new \
HashConfig();
31 $this->manager
= null;
33 if ( !$this->store
) {
34 $this->store
= new TestBagOStuff();
35 $this->manager
= null;
38 $logger = new \Psr\Log\
NullLogger();
39 if ( !$this->manager
) {
40 $this->manager
= new SessionManager( [
41 'store' => $this->store
,
43 'config' => $this->config
,
47 if ( !$this->provider
) {
48 $this->provider
= new \
DummySessionProvider();
50 $this->provider
->setLogger( $logger );
51 $this->provider
->setConfig( $this->config
);
52 $this->provider
->setManager( $this->manager
);
54 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
55 'provider' => $this->provider
,
56 'id' => $id ?
: self
::SESSIONID
,
58 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
61 $id = new SessionId( $info->getId() );
63 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
64 $priv = \TestingAccessWrapper
::newFromObject( $backend );
65 $priv->persist
= false;
66 $priv->requests
= [ 100 => new \
FauxRequest() ];
67 $priv->requests
[100]->setSessionId( $id );
68 $priv->usePhpSessionHandling
= false;
70 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
71 $manager->allSessionBackends
= [ $backend->getId() => $backend ] +
$manager->allSessionBackends
;
72 $manager->allSessionIds
= [ $backend->getId() => $id ] +
$manager->allSessionIds
;
73 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
78 public function testConstructor() {
82 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
83 'provider' => $this->provider
,
84 'id' => self
::SESSIONID
,
86 'userInfo' => UserInfo
::newFromName( 'UTSysop', false ),
89 $id = new SessionId( $info->getId() );
90 $logger = new \Psr\Log\
NullLogger();
92 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
93 $this->fail( 'Expected exception not thrown' );
94 } catch ( \InvalidArgumentException
$ex ) {
96 "Refusing to create session for unverified user {$info->getUserInfo()}",
101 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
102 'id' => self
::SESSIONID
,
103 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
106 $id = new SessionId( $info->getId() );
108 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
109 $this->fail( 'Expected exception not thrown' );
110 } catch ( \InvalidArgumentException
$ex ) {
111 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
114 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
115 'provider' => $this->provider
,
116 'id' => self
::SESSIONID
,
118 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
121 $id = new SessionId( '!' . $info->getId() );
123 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
124 $this->fail( 'Expected exception not thrown' );
125 } catch ( \InvalidArgumentException
$ex ) {
127 'SessionId and SessionInfo don\'t match',
132 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
133 'provider' => $this->provider
,
134 'id' => self
::SESSIONID
,
136 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
139 $id = new SessionId( $info->getId() );
140 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
141 $this->assertSame( self
::SESSIONID
, $backend->getId() );
142 $this->assertSame( $id, $backend->getSessionId() );
143 $this->assertSame( $this->provider
, $backend->getProvider() );
144 $this->assertInstanceOf( 'User', $backend->getUser() );
145 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
146 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
147 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
148 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
150 $expire = time() +
100;
151 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ], 2 );
153 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
154 'provider' => $this->provider
,
155 'id' => self
::SESSIONID
,
157 'forceHTTPS' => true,
158 'metadata' => [ 'foo' ],
161 $id = new SessionId( $info->getId() );
162 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
163 $this->assertSame( self
::SESSIONID
, $backend->getId() );
164 $this->assertSame( $id, $backend->getSessionId() );
165 $this->assertSame( $this->provider
, $backend->getProvider() );
166 $this->assertInstanceOf( 'User', $backend->getUser() );
167 $this->assertTrue( $backend->getUser()->isAnon() );
168 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
169 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
170 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
171 $this->assertSame( $expire, \TestingAccessWrapper
::newFromObject( $backend )->expires
);
172 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
175 public function testSessionStuff() {
176 $backend = $this->getBackend();
177 $priv = \TestingAccessWrapper
::newFromObject( $backend );
178 $priv->requests
= []; // Remove dummy session
180 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
182 $request1 = new \
FauxRequest();
183 $session1 = $backend->getSession( $request1 );
184 $request2 = new \
FauxRequest();
185 $session2 = $backend->getSession( $request2 );
187 $this->assertInstanceOf( Session
::class, $session1 );
188 $this->assertInstanceOf( Session
::class, $session2 );
189 $this->assertSame( 2, count( $priv->requests
) );
191 $index = \TestingAccessWrapper
::newFromObject( $session1 )->index
;
193 $this->assertSame( $request1, $backend->getRequest( $index ) );
194 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
195 $request1->setCookie( 'UserName', 'Example' );
196 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
199 $this->assertSame( 1, count( $priv->requests
) );
200 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
201 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
203 $backend->getRequest( $index );
204 $this->fail( 'Expected exception not thrown' );
205 } catch ( \InvalidArgumentException
$ex ) {
206 $this->assertSame( 'Invalid session index', $ex->getMessage() );
209 $backend->suggestLoginUsername( $index );
210 $this->fail( 'Expected exception not thrown' );
211 } catch ( \InvalidArgumentException
$ex ) {
212 $this->assertSame( 'Invalid session index', $ex->getMessage() );
216 $this->assertSame( 0, count( $priv->requests
) );
217 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
218 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
221 public function testSetProviderMetadata() {
222 $backend = $this->getBackend();
223 $priv = \TestingAccessWrapper
::newFromObject( $backend );
224 $priv->providerMetadata
= [ 'dummy' ];
227 $backend->setProviderMetadata( 'foo' );
228 $this->fail( 'Expected exception not thrown' );
229 } catch ( \InvalidArgumentException
$ex ) {
230 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
234 $backend->setProviderMetadata( (object)[] );
235 $this->fail( 'Expected exception not thrown' );
236 } catch ( \InvalidArgumentException
$ex ) {
237 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
240 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
241 $backend->setProviderMetadata( [ 'dummy' ] );
242 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
244 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
245 $backend->setProviderMetadata( [ 'test' ] );
246 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
247 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
248 $this->store
->deleteSession( self
::SESSIONID
);
250 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
251 $backend->setProviderMetadata( null );
252 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
253 $this->assertSame( null, $backend->getProviderMetadata() );
254 $this->store
->deleteSession( self
::SESSIONID
);
257 public function testResetId() {
260 $builder = $this->getMockBuilder( 'DummySessionProvider' )
261 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
263 $this->provider
= $builder->getMock();
264 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
265 ->will( $this->returnValue( false ) );
266 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
267 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
268 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
269 $sessionId = $backend->getSessionId();
271 $this->assertSame( self
::SESSIONID
, $backend->getId() );
272 $this->assertSame( $backend->getId(), $sessionId->getId() );
273 $this->assertSame( $id, session_id() );
274 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
276 $this->provider
= $builder->getMock();
277 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
278 ->will( $this->returnValue( true ) );
279 $backend = $this->getBackend();
280 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
281 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
282 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
283 $sessionId = $backend->getSessionId();
285 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
286 $this->assertSame( $backend->getId(), $sessionId->getId() );
287 $this->assertInternalType( 'array', $this->store
->getSession( $backend->getId() ) );
288 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
289 $this->assertSame( $id, session_id() );
290 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
291 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
292 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
295 public function testPersist() {
296 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
297 $this->provider
->expects( $this->once() )->method( 'persistSession' );
298 $backend = $this->getBackend();
299 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
300 $backend->save(); // This one shouldn't call $provider->persistSession()
303 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
305 $this->provider
= null;
306 $backend = $this->getBackend();
307 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
308 $wrap->persist
= true;
311 $this->assertNotEquals( 0, $wrap->expires
);
314 public function testUnpersist() {
315 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'unpersistSession' ] );
316 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
317 $backend = $this->getBackend();
318 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
319 $wrap->store
= new \
CachedBagOStuff( $this->store
);
320 $wrap->persist
= true;
321 $wrap->dataDirty
= true;
323 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
324 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
325 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
327 $backend->unpersist();
328 $this->assertFalse( $backend->isPersistent() );
329 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
330 $this->assertNotFalse( $wrap->store
->get( wfMemcKey( 'MWSession', self
::SESSIONID
) ) );
333 public function testRememberUser() {
334 $backend = $this->getBackend();
336 $remembered = $backend->shouldRememberUser();
337 $backend->setRememberUser( !$remembered );
338 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
339 $backend->setRememberUser( $remembered );
340 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
343 public function testForceHTTPS() {
344 $backend = $this->getBackend();
346 $force = $backend->shouldForceHTTPS();
347 $backend->setForceHTTPS( !$force );
348 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
349 $backend->setForceHTTPS( $force );
350 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
353 public function testLoggedOutTimestamp() {
354 $backend = $this->getBackend();
356 $backend->setLoggedOutTimestamp( 42 );
357 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
358 $backend->setLoggedOutTimestamp( '123' );
359 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
362 public function testSetUser() {
363 $user = static::getTestSysop()->getUser();
365 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'canChangeUser' ] );
366 $this->provider
->expects( $this->any() )->method( 'canChangeUser' )
367 ->will( $this->returnValue( false ) );
368 $backend = $this->getBackend();
369 $this->assertFalse( $backend->canSetUser() );
371 $backend->setUser( $user );
372 $this->fail( 'Expected exception not thrown' );
373 } catch ( \BadMethodCallException
$ex ) {
375 'Cannot set user on this session; check $session->canSetUser() first',
379 $this->assertNotSame( $user, $backend->getUser() );
381 $this->provider
= null;
382 $backend = $this->getBackend();
383 $this->assertTrue( $backend->canSetUser() );
384 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
385 $backend->setUser( $user );
386 $this->assertSame( $user, $backend->getUser() );
389 public function testDirty() {
390 $backend = $this->getBackend();
391 $priv = \TestingAccessWrapper
::newFromObject( $backend );
392 $priv->dataDirty
= false;
394 $this->assertTrue( $priv->dataDirty
);
397 public function testGetData() {
398 $backend = $this->getBackend();
399 $data = $backend->getData();
400 $this->assertSame( [], $data );
401 $this->assertTrue( \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
402 $data['???'] = '!!!';
403 $this->assertSame( [ '???' => '!!!' ], $data );
405 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
406 $this->store
->setSessionData( self
::SESSIONID
, $testData );
407 $backend = $this->getBackend();
408 $this->assertSame( $testData, $backend->getData() );
409 $this->assertFalse( \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
412 public function testAddData() {
413 $backend = $this->getBackend();
414 $priv = \TestingAccessWrapper
::newFromObject( $backend );
416 $priv->data
= [ 'foo' => 1 ];
417 $priv->dataDirty
= false;
418 $backend->addData( [ 'foo' => 1 ] );
419 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
420 $this->assertFalse( $priv->dataDirty
);
422 $priv->data
= [ 'foo' => 1 ];
423 $priv->dataDirty
= false;
424 $backend->addData( [ 'foo' => '1' ] );
425 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
426 $this->assertTrue( $priv->dataDirty
);
428 $priv->data
= [ 'foo' => 1 ];
429 $priv->dataDirty
= false;
430 $backend->addData( [ 'bar' => 2 ] );
431 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
432 $this->assertTrue( $priv->dataDirty
);
435 public function testDelaySave() {
436 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
437 $backend = $this->getBackend();
438 $priv = \TestingAccessWrapper
::newFromObject( $backend );
439 $priv->persist
= true;
441 // Saves happen normally when no delay is in effect
442 $this->onSessionMetadataCalled
= false;
443 $priv->metaDirty
= true;
445 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
447 $this->onSessionMetadataCalled
= false;
448 $priv->metaDirty
= true;
450 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
452 $delay = $backend->delaySave();
454 // Autosave doesn't happen when no delay is in effect
455 $this->onSessionMetadataCalled
= false;
456 $priv->metaDirty
= true;
458 $this->assertFalse( $this->onSessionMetadataCalled
);
460 // Save still does happen when no delay is in effect
462 $this->assertTrue( $this->onSessionMetadataCalled
);
464 // Save happens when delay is consumed
465 $this->onSessionMetadataCalled
= false;
466 $priv->metaDirty
= true;
467 \Wikimedia\ScopedCallback
::consume( $delay );
468 $this->assertTrue( $this->onSessionMetadataCalled
);
470 // Test multiple delays
471 $delay1 = $backend->delaySave();
472 $delay2 = $backend->delaySave();
473 $delay3 = $backend->delaySave();
474 $this->onSessionMetadataCalled
= false;
475 $priv->metaDirty
= true;
477 $this->assertFalse( $this->onSessionMetadataCalled
);
478 \Wikimedia\ScopedCallback
::consume( $delay3 );
479 $this->assertFalse( $this->onSessionMetadataCalled
);
480 \Wikimedia\ScopedCallback
::consume( $delay1 );
481 $this->assertFalse( $this->onSessionMetadataCalled
);
482 \Wikimedia\ScopedCallback
::consume( $delay2 );
483 $this->assertTrue( $this->onSessionMetadataCalled
);
486 public function testSave() {
487 $user = static::getTestSysop()->getUser();
488 $this->store
= new TestBagOStuff();
489 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
491 $neverHook = $this->getMock( __CLASS__
, [ 'onSessionMetadata' ] );
492 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
494 $builder = $this->getMockBuilder( 'DummySessionProvider' )
495 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
497 $neverProvider = $builder->getMock();
498 $neverProvider->expects( $this->never() )->method( 'persistSession' );
499 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
501 // Not persistent or dirty
502 $this->provider
= $neverProvider;
503 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
504 $this->store
->setSessionData( self
::SESSIONID
, $testData );
505 $backend = $this->getBackend( $user );
506 $this->store
->deleteSession( self
::SESSIONID
);
507 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
508 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
509 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
511 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
513 // (but does unpersist if forced)
514 $this->provider
= $builder->getMock();
515 $this->provider
->expects( $this->never() )->method( 'persistSession' );
516 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
517 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
518 $this->store
->setSessionData( self
::SESSIONID
, $testData );
519 $backend = $this->getBackend( $user );
520 $this->store
->deleteSession( self
::SESSIONID
);
521 \TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
522 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
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 not to a WebRequest associated with a different session)
530 $this->provider
= $neverProvider;
531 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
532 $this->store
->setSessionData( self
::SESSIONID
, $testData );
533 $backend = $this->getBackend( $user );
534 \TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
535 ->setSessionId( new SessionId( 'x' ) );
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 // Not persistent, but dirty
546 $this->provider
= $neverProvider;
547 $this->onSessionMetadataCalled
= false;
548 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
549 $this->store
->setSessionData( self
::SESSIONID
, $testData );
550 $backend = $this->getBackend( $user );
551 $this->store
->deleteSession( self
::SESSIONID
);
552 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
553 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
554 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
556 $this->assertTrue( $this->onSessionMetadataCalled
);
557 $blob = $this->store
->getSession( self
::SESSIONID
);
558 $this->assertInternalType( 'array', $blob );
559 $this->assertArrayHasKey( 'metadata', $blob );
560 $metadata = $blob['metadata'];
561 $this->assertInternalType( 'array', $metadata );
562 $this->assertArrayHasKey( '???', $metadata );
563 $this->assertSame( '!!!', $metadata['???'] );
564 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
565 'making sure it didn\'t save to backend' );
567 // Persistent, not dirty
568 $this->provider
= $neverProvider;
569 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
570 $this->store
->setSessionData( self
::SESSIONID
, $testData );
571 $backend = $this->getBackend( $user );
572 $this->store
->deleteSession( self
::SESSIONID
);
573 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
574 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
575 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
576 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
578 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
580 // (but will persist if forced)
581 $this->provider
= $builder->getMock();
582 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
583 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
584 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
585 $this->store
->setSessionData( self
::SESSIONID
, $testData );
586 $backend = $this->getBackend( $user );
587 $this->store
->deleteSession( self
::SESSIONID
);
588 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
589 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= 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 // Persistent and dirty
597 $this->provider
= $neverProvider;
598 $this->onSessionMetadataCalled
= false;
599 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
600 $this->store
->setSessionData( self
::SESSIONID
, $testData );
601 $backend = $this->getBackend( $user );
602 $this->store
->deleteSession( self
::SESSIONID
);
603 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
604 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
605 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
606 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
608 $this->assertTrue( $this->onSessionMetadataCalled
);
609 $blob = $this->store
->getSession( self
::SESSIONID
);
610 $this->assertInternalType( 'array', $blob );
611 $this->assertArrayHasKey( 'metadata', $blob );
612 $metadata = $blob['metadata'];
613 $this->assertInternalType( 'array', $metadata );
614 $this->assertArrayHasKey( '???', $metadata );
615 $this->assertSame( '!!!', $metadata['???'] );
616 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
617 'making sure it did save to backend' );
619 // (also persists if forced)
620 $this->provider
= $builder->getMock();
621 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
622 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
623 $this->onSessionMetadataCalled
= false;
624 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
625 $this->store
->setSessionData( self
::SESSIONID
, $testData );
626 $backend = $this->getBackend( $user );
627 $this->store
->deleteSession( self
::SESSIONID
);
628 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
629 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
630 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
631 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
632 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
634 $this->assertTrue( $this->onSessionMetadataCalled
);
635 $blob = $this->store
->getSession( self
::SESSIONID
);
636 $this->assertInternalType( 'array', $blob );
637 $this->assertArrayHasKey( 'metadata', $blob );
638 $metadata = $blob['metadata'];
639 $this->assertInternalType( 'array', $metadata );
640 $this->assertArrayHasKey( '???', $metadata );
641 $this->assertSame( '!!!', $metadata['???'] );
642 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
643 'making sure it did save to backend' );
645 // (also persists if metadata dirty)
646 $this->provider
= $builder->getMock();
647 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
648 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
649 $this->onSessionMetadataCalled
= false;
650 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
651 $this->store
->setSessionData( self
::SESSIONID
, $testData );
652 $backend = $this->getBackend( $user );
653 $this->store
->deleteSession( self
::SESSIONID
);
654 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
655 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
656 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
657 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
659 $this->assertTrue( $this->onSessionMetadataCalled
);
660 $blob = $this->store
->getSession( self
::SESSIONID
);
661 $this->assertInternalType( 'array', $blob );
662 $this->assertArrayHasKey( 'metadata', $blob );
663 $metadata = $blob['metadata'];
664 $this->assertInternalType( 'array', $metadata );
665 $this->assertArrayHasKey( '???', $metadata );
666 $this->assertSame( '!!!', $metadata['???'] );
667 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
668 'making sure it did save to backend' );
670 // Not marked dirty, but dirty data
671 // (e.g. indirect modification from ArrayAccess::offsetGet)
672 $this->provider
= $neverProvider;
673 $this->onSessionMetadataCalled
= false;
674 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
675 $this->store
->setSessionData( self
::SESSIONID
, $testData );
676 $backend = $this->getBackend( $user );
677 $this->store
->deleteSession( self
::SESSIONID
);
678 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
679 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
680 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
681 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
682 \TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
684 $this->assertTrue( $this->onSessionMetadataCalled
);
685 $blob = $this->store
->getSession( self
::SESSIONID
);
686 $this->assertInternalType( 'array', $blob );
687 $this->assertArrayHasKey( 'metadata', $blob );
688 $metadata = $blob['metadata'];
689 $this->assertInternalType( 'array', $metadata );
690 $this->assertArrayHasKey( '???', $metadata );
691 $this->assertSame( '!!!', $metadata['???'] );
692 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
693 'making sure it did save to backend' );
696 $this->provider
= null;
697 $mockHook = $this->getMock( __CLASS__
, [ 'onSessionMetadata' ] );
698 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
699 ->will( $this->returnCallback(
700 function ( SessionBackend
$backend, array &$metadata, array $requests ) {
701 $metadata['userId']++
;
704 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
705 $this->store
->setSessionData( self
::SESSIONID
, $testData );
706 $backend = $this->getBackend( $user );
710 $this->fail( 'Expected exception not thrown' );
711 } catch ( \UnexpectedValueException
$ex ) {
713 'SessionMetadata hook changed metadata key "userId"',
718 // SessionManager::preventSessionsForUser
719 \TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
720 $user->getName() => true,
722 $this->provider
= $neverProvider;
723 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
724 $this->store
->setSessionData( self
::SESSIONID
, $testData );
725 $backend = $this->getBackend( $user );
726 $this->store
->deleteSession( self
::SESSIONID
);
727 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
728 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
729 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
730 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
732 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
735 public function testRenew() {
736 $user = static::getTestSysop()->getUser();
737 $this->store
= new TestBagOStuff();
738 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
741 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
742 $this->provider
->expects( $this->never() )->method( 'persistSession' );
743 $this->onSessionMetadataCalled
= false;
744 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
745 $this->store
->setSessionData( self
::SESSIONID
, $testData );
746 $backend = $this->getBackend( $user );
747 $this->store
->deleteSession( self
::SESSIONID
);
748 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
749 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
750 $wrap->metaDirty
= false;
751 $wrap->dataDirty
= false;
752 $wrap->forcePersist
= false;
755 $this->assertTrue( $this->onSessionMetadataCalled
);
756 $blob = $this->store
->getSession( self
::SESSIONID
);
757 $this->assertInternalType( 'array', $blob );
758 $this->assertArrayHasKey( 'metadata', $blob );
759 $metadata = $blob['metadata'];
760 $this->assertInternalType( 'array', $metadata );
761 $this->assertArrayHasKey( '???', $metadata );
762 $this->assertSame( '!!!', $metadata['???'] );
763 $this->assertNotEquals( 0, $wrap->expires
);
766 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
767 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
768 $this->onSessionMetadataCalled
= false;
769 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
770 $this->store
->setSessionData( self
::SESSIONID
, $testData );
771 $backend = $this->getBackend( $user );
772 $this->store
->deleteSession( self
::SESSIONID
);
773 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
774 $wrap->persist
= true;
775 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
776 $wrap->metaDirty
= false;
777 $wrap->dataDirty
= false;
778 $wrap->forcePersist
= false;
781 $this->assertTrue( $this->onSessionMetadataCalled
);
782 $blob = $this->store
->getSession( self
::SESSIONID
);
783 $this->assertInternalType( 'array', $blob );
784 $this->assertArrayHasKey( 'metadata', $blob );
785 $metadata = $blob['metadata'];
786 $this->assertInternalType( 'array', $metadata );
787 $this->assertArrayHasKey( '???', $metadata );
788 $this->assertSame( '!!!', $metadata['???'] );
789 $this->assertNotEquals( 0, $wrap->expires
);
791 // Not persistent, not expiring
792 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
793 $this->provider
->expects( $this->never() )->method( 'persistSession' );
794 $this->onSessionMetadataCalled
= false;
795 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
796 $this->store
->setSessionData( self
::SESSIONID
, $testData );
797 $backend = $this->getBackend( $user );
798 $this->store
->deleteSession( self
::SESSIONID
);
799 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
800 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
801 $wrap->metaDirty
= false;
802 $wrap->dataDirty
= false;
803 $wrap->forcePersist
= false;
804 $expires = time() +
$wrap->lifetime +
100;
805 $wrap->expires
= $expires;
807 $this->assertFalse( $this->onSessionMetadataCalled
);
808 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
809 $this->assertEquals( $expires, $wrap->expires
);
812 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
813 $this->onSessionMetadataCalled
= true;
814 $metadata['???'] = '!!!';
817 public function testTakeOverGlobalSession() {
818 if ( !PHPSessionHandler
::isInstalled() ) {
819 PHPSessionHandler
::install( SessionManager
::singleton() );
821 if ( !PHPSessionHandler
::isEnabled() ) {
822 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
823 $rProp->setAccessible( true );
824 $handler = \TestingAccessWrapper
::newFromObject( $rProp->getValue() );
825 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
826 session_write_close();
827 $handler->enable
= false;
829 $handler->enable
= true;
832 $backend = $this->getBackend( static::getTestSysop()->getUser() );
833 \TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
835 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
837 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
838 $request = \RequestContext
::getMain()->getRequest();
839 $manager->globalSession
= $backend->getSession( $request );
840 $manager->globalSessionRequest
= $request;
843 \TestingAccessWrapper
::newFromObject( $backend )->checkPHPSession();
844 $this->assertSame( $backend->getId(), session_id() );
845 session_write_close();
847 $backend2 = $this->getBackend(
848 User
::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
850 \TestingAccessWrapper
::newFromObject( $backend2 )->usePhpSessionHandling
= true;
853 \TestingAccessWrapper
::newFromObject( $backend2 )->checkPHPSession();
854 $this->assertSame( '', session_id() );
857 public function testResetIdOfGlobalSession() {
858 if ( !PHPSessionHandler
::isInstalled() ) {
859 PHPSessionHandler
::install( SessionManager
::singleton() );
861 if ( !PHPSessionHandler
::isEnabled() ) {
862 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
863 $rProp->setAccessible( true );
864 $handler = \TestingAccessWrapper
::newFromObject( $rProp->getValue() );
865 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
866 session_write_close();
867 $handler->enable
= false;
869 $handler->enable
= true;
872 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
873 \TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
875 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
877 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
878 $request = \RequestContext
::getMain()->getRequest();
879 $manager->globalSession
= $backend->getSession( $request );
880 $manager->globalSessionRequest
= $request;
882 session_id( self
::SESSIONID
);
883 \MediaWiki\
quietCall( 'session_start' );
884 $_SESSION['foo'] = __METHOD__
;
886 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
887 $this->assertSame( $backend->getId(), session_id() );
888 $this->assertArrayHasKey( 'foo', $_SESSION );
889 $this->assertSame( __METHOD__
, $_SESSION['foo'] );
890 session_write_close();
893 public function testUnpersistOfGlobalSession() {
894 if ( !PHPSessionHandler
::isInstalled() ) {
895 PHPSessionHandler
::install( SessionManager
::singleton() );
897 if ( !PHPSessionHandler
::isEnabled() ) {
898 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
899 $rProp->setAccessible( true );
900 $handler = \TestingAccessWrapper
::newFromObject( $rProp->getValue() );
901 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
902 session_write_close();
903 $handler->enable
= false;
905 $handler->enable
= true;
908 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
909 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
910 $wrap->usePhpSessionHandling
= true;
911 $wrap->persist
= true;
913 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
915 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
916 $request = \RequestContext
::getMain()->getRequest();
917 $manager->globalSession
= $backend->getSession( $request );
918 $manager->globalSessionRequest
= $request;
920 session_id( self
::SESSIONID
. 'x' );
921 \MediaWiki\
quietCall( 'session_start' );
922 $backend->unpersist();
923 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
925 session_id( self
::SESSIONID
);
926 $wrap->persist
= true;
927 $backend->unpersist();
928 $this->assertSame( '', session_id() );
931 public function testGetAllowedUserRights() {
932 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
933 ->setMethods( [ 'getAllowedUserRights' ] )
935 $this->provider
->expects( $this->any() )->method( 'getAllowedUserRights' )
936 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
938 $backend = $this->getBackend();
939 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );