3 namespace MediaWiki\Session
;
7 use Wikimedia\TestingAccessWrapper
;
12 * @covers MediaWiki\Session\SessionBackend
14 class SessionBackendTest
extends MediaWikiTestCase
{
15 const SESSIONID
= 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
22 protected $onSessionMetadataCalled = false;
25 * Returns a non-persistent backend that thinks it has at least one session active
26 * @param User|null $user
29 protected function getBackend( User
$user = null, $id = null ) {
30 if ( !$this->config
) {
31 $this->config
= new \
HashConfig();
32 $this->manager
= null;
34 if ( !$this->store
) {
35 $this->store
= new TestBagOStuff();
36 $this->manager
= null;
39 $logger = new \Psr\Log\
NullLogger();
40 if ( !$this->manager
) {
41 $this->manager
= new SessionManager( [
42 'store' => $this->store
,
44 'config' => $this->config
,
48 if ( !$this->provider
) {
49 $this->provider
= new \
DummySessionProvider();
51 $this->provider
->setLogger( $logger );
52 $this->provider
->setConfig( $this->config
);
53 $this->provider
->setManager( $this->manager
);
55 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
56 'provider' => $this->provider
,
57 'id' => $id ?
: self
::SESSIONID
,
59 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
62 $id = new SessionId( $info->getId() );
64 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
65 $priv = TestingAccessWrapper
::newFromObject( $backend );
66 $priv->persist
= false;
67 $priv->requests
= [ 100 => new \
FauxRequest() ];
68 $priv->requests
[100]->setSessionId( $id );
69 $priv->usePhpSessionHandling
= false;
71 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
72 $manager->allSessionBackends
= [ $backend->getId() => $backend ] +
$manager->allSessionBackends
;
73 $manager->allSessionIds
= [ $backend->getId() => $id ] +
$manager->allSessionIds
;
74 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
79 public function testConstructor() {
83 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
84 'provider' => $this->provider
,
85 'id' => self
::SESSIONID
,
87 'userInfo' => UserInfo
::newFromName( 'UTSysop', false ),
90 $id = new SessionId( $info->getId() );
91 $logger = new \Psr\Log\
NullLogger();
93 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
94 $this->fail( 'Expected exception not thrown' );
95 } catch ( \InvalidArgumentException
$ex ) {
97 "Refusing to create session for unverified user {$info->getUserInfo()}",
102 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
103 'id' => self
::SESSIONID
,
104 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
107 $id = new SessionId( $info->getId() );
109 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
110 $this->fail( 'Expected exception not thrown' );
111 } catch ( \InvalidArgumentException
$ex ) {
112 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
115 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
116 'provider' => $this->provider
,
117 'id' => self
::SESSIONID
,
119 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
122 $id = new SessionId( '!' . $info->getId() );
124 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
125 $this->fail( 'Expected exception not thrown' );
126 } catch ( \InvalidArgumentException
$ex ) {
128 'SessionId and SessionInfo don\'t match',
133 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
134 'provider' => $this->provider
,
135 'id' => self
::SESSIONID
,
137 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
140 $id = new SessionId( $info->getId() );
141 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
142 $this->assertSame( self
::SESSIONID
, $backend->getId() );
143 $this->assertSame( $id, $backend->getSessionId() );
144 $this->assertSame( $this->provider
, $backend->getProvider() );
145 $this->assertInstanceOf( 'User', $backend->getUser() );
146 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
147 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
148 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
149 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
151 $expire = time() +
100;
152 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ], 2 );
154 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
155 'provider' => $this->provider
,
156 'id' => self
::SESSIONID
,
158 'forceHTTPS' => true,
159 'metadata' => [ 'foo' ],
162 $id = new SessionId( $info->getId() );
163 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
164 $this->assertSame( self
::SESSIONID
, $backend->getId() );
165 $this->assertSame( $id, $backend->getSessionId() );
166 $this->assertSame( $this->provider
, $backend->getProvider() );
167 $this->assertInstanceOf( 'User', $backend->getUser() );
168 $this->assertTrue( $backend->getUser()->isAnon() );
169 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
170 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
171 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
172 $this->assertSame( $expire, TestingAccessWrapper
::newFromObject( $backend )->expires
);
173 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
176 public function testSessionStuff() {
177 $backend = $this->getBackend();
178 $priv = TestingAccessWrapper
::newFromObject( $backend );
179 $priv->requests
= []; // Remove dummy session
181 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
183 $request1 = new \
FauxRequest();
184 $session1 = $backend->getSession( $request1 );
185 $request2 = new \
FauxRequest();
186 $session2 = $backend->getSession( $request2 );
188 $this->assertInstanceOf( Session
::class, $session1 );
189 $this->assertInstanceOf( Session
::class, $session2 );
190 $this->assertSame( 2, count( $priv->requests
) );
192 $index = TestingAccessWrapper
::newFromObject( $session1 )->index
;
194 $this->assertSame( $request1, $backend->getRequest( $index ) );
195 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
196 $request1->setCookie( 'UserName', 'Example' );
197 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
200 $this->assertSame( 1, count( $priv->requests
) );
201 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
202 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
204 $backend->getRequest( $index );
205 $this->fail( 'Expected exception not thrown' );
206 } catch ( \InvalidArgumentException
$ex ) {
207 $this->assertSame( 'Invalid session index', $ex->getMessage() );
210 $backend->suggestLoginUsername( $index );
211 $this->fail( 'Expected exception not thrown' );
212 } catch ( \InvalidArgumentException
$ex ) {
213 $this->assertSame( 'Invalid session index', $ex->getMessage() );
217 $this->assertSame( 0, count( $priv->requests
) );
218 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
219 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
222 public function testSetProviderMetadata() {
223 $backend = $this->getBackend();
224 $priv = TestingAccessWrapper
::newFromObject( $backend );
225 $priv->providerMetadata
= [ 'dummy' ];
228 $backend->setProviderMetadata( 'foo' );
229 $this->fail( 'Expected exception not thrown' );
230 } catch ( \InvalidArgumentException
$ex ) {
231 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
235 $backend->setProviderMetadata( (object)[] );
236 $this->fail( 'Expected exception not thrown' );
237 } catch ( \InvalidArgumentException
$ex ) {
238 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
241 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
242 $backend->setProviderMetadata( [ 'dummy' ] );
243 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
245 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
246 $backend->setProviderMetadata( [ 'test' ] );
247 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
248 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
249 $this->store
->deleteSession( self
::SESSIONID
);
251 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
252 $backend->setProviderMetadata( null );
253 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
254 $this->assertSame( null, $backend->getProviderMetadata() );
255 $this->store
->deleteSession( self
::SESSIONID
);
258 public function testResetId() {
261 $builder = $this->getMockBuilder( 'DummySessionProvider' )
262 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
264 $this->provider
= $builder->getMock();
265 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
266 ->will( $this->returnValue( false ) );
267 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
268 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
269 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
270 $sessionId = $backend->getSessionId();
272 $this->assertSame( self
::SESSIONID
, $backend->getId() );
273 $this->assertSame( $backend->getId(), $sessionId->getId() );
274 $this->assertSame( $id, session_id() );
275 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
277 $this->provider
= $builder->getMock();
278 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
279 ->will( $this->returnValue( true ) );
280 $backend = $this->getBackend();
281 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
282 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
283 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
284 $sessionId = $backend->getSessionId();
286 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
287 $this->assertSame( $backend->getId(), $sessionId->getId() );
288 $this->assertInternalType( 'array', $this->store
->getSession( $backend->getId() ) );
289 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
290 $this->assertSame( $id, session_id() );
291 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
292 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
293 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
296 public function testPersist() {
297 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
298 ->setMethods( [ 'persistSession' ] )->getMock();
299 $this->provider
->expects( $this->once() )->method( 'persistSession' );
300 $backend = $this->getBackend();
301 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
302 $backend->save(); // This one shouldn't call $provider->persistSession()
305 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
307 $this->provider
= null;
308 $backend = $this->getBackend();
309 $wrap = TestingAccessWrapper
::newFromObject( $backend );
310 $wrap->persist
= true;
313 $this->assertNotEquals( 0, $wrap->expires
);
316 public function testUnpersist() {
317 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
318 ->setMethods( [ 'unpersistSession' ] )->getMock();
319 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
320 $backend = $this->getBackend();
321 $wrap = TestingAccessWrapper
::newFromObject( $backend );
322 $wrap->store
= new \
CachedBagOStuff( $this->store
);
323 $wrap->persist
= true;
324 $wrap->dataDirty
= true;
326 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
327 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
328 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
330 $backend->unpersist();
331 $this->assertFalse( $backend->isPersistent() );
332 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
333 $this->assertNotFalse( $wrap->store
->get( wfMemcKey( 'MWSession', self
::SESSIONID
) ) );
336 public function testRememberUser() {
337 $backend = $this->getBackend();
339 $remembered = $backend->shouldRememberUser();
340 $backend->setRememberUser( !$remembered );
341 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
342 $backend->setRememberUser( $remembered );
343 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
346 public function testForceHTTPS() {
347 $backend = $this->getBackend();
349 $force = $backend->shouldForceHTTPS();
350 $backend->setForceHTTPS( !$force );
351 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
352 $backend->setForceHTTPS( $force );
353 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
356 public function testLoggedOutTimestamp() {
357 $backend = $this->getBackend();
359 $backend->setLoggedOutTimestamp( 42 );
360 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
361 $backend->setLoggedOutTimestamp( '123' );
362 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
365 public function testSetUser() {
366 $user = static::getTestSysop()->getUser();
368 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
369 ->setMethods( [ 'canChangeUser' ] )->getMock();
370 $this->provider
->expects( $this->any() )->method( 'canChangeUser' )
371 ->will( $this->returnValue( false ) );
372 $backend = $this->getBackend();
373 $this->assertFalse( $backend->canSetUser() );
375 $backend->setUser( $user );
376 $this->fail( 'Expected exception not thrown' );
377 } catch ( \BadMethodCallException
$ex ) {
379 'Cannot set user on this session; check $session->canSetUser() first',
383 $this->assertNotSame( $user, $backend->getUser() );
385 $this->provider
= null;
386 $backend = $this->getBackend();
387 $this->assertTrue( $backend->canSetUser() );
388 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
389 $backend->setUser( $user );
390 $this->assertSame( $user, $backend->getUser() );
393 public function testDirty() {
394 $backend = $this->getBackend();
395 $priv = TestingAccessWrapper
::newFromObject( $backend );
396 $priv->dataDirty
= false;
398 $this->assertTrue( $priv->dataDirty
);
401 public function testGetData() {
402 $backend = $this->getBackend();
403 $data = $backend->getData();
404 $this->assertSame( [], $data );
405 $this->assertTrue( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
406 $data['???'] = '!!!';
407 $this->assertSame( [ '???' => '!!!' ], $data );
409 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
410 $this->store
->setSessionData( self
::SESSIONID
, $testData );
411 $backend = $this->getBackend();
412 $this->assertSame( $testData, $backend->getData() );
413 $this->assertFalse( TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
416 public function testAddData() {
417 $backend = $this->getBackend();
418 $priv = TestingAccessWrapper
::newFromObject( $backend );
420 $priv->data
= [ 'foo' => 1 ];
421 $priv->dataDirty
= false;
422 $backend->addData( [ 'foo' => 1 ] );
423 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
424 $this->assertFalse( $priv->dataDirty
);
426 $priv->data
= [ 'foo' => 1 ];
427 $priv->dataDirty
= false;
428 $backend->addData( [ 'foo' => '1' ] );
429 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
430 $this->assertTrue( $priv->dataDirty
);
432 $priv->data
= [ 'foo' => 1 ];
433 $priv->dataDirty
= false;
434 $backend->addData( [ 'bar' => 2 ] );
435 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
436 $this->assertTrue( $priv->dataDirty
);
439 public function testDelaySave() {
440 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
441 $backend = $this->getBackend();
442 $priv = TestingAccessWrapper
::newFromObject( $backend );
443 $priv->persist
= true;
445 // Saves happen normally when no delay is in effect
446 $this->onSessionMetadataCalled
= false;
447 $priv->metaDirty
= true;
449 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
451 $this->onSessionMetadataCalled
= false;
452 $priv->metaDirty
= true;
454 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
456 $delay = $backend->delaySave();
458 // Autosave doesn't happen when no delay is in effect
459 $this->onSessionMetadataCalled
= false;
460 $priv->metaDirty
= true;
462 $this->assertFalse( $this->onSessionMetadataCalled
);
464 // Save still does happen when no delay is in effect
466 $this->assertTrue( $this->onSessionMetadataCalled
);
468 // Save happens when delay is consumed
469 $this->onSessionMetadataCalled
= false;
470 $priv->metaDirty
= true;
471 \Wikimedia\ScopedCallback
::consume( $delay );
472 $this->assertTrue( $this->onSessionMetadataCalled
);
474 // Test multiple delays
475 $delay1 = $backend->delaySave();
476 $delay2 = $backend->delaySave();
477 $delay3 = $backend->delaySave();
478 $this->onSessionMetadataCalled
= false;
479 $priv->metaDirty
= true;
481 $this->assertFalse( $this->onSessionMetadataCalled
);
482 \Wikimedia\ScopedCallback
::consume( $delay3 );
483 $this->assertFalse( $this->onSessionMetadataCalled
);
484 \Wikimedia\ScopedCallback
::consume( $delay1 );
485 $this->assertFalse( $this->onSessionMetadataCalled
);
486 \Wikimedia\ScopedCallback
::consume( $delay2 );
487 $this->assertTrue( $this->onSessionMetadataCalled
);
490 public function testSave() {
491 $user = static::getTestSysop()->getUser();
492 $this->store
= new TestBagOStuff();
493 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
495 $neverHook = $this->getMockBuilder( __CLASS__
)
496 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
497 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
499 $builder = $this->getMockBuilder( 'DummySessionProvider' )
500 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
502 $neverProvider = $builder->getMock();
503 $neverProvider->expects( $this->never() )->method( 'persistSession' );
504 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
506 // Not persistent or dirty
507 $this->provider
= $neverProvider;
508 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
509 $this->store
->setSessionData( self
::SESSIONID
, $testData );
510 $backend = $this->getBackend( $user );
511 $this->store
->deleteSession( self
::SESSIONID
);
512 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
513 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
514 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
516 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
518 // (but does unpersist if forced)
519 $this->provider
= $builder->getMock();
520 $this->provider
->expects( $this->never() )->method( 'persistSession' );
521 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
522 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
523 $this->store
->setSessionData( self
::SESSIONID
, $testData );
524 $backend = $this->getBackend( $user );
525 $this->store
->deleteSession( self
::SESSIONID
);
526 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
527 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
528 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
529 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
530 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
532 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
534 // (but not to a WebRequest associated with a different session)
535 $this->provider
= $neverProvider;
536 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
537 $this->store
->setSessionData( self
::SESSIONID
, $testData );
538 $backend = $this->getBackend( $user );
539 TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
540 ->setSessionId( new SessionId( 'x' ) );
541 $this->store
->deleteSession( self
::SESSIONID
);
542 TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
543 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
544 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
545 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
546 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
548 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
550 // Not persistent, but dirty
551 $this->provider
= $neverProvider;
552 $this->onSessionMetadataCalled
= false;
553 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
554 $this->store
->setSessionData( self
::SESSIONID
, $testData );
555 $backend = $this->getBackend( $user );
556 $this->store
->deleteSession( self
::SESSIONID
);
557 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
558 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
559 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
561 $this->assertTrue( $this->onSessionMetadataCalled
);
562 $blob = $this->store
->getSession( self
::SESSIONID
);
563 $this->assertInternalType( 'array', $blob );
564 $this->assertArrayHasKey( 'metadata', $blob );
565 $metadata = $blob['metadata'];
566 $this->assertInternalType( 'array', $metadata );
567 $this->assertArrayHasKey( '???', $metadata );
568 $this->assertSame( '!!!', $metadata['???'] );
569 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
570 'making sure it didn\'t save to backend' );
572 // Persistent, not dirty
573 $this->provider
= $neverProvider;
574 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
575 $this->store
->setSessionData( self
::SESSIONID
, $testData );
576 $backend = $this->getBackend( $user );
577 $this->store
->deleteSession( self
::SESSIONID
);
578 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
579 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
580 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
581 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
583 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
585 // (but will persist if forced)
586 $this->provider
= $builder->getMock();
587 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
588 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
589 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
590 $this->store
->setSessionData( self
::SESSIONID
, $testData );
591 $backend = $this->getBackend( $user );
592 $this->store
->deleteSession( self
::SESSIONID
);
593 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
594 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
595 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
596 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
597 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
599 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
601 // Persistent and dirty
602 $this->provider
= $neverProvider;
603 $this->onSessionMetadataCalled
= false;
604 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
605 $this->store
->setSessionData( self
::SESSIONID
, $testData );
606 $backend = $this->getBackend( $user );
607 $this->store
->deleteSession( self
::SESSIONID
);
608 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
609 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
610 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
611 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
613 $this->assertTrue( $this->onSessionMetadataCalled
);
614 $blob = $this->store
->getSession( self
::SESSIONID
);
615 $this->assertInternalType( 'array', $blob );
616 $this->assertArrayHasKey( 'metadata', $blob );
617 $metadata = $blob['metadata'];
618 $this->assertInternalType( 'array', $metadata );
619 $this->assertArrayHasKey( '???', $metadata );
620 $this->assertSame( '!!!', $metadata['???'] );
621 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
622 'making sure it did save to backend' );
624 // (also persists if forced)
625 $this->provider
= $builder->getMock();
626 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
627 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
628 $this->onSessionMetadataCalled
= false;
629 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
630 $this->store
->setSessionData( self
::SESSIONID
, $testData );
631 $backend = $this->getBackend( $user );
632 $this->store
->deleteSession( self
::SESSIONID
);
633 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
634 TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
635 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
636 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
637 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
639 $this->assertTrue( $this->onSessionMetadataCalled
);
640 $blob = $this->store
->getSession( self
::SESSIONID
);
641 $this->assertInternalType( 'array', $blob );
642 $this->assertArrayHasKey( 'metadata', $blob );
643 $metadata = $blob['metadata'];
644 $this->assertInternalType( 'array', $metadata );
645 $this->assertArrayHasKey( '???', $metadata );
646 $this->assertSame( '!!!', $metadata['???'] );
647 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
648 'making sure it did save to backend' );
650 // (also persists if metadata dirty)
651 $this->provider
= $builder->getMock();
652 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
653 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
654 $this->onSessionMetadataCalled
= false;
655 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
656 $this->store
->setSessionData( self
::SESSIONID
, $testData );
657 $backend = $this->getBackend( $user );
658 $this->store
->deleteSession( self
::SESSIONID
);
659 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
660 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
661 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
662 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
664 $this->assertTrue( $this->onSessionMetadataCalled
);
665 $blob = $this->store
->getSession( self
::SESSIONID
);
666 $this->assertInternalType( 'array', $blob );
667 $this->assertArrayHasKey( 'metadata', $blob );
668 $metadata = $blob['metadata'];
669 $this->assertInternalType( 'array', $metadata );
670 $this->assertArrayHasKey( '???', $metadata );
671 $this->assertSame( '!!!', $metadata['???'] );
672 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
673 'making sure it did save to backend' );
675 // Not marked dirty, but dirty data
676 // (e.g. indirect modification from ArrayAccess::offsetGet)
677 $this->provider
= $neverProvider;
678 $this->onSessionMetadataCalled
= false;
679 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
680 $this->store
->setSessionData( self
::SESSIONID
, $testData );
681 $backend = $this->getBackend( $user );
682 $this->store
->deleteSession( self
::SESSIONID
);
683 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
684 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
685 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
686 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
687 TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
689 $this->assertTrue( $this->onSessionMetadataCalled
);
690 $blob = $this->store
->getSession( self
::SESSIONID
);
691 $this->assertInternalType( 'array', $blob );
692 $this->assertArrayHasKey( 'metadata', $blob );
693 $metadata = $blob['metadata'];
694 $this->assertInternalType( 'array', $metadata );
695 $this->assertArrayHasKey( '???', $metadata );
696 $this->assertSame( '!!!', $metadata['???'] );
697 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
698 'making sure it did save to backend' );
701 $this->provider
= null;
702 $mockHook = $this->getMockBuilder( __CLASS__
)
703 ->setMethods( [ 'onSessionMetadata' ] )->getMock();
704 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
705 ->will( $this->returnCallback(
706 function ( SessionBackend
$backend, array &$metadata, array $requests ) {
707 $metadata['userId']++
;
710 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
711 $this->store
->setSessionData( self
::SESSIONID
, $testData );
712 $backend = $this->getBackend( $user );
716 $this->fail( 'Expected exception not thrown' );
717 } catch ( \UnexpectedValueException
$ex ) {
719 'SessionMetadata hook changed metadata key "userId"',
724 // SessionManager::preventSessionsForUser
725 TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
726 $user->getName() => true,
728 $this->provider
= $neverProvider;
729 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
730 $this->store
->setSessionData( self
::SESSIONID
, $testData );
731 $backend = $this->getBackend( $user );
732 $this->store
->deleteSession( self
::SESSIONID
);
733 TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
734 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
735 TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
736 TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
738 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
741 public function testRenew() {
742 $user = static::getTestSysop()->getUser();
743 $this->store
= new TestBagOStuff();
744 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
747 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
748 ->setMethods( [ 'persistSession' ] )->getMock();
749 $this->provider
->expects( $this->never() )->method( 'persistSession' );
750 $this->onSessionMetadataCalled
= false;
751 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
752 $this->store
->setSessionData( self
::SESSIONID
, $testData );
753 $backend = $this->getBackend( $user );
754 $this->store
->deleteSession( self
::SESSIONID
);
755 $wrap = TestingAccessWrapper
::newFromObject( $backend );
756 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
757 $wrap->metaDirty
= false;
758 $wrap->dataDirty
= false;
759 $wrap->forcePersist
= false;
762 $this->assertTrue( $this->onSessionMetadataCalled
);
763 $blob = $this->store
->getSession( self
::SESSIONID
);
764 $this->assertInternalType( 'array', $blob );
765 $this->assertArrayHasKey( 'metadata', $blob );
766 $metadata = $blob['metadata'];
767 $this->assertInternalType( 'array', $metadata );
768 $this->assertArrayHasKey( '???', $metadata );
769 $this->assertSame( '!!!', $metadata['???'] );
770 $this->assertNotEquals( 0, $wrap->expires
);
773 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
774 ->setMethods( [ 'persistSession' ] )->getMock();
775 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
776 $this->onSessionMetadataCalled
= false;
777 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
778 $this->store
->setSessionData( self
::SESSIONID
, $testData );
779 $backend = $this->getBackend( $user );
780 $this->store
->deleteSession( self
::SESSIONID
);
781 $wrap = TestingAccessWrapper
::newFromObject( $backend );
782 $wrap->persist
= true;
783 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
784 $wrap->metaDirty
= false;
785 $wrap->dataDirty
= false;
786 $wrap->forcePersist
= false;
789 $this->assertTrue( $this->onSessionMetadataCalled
);
790 $blob = $this->store
->getSession( self
::SESSIONID
);
791 $this->assertInternalType( 'array', $blob );
792 $this->assertArrayHasKey( 'metadata', $blob );
793 $metadata = $blob['metadata'];
794 $this->assertInternalType( 'array', $metadata );
795 $this->assertArrayHasKey( '???', $metadata );
796 $this->assertSame( '!!!', $metadata['???'] );
797 $this->assertNotEquals( 0, $wrap->expires
);
799 // Not persistent, not expiring
800 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
801 ->setMethods( [ 'persistSession' ] )->getMock();
802 $this->provider
->expects( $this->never() )->method( 'persistSession' );
803 $this->onSessionMetadataCalled
= false;
804 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
805 $this->store
->setSessionData( self
::SESSIONID
, $testData );
806 $backend = $this->getBackend( $user );
807 $this->store
->deleteSession( self
::SESSIONID
);
808 $wrap = TestingAccessWrapper
::newFromObject( $backend );
809 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
810 $wrap->metaDirty
= false;
811 $wrap->dataDirty
= false;
812 $wrap->forcePersist
= false;
813 $expires = time() +
$wrap->lifetime +
100;
814 $wrap->expires
= $expires;
816 $this->assertFalse( $this->onSessionMetadataCalled
);
817 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
818 $this->assertEquals( $expires, $wrap->expires
);
821 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
822 $this->onSessionMetadataCalled
= true;
823 $metadata['???'] = '!!!';
826 public function testTakeOverGlobalSession() {
827 if ( !PHPSessionHandler
::isInstalled() ) {
828 PHPSessionHandler
::install( SessionManager
::singleton() );
830 if ( !PHPSessionHandler
::isEnabled() ) {
831 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
832 $rProp->setAccessible( true );
833 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
834 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
835 session_write_close();
836 $handler->enable
= false;
838 $handler->enable
= true;
841 $backend = $this->getBackend( static::getTestSysop()->getUser() );
842 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
844 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
846 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
847 $request = \RequestContext
::getMain()->getRequest();
848 $manager->globalSession
= $backend->getSession( $request );
849 $manager->globalSessionRequest
= $request;
852 TestingAccessWrapper
::newFromObject( $backend )->checkPHPSession();
853 $this->assertSame( $backend->getId(), session_id() );
854 session_write_close();
856 $backend2 = $this->getBackend(
857 User
::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
859 TestingAccessWrapper
::newFromObject( $backend2 )->usePhpSessionHandling
= true;
862 TestingAccessWrapper
::newFromObject( $backend2 )->checkPHPSession();
863 $this->assertSame( '', session_id() );
866 public function testResetIdOfGlobalSession() {
867 if ( !PHPSessionHandler
::isInstalled() ) {
868 PHPSessionHandler
::install( SessionManager
::singleton() );
870 if ( !PHPSessionHandler
::isEnabled() ) {
871 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
872 $rProp->setAccessible( true );
873 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
874 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
875 session_write_close();
876 $handler->enable
= false;
878 $handler->enable
= true;
881 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
882 TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
884 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
886 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
887 $request = \RequestContext
::getMain()->getRequest();
888 $manager->globalSession
= $backend->getSession( $request );
889 $manager->globalSessionRequest
= $request;
891 session_id( self
::SESSIONID
);
892 \MediaWiki\
quietCall( 'session_start' );
893 $_SESSION['foo'] = __METHOD__
;
895 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
896 $this->assertSame( $backend->getId(), session_id() );
897 $this->assertArrayHasKey( 'foo', $_SESSION );
898 $this->assertSame( __METHOD__
, $_SESSION['foo'] );
899 session_write_close();
902 public function testUnpersistOfGlobalSession() {
903 if ( !PHPSessionHandler
::isInstalled() ) {
904 PHPSessionHandler
::install( SessionManager
::singleton() );
906 if ( !PHPSessionHandler
::isEnabled() ) {
907 $rProp = new \
ReflectionProperty( PHPSessionHandler
::class, 'instance' );
908 $rProp->setAccessible( true );
909 $handler = TestingAccessWrapper
::newFromObject( $rProp->getValue() );
910 $resetHandler = new \Wikimedia\
ScopedCallback( function () use ( $handler ) {
911 session_write_close();
912 $handler->enable
= false;
914 $handler->enable
= true;
917 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
918 $wrap = TestingAccessWrapper
::newFromObject( $backend );
919 $wrap->usePhpSessionHandling
= true;
920 $wrap->persist
= true;
922 $resetSingleton = TestUtils
::setSessionManagerSingleton( $this->manager
);
924 $manager = TestingAccessWrapper
::newFromObject( $this->manager
);
925 $request = \RequestContext
::getMain()->getRequest();
926 $manager->globalSession
= $backend->getSession( $request );
927 $manager->globalSessionRequest
= $request;
929 session_id( self
::SESSIONID
. 'x' );
930 \MediaWiki\
quietCall( 'session_start' );
931 $backend->unpersist();
932 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
934 session_id( self
::SESSIONID
);
935 $wrap->persist
= true;
936 $backend->unpersist();
937 $this->assertSame( '', session_id() );
940 public function testGetAllowedUserRights() {
941 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
942 ->setMethods( [ 'getAllowedUserRights' ] )
944 $this->provider
->expects( $this->any() )->method( 'getAllowedUserRights' )
945 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
947 $backend = $this->getBackend();
948 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );