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
27 protected function getBackend( User
$user = null ) {
28 if ( !$this->config
) {
29 $this->config
= new \
HashConfig();
30 $this->manager
= null;
32 if ( !$this->store
) {
33 $this->store
= new TestBagOStuff();
34 $this->manager
= null;
37 $logger = new \Psr\Log\
NullLogger();
38 if ( !$this->manager
) {
39 $this->manager
= new SessionManager( [
40 'store' => $this->store
,
42 'config' => $this->config
,
46 if ( !$this->provider
) {
47 $this->provider
= new \
DummySessionProvider();
49 $this->provider
->setLogger( $logger );
50 $this->provider
->setConfig( $this->config
);
51 $this->provider
->setManager( $this->manager
);
53 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
54 'provider' => $this->provider
,
55 'id' => self
::SESSIONID
,
57 'userInfo' => UserInfo
::newFromUser( $user ?
: new User
, true ),
60 $id = new SessionId( $info->getId() );
62 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
63 $priv = \TestingAccessWrapper
::newFromObject( $backend );
64 $priv->persist
= false;
65 $priv->requests
= [ 100 => new \
FauxRequest() ];
66 $priv->requests
[100]->setSessionId( $id );
67 $priv->usePhpSessionHandling
= false;
69 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
70 $manager->allSessionBackends
= [ $backend->getId() => $backend ];
71 $manager->allSessionIds
= [ $backend->getId() => $id ];
72 $manager->sessionProviders
= [ (string)$this->provider
=> $this->provider
];
77 public function testConstructor() {
81 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
82 'provider' => $this->provider
,
83 'id' => self
::SESSIONID
,
85 'userInfo' => UserInfo
::newFromName( 'UTSysop', false ),
88 $id = new SessionId( $info->getId() );
89 $logger = new \Psr\Log\
NullLogger();
91 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
92 $this->fail( 'Expected exception not thrown' );
93 } catch ( \InvalidArgumentException
$ex ) {
95 "Refusing to create session for unverified user {$info->getUserInfo()}",
100 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
101 'id' => self
::SESSIONID
,
102 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
105 $id = new SessionId( $info->getId() );
107 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
108 $this->fail( 'Expected exception not thrown' );
109 } catch ( \InvalidArgumentException
$ex ) {
110 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
113 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
114 'provider' => $this->provider
,
115 'id' => self
::SESSIONID
,
117 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
120 $id = new SessionId( '!' . $info->getId() );
122 new SessionBackend( $id, $info, $this->store
, $logger, 10 );
123 $this->fail( 'Expected exception not thrown' );
124 } catch ( \InvalidArgumentException
$ex ) {
126 'SessionId and SessionInfo don\'t match',
131 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
132 'provider' => $this->provider
,
133 'id' => self
::SESSIONID
,
135 'userInfo' => UserInfo
::newFromName( 'UTSysop', true ),
138 $id = new SessionId( $info->getId() );
139 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
140 $this->assertSame( self
::SESSIONID
, $backend->getId() );
141 $this->assertSame( $id, $backend->getSessionId() );
142 $this->assertSame( $this->provider
, $backend->getProvider() );
143 $this->assertInstanceOf( 'User', $backend->getUser() );
144 $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
145 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
146 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
147 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
149 $expire = time() +
100;
150 $this->store
->setSessionMeta( self
::SESSIONID
, [ 'expires' => $expire ], 2 );
152 $info = new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
153 'provider' => $this->provider
,
154 'id' => self
::SESSIONID
,
156 'forceHTTPS' => true,
157 'metadata' => [ 'foo' ],
160 $id = new SessionId( $info->getId() );
161 $backend = new SessionBackend( $id, $info, $this->store
, $logger, 10 );
162 $this->assertSame( self
::SESSIONID
, $backend->getId() );
163 $this->assertSame( $id, $backend->getSessionId() );
164 $this->assertSame( $this->provider
, $backend->getProvider() );
165 $this->assertInstanceOf( 'User', $backend->getUser() );
166 $this->assertTrue( $backend->getUser()->isAnon() );
167 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
168 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
169 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
170 $this->assertSame( $expire, \TestingAccessWrapper
::newFromObject( $backend )->expires
);
171 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
174 public function testSessionStuff() {
175 $backend = $this->getBackend();
176 $priv = \TestingAccessWrapper
::newFromObject( $backend );
177 $priv->requests
= []; // Remove dummy session
179 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
181 $request1 = new \
FauxRequest();
182 $session1 = $backend->getSession( $request1 );
183 $request2 = new \
FauxRequest();
184 $session2 = $backend->getSession( $request2 );
186 $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session1 );
187 $this->assertInstanceOf( 'MediaWiki\\Session\\Session', $session2 );
188 $this->assertSame( 2, count( $priv->requests
) );
190 $index = \TestingAccessWrapper
::newFromObject( $session1 )->index
;
192 $this->assertSame( $request1, $backend->getRequest( $index ) );
193 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
194 $request1->setCookie( 'UserName', 'Example' );
195 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
198 $this->assertSame( 1, count( $priv->requests
) );
199 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
200 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
202 $backend->getRequest( $index );
203 $this->fail( 'Expected exception not thrown' );
204 } catch ( \InvalidArgumentException
$ex ) {
205 $this->assertSame( 'Invalid session index', $ex->getMessage() );
208 $backend->suggestLoginUsername( $index );
209 $this->fail( 'Expected exception not thrown' );
210 } catch ( \InvalidArgumentException
$ex ) {
211 $this->assertSame( 'Invalid session index', $ex->getMessage() );
215 $this->assertSame( 0, count( $priv->requests
) );
216 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends
);
217 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds
);
220 public function testSetProviderMetadata() {
221 $backend = $this->getBackend();
222 $priv = \TestingAccessWrapper
::newFromObject( $backend );
223 $priv->providerMetadata
= [ 'dummy' ];
226 $backend->setProviderMetadata( 'foo' );
227 $this->fail( 'Expected exception not thrown' );
228 } catch ( \InvalidArgumentException
$ex ) {
229 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
233 $backend->setProviderMetadata( (object)[] );
234 $this->fail( 'Expected exception not thrown' );
235 } catch ( \InvalidArgumentException
$ex ) {
236 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
239 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
240 $backend->setProviderMetadata( [ 'dummy' ] );
241 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
243 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
244 $backend->setProviderMetadata( [ 'test' ] );
245 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
246 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
247 $this->store
->deleteSession( self
::SESSIONID
);
249 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
250 $backend->setProviderMetadata( null );
251 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
) );
252 $this->assertSame( null, $backend->getProviderMetadata() );
253 $this->store
->deleteSession( self
::SESSIONID
);
256 public function testResetId() {
259 $builder = $this->getMockBuilder( 'DummySessionProvider' )
260 ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
262 $this->provider
= $builder->getMock();
263 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
264 ->will( $this->returnValue( false ) );
265 $this->provider
->expects( $this->never() )->method( 'sessionIdWasReset' );
266 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
267 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
268 $sessionId = $backend->getSessionId();
270 $this->assertSame( self
::SESSIONID
, $backend->getId() );
271 $this->assertSame( $backend->getId(), $sessionId->getId() );
272 $this->assertSame( $id, session_id() );
273 $this->assertSame( $backend, $manager->allSessionBackends
[self
::SESSIONID
] );
275 $this->provider
= $builder->getMock();
276 $this->provider
->expects( $this->any() )->method( 'persistsSessionId' )
277 ->will( $this->returnValue( true ) );
278 $backend = $this->getBackend();
279 $this->provider
->expects( $this->once() )->method( 'sessionIdWasReset' )
280 ->with( $this->identicalTo( $backend ), $this->identicalTo( self
::SESSIONID
) );
281 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
282 $sessionId = $backend->getSessionId();
284 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
285 $this->assertSame( $backend->getId(), $sessionId->getId() );
286 $this->assertInternalType( 'array', $this->store
->getSession( $backend->getId() ) );
287 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
288 $this->assertSame( $id, session_id() );
289 $this->assertArrayNotHasKey( self
::SESSIONID
, $manager->allSessionBackends
);
290 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends
);
291 $this->assertSame( $backend, $manager->allSessionBackends
[$backend->getId()] );
294 public function testPersist() {
295 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
296 $this->provider
->expects( $this->once() )->method( 'persistSession' );
297 $backend = $this->getBackend();
298 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
299 $backend->save(); // This one shouldn't call $provider->persistSession()
302 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
304 $this->provider
= null;
305 $backend = $this->getBackend();
306 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
307 $wrap->persist
= true;
310 $this->assertNotEquals( 0, $wrap->expires
);
313 public function testUnpersist() {
314 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'unpersistSession' ] );
315 $this->provider
->expects( $this->once() )->method( 'unpersistSession' );
316 $backend = $this->getBackend();
317 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
318 $wrap->store
= new \
CachedBagOStuff( $this->store
);
319 $wrap->persist
= true;
320 $wrap->dataDirty
= true;
322 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
323 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
324 $this->assertNotFalse( $this->store
->getSession( self
::SESSIONID
), 'sanity check' );
326 $backend->unpersist();
327 $this->assertFalse( $backend->isPersistent() );
328 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
) );
329 $this->assertNotFalse( $wrap->store
->get( wfMemcKey( 'MWSession', self
::SESSIONID
) ) );
332 public function testRememberUser() {
333 $backend = $this->getBackend();
335 $remembered = $backend->shouldRememberUser();
336 $backend->setRememberUser( !$remembered );
337 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
338 $backend->setRememberUser( $remembered );
339 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
342 public function testForceHTTPS() {
343 $backend = $this->getBackend();
345 $force = $backend->shouldForceHTTPS();
346 $backend->setForceHTTPS( !$force );
347 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
348 $backend->setForceHTTPS( $force );
349 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
352 public function testLoggedOutTimestamp() {
353 $backend = $this->getBackend();
355 $backend->setLoggedOutTimestamp( 42 );
356 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
357 $backend->setLoggedOutTimestamp( '123' );
358 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
361 public function testSetUser() {
362 $user = User
::newFromName( 'UTSysop' );
364 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'canChangeUser' ] );
365 $this->provider
->expects( $this->any() )->method( 'canChangeUser' )
366 ->will( $this->returnValue( false ) );
367 $backend = $this->getBackend();
368 $this->assertFalse( $backend->canSetUser() );
370 $backend->setUser( $user );
371 $this->fail( 'Expected exception not thrown' );
372 } catch ( \BadMethodCallException
$ex ) {
374 'Cannot set user on this session; check $session->canSetUser() first',
378 $this->assertNotSame( $user, $backend->getUser() );
380 $this->provider
= null;
381 $backend = $this->getBackend();
382 $this->assertTrue( $backend->canSetUser() );
383 $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
384 $backend->setUser( $user );
385 $this->assertSame( $user, $backend->getUser() );
388 public function testDirty() {
389 $backend = $this->getBackend();
390 $priv = \TestingAccessWrapper
::newFromObject( $backend );
391 $priv->dataDirty
= false;
393 $this->assertTrue( $priv->dataDirty
);
396 public function testGetData() {
397 $backend = $this->getBackend();
398 $data = $backend->getData();
399 $this->assertSame( [], $data );
400 $this->assertTrue( \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
401 $data['???'] = '!!!';
402 $this->assertSame( [ '???' => '!!!' ], $data );
404 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
405 $this->store
->setSessionData( self
::SESSIONID
, $testData );
406 $backend = $this->getBackend();
407 $this->assertSame( $testData, $backend->getData() );
408 $this->assertFalse( \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
);
411 public function testAddData() {
412 $backend = $this->getBackend();
413 $priv = \TestingAccessWrapper
::newFromObject( $backend );
415 $priv->data
= [ 'foo' => 1 ];
416 $priv->dataDirty
= false;
417 $backend->addData( [ 'foo' => 1 ] );
418 $this->assertSame( [ 'foo' => 1 ], $priv->data
);
419 $this->assertFalse( $priv->dataDirty
);
421 $priv->data
= [ 'foo' => 1 ];
422 $priv->dataDirty
= false;
423 $backend->addData( [ 'foo' => '1' ] );
424 $this->assertSame( [ 'foo' => '1' ], $priv->data
);
425 $this->assertTrue( $priv->dataDirty
);
427 $priv->data
= [ 'foo' => 1 ];
428 $priv->dataDirty
= false;
429 $backend->addData( [ 'bar' => 2 ] );
430 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data
);
431 $this->assertTrue( $priv->dataDirty
);
434 public function testDelaySave() {
435 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
436 $backend = $this->getBackend();
437 $priv = \TestingAccessWrapper
::newFromObject( $backend );
438 $priv->persist
= true;
440 // Saves happen normally when no delay is in effect
441 $this->onSessionMetadataCalled
= false;
442 $priv->metaDirty
= true;
444 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
446 $this->onSessionMetadataCalled
= false;
447 $priv->metaDirty
= true;
449 $this->assertTrue( $this->onSessionMetadataCalled
, 'sanity check' );
451 $delay = $backend->delaySave();
453 // Autosave doesn't happen when no delay is in effect
454 $this->onSessionMetadataCalled
= false;
455 $priv->metaDirty
= true;
457 $this->assertFalse( $this->onSessionMetadataCalled
);
459 // Save still does happen when no delay is in effect
461 $this->assertTrue( $this->onSessionMetadataCalled
);
463 // Save happens when delay is consumed
464 $this->onSessionMetadataCalled
= false;
465 $priv->metaDirty
= true;
466 \ScopedCallback
::consume( $delay );
467 $this->assertTrue( $this->onSessionMetadataCalled
);
469 // Test multiple delays
470 $delay1 = $backend->delaySave();
471 $delay2 = $backend->delaySave();
472 $delay3 = $backend->delaySave();
473 $this->onSessionMetadataCalled
= false;
474 $priv->metaDirty
= true;
476 $this->assertFalse( $this->onSessionMetadataCalled
);
477 \ScopedCallback
::consume( $delay3 );
478 $this->assertFalse( $this->onSessionMetadataCalled
);
479 \ScopedCallback
::consume( $delay1 );
480 $this->assertFalse( $this->onSessionMetadataCalled
);
481 \ScopedCallback
::consume( $delay2 );
482 $this->assertTrue( $this->onSessionMetadataCalled
);
485 public function testSave() {
486 $user = User
::newFromName( 'UTSysop' );
487 $this->store
= new TestBagOStuff();
488 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
490 $neverHook = $this->getMock( __CLASS__
, [ 'onSessionMetadata' ] );
491 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
493 $builder = $this->getMockBuilder( 'DummySessionProvider' )
494 ->setMethods( [ 'persistSession', 'unpersistSession' ] );
496 $neverProvider = $builder->getMock();
497 $neverProvider->expects( $this->never() )->method( 'persistSession' );
498 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
500 // Not persistent or dirty
501 $this->provider
= $neverProvider;
502 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
503 $this->store
->setSessionData( self
::SESSIONID
, $testData );
504 $backend = $this->getBackend( $user );
505 $this->store
->deleteSession( self
::SESSIONID
);
506 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
507 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
508 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
510 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
512 // (but does unpersist if forced)
513 $this->provider
= $builder->getMock();
514 $this->provider
->expects( $this->never() )->method( 'persistSession' );
515 $this->provider
->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
516 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
517 $this->store
->setSessionData( self
::SESSIONID
, $testData );
518 $backend = $this->getBackend( $user );
519 $this->store
->deleteSession( self
::SESSIONID
);
520 \TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
521 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
522 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
523 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
524 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
526 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
528 // (but not to a WebRequest associated with a different session)
529 $this->provider
= $neverProvider;
530 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
531 $this->store
->setSessionData( self
::SESSIONID
, $testData );
532 $backend = $this->getBackend( $user );
533 \TestingAccessWrapper
::newFromObject( $backend )->requests
[100]
534 ->setSessionId( new SessionId( 'x' ) );
535 $this->store
->deleteSession( self
::SESSIONID
);
536 \TestingAccessWrapper
::newFromObject( $backend )->persist
= false;
537 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
538 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
539 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
540 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
542 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
544 // Not persistent, but dirty
545 $this->provider
= $neverProvider;
546 $this->onSessionMetadataCalled
= false;
547 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
548 $this->store
->setSessionData( self
::SESSIONID
, $testData );
549 $backend = $this->getBackend( $user );
550 $this->store
->deleteSession( self
::SESSIONID
);
551 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
552 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
553 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
555 $this->assertTrue( $this->onSessionMetadataCalled
);
556 $blob = $this->store
->getSession( self
::SESSIONID
);
557 $this->assertInternalType( 'array', $blob );
558 $this->assertArrayHasKey( 'metadata', $blob );
559 $metadata = $blob['metadata'];
560 $this->assertInternalType( 'array', $metadata );
561 $this->assertArrayHasKey( '???', $metadata );
562 $this->assertSame( '!!!', $metadata['???'] );
563 $this->assertFalse( $this->store
->getSessionFromBackend( self
::SESSIONID
),
564 'making sure it didn\'t save to backend' );
566 // Persistent, not dirty
567 $this->provider
= $neverProvider;
568 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
569 $this->store
->setSessionData( self
::SESSIONID
, $testData );
570 $backend = $this->getBackend( $user );
571 $this->store
->deleteSession( self
::SESSIONID
);
572 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
573 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
574 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
575 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
577 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
579 // (but will persist if forced)
580 $this->provider
= $builder->getMock();
581 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
582 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
583 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
584 $this->store
->setSessionData( self
::SESSIONID
, $testData );
585 $backend = $this->getBackend( $user );
586 $this->store
->deleteSession( self
::SESSIONID
);
587 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
588 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
589 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
590 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
591 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
593 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
595 // Persistent and dirty
596 $this->provider
= $neverProvider;
597 $this->onSessionMetadataCalled
= false;
598 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
599 $this->store
->setSessionData( self
::SESSIONID
, $testData );
600 $backend = $this->getBackend( $user );
601 $this->store
->deleteSession( self
::SESSIONID
);
602 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
603 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
604 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
605 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
607 $this->assertTrue( $this->onSessionMetadataCalled
);
608 $blob = $this->store
->getSession( self
::SESSIONID
);
609 $this->assertInternalType( 'array', $blob );
610 $this->assertArrayHasKey( 'metadata', $blob );
611 $metadata = $blob['metadata'];
612 $this->assertInternalType( 'array', $metadata );
613 $this->assertArrayHasKey( '???', $metadata );
614 $this->assertSame( '!!!', $metadata['???'] );
615 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
616 'making sure it did save to backend' );
618 // (also persists if forced)
619 $this->provider
= $builder->getMock();
620 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
621 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
622 $this->onSessionMetadataCalled
= false;
623 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
624 $this->store
->setSessionData( self
::SESSIONID
, $testData );
625 $backend = $this->getBackend( $user );
626 $this->store
->deleteSession( self
::SESSIONID
);
627 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
628 \TestingAccessWrapper
::newFromObject( $backend )->forcePersist
= true;
629 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
630 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
631 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
633 $this->assertTrue( $this->onSessionMetadataCalled
);
634 $blob = $this->store
->getSession( self
::SESSIONID
);
635 $this->assertInternalType( 'array', $blob );
636 $this->assertArrayHasKey( 'metadata', $blob );
637 $metadata = $blob['metadata'];
638 $this->assertInternalType( 'array', $metadata );
639 $this->assertArrayHasKey( '???', $metadata );
640 $this->assertSame( '!!!', $metadata['???'] );
641 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
642 'making sure it did save to backend' );
644 // (also persists if metadata dirty)
645 $this->provider
= $builder->getMock();
646 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
647 $this->provider
->expects( $this->never() )->method( 'unpersistSession' );
648 $this->onSessionMetadataCalled
= false;
649 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
650 $this->store
->setSessionData( self
::SESSIONID
, $testData );
651 $backend = $this->getBackend( $user );
652 $this->store
->deleteSession( self
::SESSIONID
);
653 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
654 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
655 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
656 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
658 $this->assertTrue( $this->onSessionMetadataCalled
);
659 $blob = $this->store
->getSession( self
::SESSIONID
);
660 $this->assertInternalType( 'array', $blob );
661 $this->assertArrayHasKey( 'metadata', $blob );
662 $metadata = $blob['metadata'];
663 $this->assertInternalType( 'array', $metadata );
664 $this->assertArrayHasKey( '???', $metadata );
665 $this->assertSame( '!!!', $metadata['???'] );
666 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
667 'making sure it did save to backend' );
669 // Not marked dirty, but dirty data
670 // (e.g. indirect modification from ArrayAccess::offsetGet)
671 $this->provider
= $neverProvider;
672 $this->onSessionMetadataCalled
= false;
673 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
674 $this->store
->setSessionData( self
::SESSIONID
, $testData );
675 $backend = $this->getBackend( $user );
676 $this->store
->deleteSession( self
::SESSIONID
);
677 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
678 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
679 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= false;
680 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= false;
681 \TestingAccessWrapper
::newFromObject( $backend )->dataHash
= 'Doesn\'t match';
683 $this->assertTrue( $this->onSessionMetadataCalled
);
684 $blob = $this->store
->getSession( self
::SESSIONID
);
685 $this->assertInternalType( 'array', $blob );
686 $this->assertArrayHasKey( 'metadata', $blob );
687 $metadata = $blob['metadata'];
688 $this->assertInternalType( 'array', $metadata );
689 $this->assertArrayHasKey( '???', $metadata );
690 $this->assertSame( '!!!', $metadata['???'] );
691 $this->assertNotSame( false, $this->store
->getSessionFromBackend( self
::SESSIONID
),
692 'making sure it did save to backend' );
695 $this->provider
= null;
696 $mockHook = $this->getMock( __CLASS__
, [ 'onSessionMetadata' ] );
697 $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
698 ->will( $this->returnCallback(
699 function ( SessionBackend
$backend, array &$metadata, array $requests ) {
700 $metadata['userId']++
;
703 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
704 $this->store
->setSessionData( self
::SESSIONID
, $testData );
705 $backend = $this->getBackend( $user );
709 $this->fail( 'Expected exception not thrown' );
710 } catch ( \UnexpectedValueException
$ex ) {
712 'SessionMetadata hook changed metadata key "userId"',
717 // SessionManager::preventSessionsForUser
718 \TestingAccessWrapper
::newFromObject( $this->manager
)->preventUsers
= [
719 $user->getName() => true,
721 $this->provider
= $neverProvider;
722 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
723 $this->store
->setSessionData( self
::SESSIONID
, $testData );
724 $backend = $this->getBackend( $user );
725 $this->store
->deleteSession( self
::SESSIONID
);
726 \TestingAccessWrapper
::newFromObject( $backend )->persist
= true;
727 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
728 \TestingAccessWrapper
::newFromObject( $backend )->metaDirty
= true;
729 \TestingAccessWrapper
::newFromObject( $backend )->dataDirty
= true;
731 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
734 public function testRenew() {
735 $user = User
::newFromName( 'UTSysop' );
736 $this->store
= new TestBagOStuff();
737 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
740 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
741 $this->provider
->expects( $this->never() )->method( 'persistSession' );
742 $this->onSessionMetadataCalled
= false;
743 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
744 $this->store
->setSessionData( self
::SESSIONID
, $testData );
745 $backend = $this->getBackend( $user );
746 $this->store
->deleteSession( self
::SESSIONID
);
747 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
748 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
749 $wrap->metaDirty
= false;
750 $wrap->dataDirty
= false;
751 $wrap->forcePersist
= false;
754 $this->assertTrue( $this->onSessionMetadataCalled
);
755 $blob = $this->store
->getSession( self
::SESSIONID
);
756 $this->assertInternalType( 'array', $blob );
757 $this->assertArrayHasKey( 'metadata', $blob );
758 $metadata = $blob['metadata'];
759 $this->assertInternalType( 'array', $metadata );
760 $this->assertArrayHasKey( '???', $metadata );
761 $this->assertSame( '!!!', $metadata['???'] );
762 $this->assertNotEquals( 0, $wrap->expires
);
765 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
766 $this->provider
->expects( $this->atLeastOnce() )->method( 'persistSession' );
767 $this->onSessionMetadataCalled
= false;
768 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
769 $this->store
->setSessionData( self
::SESSIONID
, $testData );
770 $backend = $this->getBackend( $user );
771 $this->store
->deleteSession( self
::SESSIONID
);
772 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
773 $wrap->persist
= true;
774 $this->assertTrue( $backend->isPersistent(), 'sanity check' );
775 $wrap->metaDirty
= false;
776 $wrap->dataDirty
= false;
777 $wrap->forcePersist
= false;
780 $this->assertTrue( $this->onSessionMetadataCalled
);
781 $blob = $this->store
->getSession( self
::SESSIONID
);
782 $this->assertInternalType( 'array', $blob );
783 $this->assertArrayHasKey( 'metadata', $blob );
784 $metadata = $blob['metadata'];
785 $this->assertInternalType( 'array', $metadata );
786 $this->assertArrayHasKey( '???', $metadata );
787 $this->assertSame( '!!!', $metadata['???'] );
788 $this->assertNotEquals( 0, $wrap->expires
);
790 // Not persistent, not expiring
791 $this->provider
= $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
792 $this->provider
->expects( $this->never() )->method( 'persistSession' );
793 $this->onSessionMetadataCalled
= false;
794 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
795 $this->store
->setSessionData( self
::SESSIONID
, $testData );
796 $backend = $this->getBackend( $user );
797 $this->store
->deleteSession( self
::SESSIONID
);
798 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
799 $this->assertFalse( $backend->isPersistent(), 'sanity check' );
800 $wrap->metaDirty
= false;
801 $wrap->dataDirty
= false;
802 $wrap->forcePersist
= false;
803 $expires = time() +
$wrap->lifetime +
100;
804 $wrap->expires
= $expires;
806 $this->assertFalse( $this->onSessionMetadataCalled
);
807 $this->assertFalse( $this->store
->getSession( self
::SESSIONID
), 'making sure it didn\'t save' );
808 $this->assertEquals( $expires, $wrap->expires
);
811 public function onSessionMetadata( SessionBackend
$backend, array &$metadata, array $requests ) {
812 $this->onSessionMetadataCalled
= true;
813 $metadata['???'] = '!!!';
816 public function testResetIdOfGlobalSession() {
817 if ( !PHPSessionHandler
::isInstalled() ) {
818 PHPSessionHandler
::install( SessionManager
::singleton() );
820 if ( !PHPSessionHandler
::isEnabled() ) {
821 $rProp = new \
ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
822 $rProp->setAccessible( true );
823 $handler = \TestingAccessWrapper
::newFromObject( $rProp->getValue() );
824 $resetHandler = new \
ScopedCallback( function () use ( $handler ) {
825 session_write_close();
826 $handler->enable
= false;
828 $handler->enable
= true;
831 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
832 \TestingAccessWrapper
::newFromObject( $backend )->usePhpSessionHandling
= true;
834 TestUtils
::setSessionManagerSingleton( $this->manager
);
836 $manager = \TestingAccessWrapper
::newFromObject( $this->manager
);
837 $request = \RequestContext
::getMain()->getRequest();
838 $manager->globalSession
= $backend->getSession( $request );
839 $manager->globalSessionRequest
= $request;
841 session_id( self
::SESSIONID
);
842 \MediaWiki\
quietCall( 'session_start' );
844 $this->assertNotEquals( self
::SESSIONID
, $backend->getId() );
845 $this->assertSame( $backend->getId(), session_id() );
846 session_write_close();
849 $this->assertNotSame( $backend->getId(), session_id(), 'sanity check' );
851 $this->assertSame( $backend->getId(), session_id() );
852 session_write_close();
855 public function testUnpersistOfGlobalSession() {
856 if ( !PHPSessionHandler
::isInstalled() ) {
857 PHPSessionHandler
::install( SessionManager
::singleton() );
859 if ( !PHPSessionHandler
::isEnabled() ) {
860 $rProp = new \
ReflectionProperty( 'MediaWiki\\Session\\PHPSessionHandler', 'instance' );
861 $rProp->setAccessible( true );
862 $handler = \TestingAccessWrapper
::newFromObject( $rProp->getValue() );
863 $resetHandler = new \
ScopedCallback( function () use ( $handler ) {
864 session_write_close();
865 $handler->enable
= false;
867 $handler->enable
= true;
870 $backend = $this->getBackend( User
::newFromName( 'UTSysop' ) );
871 $wrap = \TestingAccessWrapper
::newFromObject( $backend );
872 $wrap->usePhpSessionHandling
= true;
873 $wrap->persist
= true;
875 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
. 'x' );
883 \MediaWiki\
quietCall( 'session_start' );
884 $backend->unpersist();
885 $this->assertSame( self
::SESSIONID
. 'x', session_id() );
887 session_id( self
::SESSIONID
);
888 $wrap->persist
= true;
889 $backend->unpersist();
890 $this->assertSame( '', session_id() );
893 public function testGetAllowedUserRights() {
894 $this->provider
= $this->getMockBuilder( 'DummySessionProvider' )
895 ->setMethods( [ 'getAllowedUserRights' ] )
897 $this->provider
->expects( $this->any() )->method( 'getAllowedUserRights' )
898 ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
900 $backend = $this->getBackend();
901 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );