Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / session / SessionBackendTest.php
blob2fb66d6539fc12b389a198708fb7cecf74e8677f
1 <?php
3 namespace MediaWiki\Tests\Session;
5 use BadMethodCallException;
6 use DummySessionProvider;
7 use InvalidArgumentException;
8 use MediaWiki\Config\Config;
9 use MediaWiki\Config\HashConfig;
10 use MediaWiki\Context\RequestContext;
11 use MediaWiki\HookContainer\HookContainer;
12 use MediaWiki\Request\FauxRequest;
13 use MediaWiki\Session\PHPSessionHandler;
14 use MediaWiki\Session\Session;
15 use MediaWiki\Session\SessionBackend;
16 use MediaWiki\Session\SessionId;
17 use MediaWiki\Session\SessionInfo;
18 use MediaWiki\Session\SessionManager;
19 use MediaWiki\Session\SessionProvider;
20 use MediaWiki\Session\UserInfo;
21 use MediaWiki\User\User;
22 use MediaWikiIntegrationTestCase;
23 use Psr\Log\NullLogger;
24 use UnexpectedValueException;
25 use Wikimedia\ObjectCache\CachedBagOStuff;
26 use Wikimedia\ScopedCallback;
27 use Wikimedia\TestingAccessWrapper;
29 /**
30 * @group Session
31 * @group Database
32 * @covers \MediaWiki\Session\SessionBackend
34 class SessionBackendTest extends MediaWikiIntegrationTestCase {
35 use SessionProviderTestTrait;
37 private const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
39 /** @var SessionManager */
40 protected $manager;
42 /** @var Config */
43 protected $config;
45 /** @var SessionProvider */
46 protected $provider;
48 /** @var TestBagOStuff */
49 protected $store;
51 /** @var bool */
52 protected $onSessionMetadataCalled = false;
54 /**
55 * @return HookContainer
57 private function getHookContainer() {
58 // Need a real HookContainer to support modification of $wgHooks in the test
59 return $this->getServiceContainer()->getHookContainer();
62 /**
63 * Returns a non-persistent backend that thinks it has at least one session active
64 * @param User|null $user
65 * @param string|null $id
66 * @return SessionBackend
68 protected function getBackend( ?User $user = null, $id = null ) {
69 if ( !$this->config ) {
70 $this->config = new HashConfig();
71 $this->manager = null;
73 if ( !$this->store ) {
74 $this->store = new TestBagOStuff();
75 $this->manager = null;
78 $logger = new NullLogger();
79 if ( !$this->manager ) {
80 $this->manager = new SessionManager( [
81 'store' => $this->store,
82 'logger' => $logger,
83 'config' => $this->config,
84 ] );
87 $hookContainer = $this->getHookContainer();
89 if ( !$this->provider ) {
90 $this->provider = new DummySessionProvider();
92 $this->initProvider( $this->provider, null, $this->config, $this->manager, $hookContainer );
94 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
95 'provider' => $this->provider,
96 'id' => $id ?: self::SESSIONID,
97 'persisted' => true,
98 'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
99 'idIsSafe' => true,
100 ] );
101 $id = new SessionId( $info->getId() );
103 $backend = new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
104 $priv = TestingAccessWrapper::newFromObject( $backend );
105 $priv->persist = false;
106 $priv->requests = [ 100 => new FauxRequest() ];
107 $priv->requests[100]->setSessionId( $id );
108 $priv->usePhpSessionHandling = false;
110 $manager = TestingAccessWrapper::newFromObject( $this->manager );
111 $manager->allSessionBackends = [ $backend->getId() => $backend ] + $manager->allSessionBackends;
112 $manager->allSessionIds = [ $backend->getId() => $id ] + $manager->allSessionIds;
113 $manager->sessionProviders = [ (string)$this->provider => $this->provider ];
115 return $backend;
118 public function testConstructor() {
119 $username = 'TestConstructor';
120 // Set variables
121 $this->getBackend();
123 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
124 'provider' => $this->provider,
125 'id' => self::SESSIONID,
126 'persisted' => true,
127 'userInfo' => UserInfo::newFromName( $username, false ),
128 'idIsSafe' => true,
129 ] );
130 $id = new SessionId( $info->getId() );
131 $logger = new NullLogger();
132 $hookContainer = $this->getHookContainer();
133 try {
134 new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
135 $this->fail( 'Expected exception not thrown' );
136 } catch ( InvalidArgumentException $ex ) {
137 $this->assertSame(
138 'Refusing to create session for unverified user ' . $info->getUserInfo(),
139 $ex->getMessage()
143 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
144 'id' => self::SESSIONID,
145 'userInfo' => UserInfo::newFromName( $username, true ),
146 'idIsSafe' => true,
147 ] );
148 $id = new SessionId( $info->getId() );
149 try {
150 new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
151 $this->fail( 'Expected exception not thrown' );
152 } catch ( InvalidArgumentException $ex ) {
153 $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
156 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
157 'provider' => $this->provider,
158 'id' => self::SESSIONID,
159 'persisted' => true,
160 'userInfo' => UserInfo::newFromName( $username, true ),
161 'idIsSafe' => true,
162 ] );
163 $id = new SessionId( '!' . $info->getId() );
164 try {
165 new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
166 $this->fail( 'Expected exception not thrown' );
167 } catch ( InvalidArgumentException $ex ) {
168 $this->assertSame(
169 'SessionId and SessionInfo don\'t match',
170 $ex->getMessage()
174 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
175 'provider' => $this->provider,
176 'id' => self::SESSIONID,
177 'persisted' => true,
178 'userInfo' => UserInfo::newFromName( $username, true ),
179 'idIsSafe' => true,
180 ] );
181 $id = new SessionId( $info->getId() );
182 $backend = new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
183 $this->assertSame( self::SESSIONID, $backend->getId() );
184 $this->assertSame( $id, $backend->getSessionId() );
185 $this->assertSame( $this->provider, $backend->getProvider() );
186 $this->assertInstanceOf( User::class, $backend->getUser() );
187 $this->assertSame( $username, $backend->getUser()->getName() );
188 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
189 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
190 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
192 $expire = time() + 100;
193 $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ] );
195 $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
196 'provider' => $this->provider,
197 'id' => self::SESSIONID,
198 'persisted' => true,
199 'forceHTTPS' => true,
200 'metadata' => [ 'foo' ],
201 'idIsSafe' => true,
202 ] );
203 $id = new SessionId( $info->getId() );
204 $backend = new SessionBackend( $id, $info, $this->store, $logger, $hookContainer, 10 );
205 $this->assertSame( self::SESSIONID, $backend->getId() );
206 $this->assertSame( $id, $backend->getSessionId() );
207 $this->assertSame( $this->provider, $backend->getProvider() );
208 $this->assertInstanceOf( User::class, $backend->getUser() );
209 $this->assertTrue( $backend->getUser()->isAnon() );
210 $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
211 $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
212 $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
213 $this->assertSame( $expire, TestingAccessWrapper::newFromObject( $backend )->expires );
214 $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
217 public function testSessionStuff() {
218 $backend = $this->getBackend();
219 $priv = TestingAccessWrapper::newFromObject( $backend );
220 $priv->requests = []; // Remove dummy session
222 $manager = TestingAccessWrapper::newFromObject( $this->manager );
224 $request1 = new FauxRequest();
225 $session1 = $backend->getSession( $request1 );
226 $request2 = new FauxRequest();
227 $session2 = $backend->getSession( $request2 );
229 $this->assertInstanceOf( Session::class, $session1 );
230 $this->assertInstanceOf( Session::class, $session2 );
231 $this->assertCount( 2, $priv->requests );
233 $index = TestingAccessWrapper::newFromObject( $session1 )->index;
235 $this->assertSame( $request1, $backend->getRequest( $index ) );
236 $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
237 $request1->setCookie( 'UserName', 'Example' );
238 $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
240 $session1 = null;
241 $this->assertCount( 1, $priv->requests );
242 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
243 $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
244 try {
245 $backend->getRequest( $index );
246 $this->fail( 'Expected exception not thrown' );
247 } catch ( InvalidArgumentException $ex ) {
248 $this->assertSame( 'Invalid session index', $ex->getMessage() );
250 try {
251 $backend->suggestLoginUsername( $index );
252 $this->fail( 'Expected exception not thrown' );
253 } catch ( InvalidArgumentException $ex ) {
254 $this->assertSame( 'Invalid session index', $ex->getMessage() );
257 $session2 = null;
258 $this->assertSame( [], $priv->requests );
259 $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
260 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
263 public function testSetProviderMetadata() {
264 $backend = $this->getBackend();
265 $priv = TestingAccessWrapper::newFromObject( $backend );
266 $priv->providerMetadata = [ 'dummy' ];
268 try {
269 $backend->setProviderMetadata( 'foo' );
270 $this->fail( 'Expected exception not thrown' );
271 } catch ( InvalidArgumentException $ex ) {
272 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
275 try {
276 $backend->setProviderMetadata( (object)[] );
277 $this->fail( 'Expected exception not thrown' );
278 } catch ( InvalidArgumentException $ex ) {
279 $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
282 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
283 $backend->setProviderMetadata( [ 'dummy' ] );
284 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
286 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
287 $backend->setProviderMetadata( [ 'test' ] );
288 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
289 $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
290 $this->store->deleteSession( self::SESSIONID );
292 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
293 $backend->setProviderMetadata( null );
294 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
295 $this->assertSame( null, $backend->getProviderMetadata() );
296 $this->store->deleteSession( self::SESSIONID );
299 public function testResetId() {
300 $id = session_id();
302 $builder = $this->getMockBuilder( DummySessionProvider::class )
303 ->onlyMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
305 $this->provider = $builder->getMock();
306 $this->provider->method( 'persistsSessionId' )
307 ->willReturn( false );
308 $this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
309 $backend = $this->getBackend( User::newFromName( 'TestResetId' ) );
310 $manager = TestingAccessWrapper::newFromObject( $this->manager );
311 $sessionId = $backend->getSessionId();
312 $backend->resetId();
313 $this->assertSame( self::SESSIONID, $backend->getId() );
314 $this->assertSame( $backend->getId(), $sessionId->getId() );
315 $this->assertSame( $id, session_id() );
316 $this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
318 $this->provider = $builder->getMock();
319 $this->provider->method( 'persistsSessionId' )
320 ->willReturn( true );
321 $backend = $this->getBackend();
322 $this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
323 ->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
324 $manager = TestingAccessWrapper::newFromObject( $this->manager );
325 $sessionId = $backend->getSessionId();
326 $backend->resetId();
327 $this->assertNotEquals( self::SESSIONID, $backend->getId() );
328 $this->assertSame( $backend->getId(), $sessionId->getId() );
329 $this->assertIsArray( $this->store->getSession( $backend->getId() ) );
330 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
331 $this->assertSame( $id, session_id() );
332 $this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
333 $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
334 $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
337 public function testPersist() {
338 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
339 ->onlyMethods( [ 'persistSession' ] )->getMock();
340 $this->provider->expects( $this->once() )->method( 'persistSession' );
341 $backend = $this->getBackend();
342 $this->assertFalse( $backend->isPersistent() );
343 $backend->save(); // This one shouldn't call $provider->persistSession()
345 $backend->persist();
346 $this->assertTrue( $backend->isPersistent() );
348 $this->provider = null;
349 $backend = $this->getBackend();
350 $wrap = TestingAccessWrapper::newFromObject( $backend );
351 $wrap->persist = true;
352 $wrap->expires = 0;
353 $backend->persist();
354 $this->assertNotEquals( 0, $wrap->expires );
357 public function testUnpersist() {
358 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
359 ->onlyMethods( [ 'unpersistSession' ] )->getMock();
360 $this->provider->expects( $this->once() )->method( 'unpersistSession' );
361 $backend = $this->getBackend();
362 $wrap = TestingAccessWrapper::newFromObject( $backend );
363 $wrap->store = new CachedBagOStuff( $this->store );
364 $wrap->persist = true;
365 $wrap->dataDirty = true;
367 $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
368 $this->assertTrue( $backend->isPersistent() );
369 $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
371 $backend->unpersist();
372 $this->assertFalse( $backend->isPersistent() );
373 $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
374 $this->assertNotFalse(
375 $wrap->store->get( $wrap->store->makeKey( 'MWSession', self::SESSIONID ) )
379 public function testRememberUser() {
380 $backend = $this->getBackend();
382 $remembered = $backend->shouldRememberUser();
383 $backend->setRememberUser( !$remembered );
384 $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
385 $backend->setRememberUser( $remembered );
386 $this->assertEquals( $remembered, $backend->shouldRememberUser() );
389 public function testForceHTTPS() {
390 $backend = $this->getBackend();
392 $force = $backend->shouldForceHTTPS();
393 $backend->setForceHTTPS( !$force );
394 $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
395 $backend->setForceHTTPS( $force );
396 $this->assertEquals( $force, $backend->shouldForceHTTPS() );
399 public function testLoggedOutTimestamp() {
400 $backend = $this->getBackend();
402 $backend->setLoggedOutTimestamp( 42 );
403 $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
404 $backend->setLoggedOutTimestamp( '123' );
405 $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
408 public function testSetUser() {
409 $user = static::getTestSysop()->getUser();
411 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
412 ->onlyMethods( [ 'canChangeUser' ] )->getMock();
413 $this->provider->method( 'canChangeUser' )
414 ->willReturn( false );
415 $backend = $this->getBackend();
416 $this->assertFalse( $backend->canSetUser() );
417 try {
418 $backend->setUser( $user );
419 $this->fail( 'Expected exception not thrown' );
420 } catch ( BadMethodCallException $ex ) {
421 $this->assertSame(
422 'Cannot set user on this session; check $session->canSetUser() first',
423 $ex->getMessage()
426 $this->assertNotSame( $user, $backend->getUser() );
428 $this->provider = null;
429 $backend = $this->getBackend();
430 $this->assertTrue( $backend->canSetUser() );
431 $this->assertNotSame( $user, $backend->getUser() );
432 $backend->setUser( $user );
433 $this->assertSame( $user, $backend->getUser() );
436 public function testDirty() {
437 $backend = $this->getBackend();
438 $priv = TestingAccessWrapper::newFromObject( $backend );
439 $priv->dataDirty = false;
440 $backend->dirty();
441 $this->assertTrue( $priv->dataDirty );
444 public function testGetData() {
445 $backend = $this->getBackend();
446 $data = $backend->getData();
447 $this->assertSame( [], $data );
448 $this->assertTrue( TestingAccessWrapper::newFromObject( $backend )->dataDirty );
449 $data['???'] = '!!!';
450 $this->assertSame( [ '???' => '!!!' ], $data );
452 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
453 $this->store->setSessionData( self::SESSIONID, $testData );
454 $backend = $this->getBackend();
455 $this->assertSame( $testData, $backend->getData() );
456 $this->assertFalse( TestingAccessWrapper::newFromObject( $backend )->dataDirty );
459 public function testAddData() {
460 $backend = $this->getBackend();
461 $priv = TestingAccessWrapper::newFromObject( $backend );
463 $priv->data = [ 'foo' => 1 ];
464 $priv->dataDirty = false;
465 $backend->addData( [ 'foo' => 1 ] );
466 $this->assertSame( [ 'foo' => 1 ], $priv->data );
467 $this->assertFalse( $priv->dataDirty );
469 $priv->data = [ 'foo' => 1 ];
470 $priv->dataDirty = false;
471 $backend->addData( [ 'foo' => '1' ] );
472 $this->assertSame( [ 'foo' => '1' ], $priv->data );
473 $this->assertTrue( $priv->dataDirty );
475 $priv->data = [ 'foo' => 1 ];
476 $priv->dataDirty = false;
477 $backend->addData( [ 'bar' => 2 ] );
478 $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data );
479 $this->assertTrue( $priv->dataDirty );
482 public function testDelaySave() {
483 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
484 $backend = $this->getBackend();
485 $priv = TestingAccessWrapper::newFromObject( $backend );
486 $priv->persist = true;
488 // Saves happen normally when no delay is in effect
489 $this->onSessionMetadataCalled = false;
490 $priv->metaDirty = true;
491 $backend->save();
492 $this->assertTrue( $this->onSessionMetadataCalled );
494 $this->onSessionMetadataCalled = false;
495 $priv->metaDirty = true;
496 $priv->autosave();
497 $this->assertTrue( $this->onSessionMetadataCalled );
499 $delay = $backend->delaySave();
501 // Autosave doesn't happen when no delay is in effect
502 $this->onSessionMetadataCalled = false;
503 $priv->metaDirty = true;
504 $priv->autosave();
505 $this->assertFalse( $this->onSessionMetadataCalled );
507 // Save still does happen when no delay is in effect
508 $priv->save();
509 $this->assertTrue( $this->onSessionMetadataCalled );
511 // Save happens when delay is consumed
512 $this->onSessionMetadataCalled = false;
513 $priv->metaDirty = true;
514 ScopedCallback::consume( $delay );
515 $this->assertTrue( $this->onSessionMetadataCalled );
517 // Test multiple delays
518 $delay1 = $backend->delaySave();
519 $delay2 = $backend->delaySave();
520 $delay3 = $backend->delaySave();
521 $this->onSessionMetadataCalled = false;
522 $priv->metaDirty = true;
523 $priv->autosave();
524 $this->assertFalse( $this->onSessionMetadataCalled );
525 ScopedCallback::consume( $delay3 );
526 $this->assertFalse( $this->onSessionMetadataCalled );
527 ScopedCallback::consume( $delay1 );
528 $this->assertFalse( $this->onSessionMetadataCalled );
529 ScopedCallback::consume( $delay2 );
530 $this->assertTrue( $this->onSessionMetadataCalled );
533 public function testSave() {
534 $user = static::getTestSysop()->getUser();
535 $this->store = new TestBagOStuff();
536 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
538 $neverHook = $this->getMockBuilder( __CLASS__ )
539 ->onlyMethods( [ 'onSessionMetadata' ] )->getMock();
540 $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
542 $builder = $this->getMockBuilder( DummySessionProvider::class )
543 ->onlyMethods( [ 'persistSession', 'unpersistSession' ] );
545 $neverProvider = $builder->getMock();
546 $neverProvider->expects( $this->never() )->method( 'persistSession' );
547 $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
549 // Not persistent or dirty
550 $this->provider = $neverProvider;
551 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
552 $this->store->setSessionData( self::SESSIONID, $testData );
553 $backend = $this->getBackend( $user );
554 $this->store->deleteSession( self::SESSIONID );
555 $this->assertFalse( $backend->isPersistent() );
556 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
557 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
558 $backend->save();
559 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
561 // (but does unpersist if forced)
562 $this->provider = $builder->getMock();
563 $this->provider->expects( $this->never() )->method( 'persistSession' );
564 $this->provider->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
565 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
566 $this->store->setSessionData( self::SESSIONID, $testData );
567 $backend = $this->getBackend( $user );
568 $this->store->deleteSession( self::SESSIONID );
569 TestingAccessWrapper::newFromObject( $backend )->persist = false;
570 TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
571 $this->assertFalse( $backend->isPersistent() );
572 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
573 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
574 $backend->save();
575 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
577 // (but not to a WebRequest associated with a different session)
578 $this->provider = $neverProvider;
579 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
580 $this->store->setSessionData( self::SESSIONID, $testData );
581 $backend = $this->getBackend( $user );
582 TestingAccessWrapper::newFromObject( $backend )->requests[100]
583 ->setSessionId( new SessionId( 'x' ) );
584 $this->store->deleteSession( self::SESSIONID );
585 TestingAccessWrapper::newFromObject( $backend )->persist = false;
586 TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
587 $this->assertFalse( $backend->isPersistent() );
588 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
589 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
590 $backend->save();
591 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
593 // Not persistent, but dirty
594 $this->provider = $neverProvider;
595 $this->onSessionMetadataCalled = false;
596 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
597 $this->store->setSessionData( self::SESSIONID, $testData );
598 $backend = $this->getBackend( $user );
599 $this->store->deleteSession( self::SESSIONID );
600 $this->assertFalse( $backend->isPersistent() );
601 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
602 TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
603 $backend->save();
604 $this->assertTrue( $this->onSessionMetadataCalled );
605 $blob = $this->store->getSession( self::SESSIONID );
606 $this->assertIsArray( $blob );
607 $this->assertArrayHasKey( 'metadata', $blob );
608 $metadata = $blob['metadata'];
609 $this->assertIsArray( $metadata );
610 $this->assertArrayHasKey( '???', $metadata );
611 $this->assertSame( '!!!', $metadata['???'] );
612 $this->assertFalse( $this->store->getSessionFromBackend( self::SESSIONID ),
613 'making sure it didn\'t save to backend' );
615 // Persistent, not dirty
616 $this->provider = $neverProvider;
617 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
618 $this->store->setSessionData( self::SESSIONID, $testData );
619 $backend = $this->getBackend( $user );
620 $this->store->deleteSession( self::SESSIONID );
621 TestingAccessWrapper::newFromObject( $backend )->persist = true;
622 $this->assertTrue( $backend->isPersistent() );
623 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
624 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
625 $backend->save();
626 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
628 // (but will persist if forced)
629 $this->provider = $builder->getMock();
630 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
631 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
632 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
633 $this->store->setSessionData( self::SESSIONID, $testData );
634 $backend = $this->getBackend( $user );
635 $this->store->deleteSession( self::SESSIONID );
636 TestingAccessWrapper::newFromObject( $backend )->persist = true;
637 TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
638 $this->assertTrue( $backend->isPersistent() );
639 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
640 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
641 $backend->save();
642 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
644 // Persistent and dirty
645 $this->provider = $neverProvider;
646 $this->onSessionMetadataCalled = false;
647 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
648 $this->store->setSessionData( self::SESSIONID, $testData );
649 $backend = $this->getBackend( $user );
650 $this->store->deleteSession( self::SESSIONID );
651 TestingAccessWrapper::newFromObject( $backend )->persist = true;
652 $this->assertTrue( $backend->isPersistent() );
653 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
654 TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
655 $backend->save();
656 $this->assertTrue( $this->onSessionMetadataCalled );
657 $blob = $this->store->getSession( self::SESSIONID );
658 $this->assertIsArray( $blob );
659 $this->assertArrayHasKey( 'metadata', $blob );
660 $metadata = $blob['metadata'];
661 $this->assertIsArray( $metadata );
662 $this->assertArrayHasKey( '???', $metadata );
663 $this->assertSame( '!!!', $metadata['???'] );
664 $this->assertIsArray( $this->store->getSessionFromBackend( self::SESSIONID ),
665 'making sure it did save to backend' );
667 // (also persists if forced)
668 $this->provider = $builder->getMock();
669 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
670 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
671 $this->onSessionMetadataCalled = false;
672 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
673 $this->store->setSessionData( self::SESSIONID, $testData );
674 $backend = $this->getBackend( $user );
675 $this->store->deleteSession( self::SESSIONID );
676 TestingAccessWrapper::newFromObject( $backend )->persist = true;
677 TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
678 $this->assertTrue( $backend->isPersistent() );
679 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
680 TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
681 $backend->save();
682 $this->assertTrue( $this->onSessionMetadataCalled );
683 $blob = $this->store->getSession( self::SESSIONID );
684 $this->assertIsArray( $blob );
685 $this->assertArrayHasKey( 'metadata', $blob );
686 $metadata = $blob['metadata'];
687 $this->assertIsArray( $metadata );
688 $this->assertArrayHasKey( '???', $metadata );
689 $this->assertSame( '!!!', $metadata['???'] );
690 $this->assertIsArray( $this->store->getSessionFromBackend( self::SESSIONID ),
691 'making sure it did save to backend' );
693 // (also persists if metadata dirty)
694 $this->provider = $builder->getMock();
695 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
696 $this->provider->expects( $this->never() )->method( 'unpersistSession' );
697 $this->onSessionMetadataCalled = false;
698 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
699 $this->store->setSessionData( self::SESSIONID, $testData );
700 $backend = $this->getBackend( $user );
701 $this->store->deleteSession( self::SESSIONID );
702 TestingAccessWrapper::newFromObject( $backend )->persist = true;
703 $this->assertTrue( $backend->isPersistent() );
704 TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
705 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
706 $backend->save();
707 $this->assertTrue( $this->onSessionMetadataCalled );
708 $blob = $this->store->getSession( self::SESSIONID );
709 $this->assertIsArray( $blob );
710 $this->assertArrayHasKey( 'metadata', $blob );
711 $metadata = $blob['metadata'];
712 $this->assertIsArray( $metadata );
713 $this->assertArrayHasKey( '???', $metadata );
714 $this->assertSame( '!!!', $metadata['???'] );
715 $this->assertIsArray( $this->store->getSessionFromBackend( self::SESSIONID ),
716 'making sure it did save to backend' );
718 // Not marked dirty, but dirty data
719 // (e.g. indirect modification from ArrayAccess::offsetGet)
720 $this->provider = $neverProvider;
721 $this->onSessionMetadataCalled = false;
722 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
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() );
728 TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
729 TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
730 TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
731 $backend->save();
732 $this->assertTrue( $this->onSessionMetadataCalled );
733 $blob = $this->store->getSession( self::SESSIONID );
734 $this->assertIsArray( $blob );
735 $this->assertArrayHasKey( 'metadata', $blob );
736 $metadata = $blob['metadata'];
737 $this->assertIsArray( $metadata );
738 $this->assertArrayHasKey( '???', $metadata );
739 $this->assertSame( '!!!', $metadata['???'] );
740 $this->assertIsArray( $this->store->getSessionFromBackend( self::SESSIONID ),
741 'making sure it did save to backend' );
743 // Bad hook
744 $this->provider = null;
745 $mockHook = $this->getMockBuilder( __CLASS__ )
746 ->onlyMethods( [ 'onSessionMetadata' ] )->getMock();
747 $mockHook->method( 'onSessionMetadata' )
748 ->willReturnCallback(
749 static function ( SessionBackend $backend, array &$metadata, array $requests ) {
750 $metadata['userId']++;
753 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
754 $this->store->setSessionData( self::SESSIONID, $testData );
755 $backend = $this->getBackend( $user );
756 $backend->dirty();
757 try {
758 $backend->save();
759 $this->fail( 'Expected exception not thrown' );
760 } catch ( UnexpectedValueException $ex ) {
761 $this->assertSame(
762 'SessionMetadata hook changed metadata key "userId"',
763 $ex->getMessage()
767 // SessionManager::preventSessionsForUser
768 TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = [
769 $user->getName() => true,
771 $this->provider = $neverProvider;
772 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
773 $this->store->setSessionData( self::SESSIONID, $testData );
774 $backend = $this->getBackend( $user );
775 $this->store->deleteSession( self::SESSIONID );
776 TestingAccessWrapper::newFromObject( $backend )->persist = true;
777 $this->assertTrue( $backend->isPersistent() );
778 TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
779 TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
780 $backend->save();
781 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
784 public function testRenew() {
785 $user = static::getTestSysop()->getUser();
786 $this->store = new TestBagOStuff();
787 $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
789 // Not persistent
790 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
791 ->onlyMethods( [ 'persistSession' ] )->getMock();
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() );
800 $wrap->metaDirty = false;
801 $wrap->dataDirty = false;
802 $wrap->forcePersist = false;
803 $wrap->expires = 0;
804 $backend->renew();
805 $this->assertTrue( $this->onSessionMetadataCalled );
806 $blob = $this->store->getSession( self::SESSIONID );
807 $this->assertIsArray( $blob );
808 $this->assertArrayHasKey( 'metadata', $blob );
809 $metadata = $blob['metadata'];
810 $this->assertIsArray( $metadata );
811 $this->assertArrayHasKey( '???', $metadata );
812 $this->assertSame( '!!!', $metadata['???'] );
813 $this->assertNotEquals( 0, $wrap->expires );
815 // Persistent
816 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
817 ->onlyMethods( [ 'persistSession' ] )->getMock();
818 $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
819 $this->onSessionMetadataCalled = false;
820 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
821 $this->store->setSessionData( self::SESSIONID, $testData );
822 $backend = $this->getBackend( $user );
823 $this->store->deleteSession( self::SESSIONID );
824 $wrap = TestingAccessWrapper::newFromObject( $backend );
825 $wrap->persist = true;
826 $this->assertTrue( $backend->isPersistent() );
827 $wrap->metaDirty = false;
828 $wrap->dataDirty = false;
829 $wrap->forcePersist = false;
830 $wrap->expires = 0;
831 $backend->renew();
832 $this->assertTrue( $this->onSessionMetadataCalled );
833 $blob = $this->store->getSession( self::SESSIONID );
834 $this->assertIsArray( $blob );
835 $this->assertArrayHasKey( 'metadata', $blob );
836 $metadata = $blob['metadata'];
837 $this->assertIsArray( $metadata );
838 $this->assertArrayHasKey( '???', $metadata );
839 $this->assertSame( '!!!', $metadata['???'] );
840 $this->assertNotEquals( 0, $wrap->expires );
842 // Not persistent, not expiring
843 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
844 ->onlyMethods( [ 'persistSession' ] )->getMock();
845 $this->provider->expects( $this->never() )->method( 'persistSession' );
846 $this->onSessionMetadataCalled = false;
847 $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
848 $this->store->setSessionData( self::SESSIONID, $testData );
849 $backend = $this->getBackend( $user );
850 $this->store->deleteSession( self::SESSIONID );
851 $wrap = TestingAccessWrapper::newFromObject( $backend );
852 $this->assertFalse( $backend->isPersistent() );
853 $wrap->metaDirty = false;
854 $wrap->dataDirty = false;
855 $wrap->forcePersist = false;
856 $expires = time() + $wrap->lifetime + 100;
857 $wrap->expires = $expires;
858 $backend->renew();
859 $this->assertFalse( $this->onSessionMetadataCalled );
860 $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
861 $this->assertEquals( $expires, $wrap->expires );
864 public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
865 $this->onSessionMetadataCalled = true;
866 $metadata['???'] = '!!!';
869 public function testTakeOverGlobalSession() {
870 if ( !PHPSessionHandler::isInstalled() ) {
871 PHPSessionHandler::install( SessionManager::singleton() );
873 if ( !PHPSessionHandler::isEnabled() ) {
874 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
875 $handler = TestingAccessWrapper::newFromObject( $staticAccess->instance );
876 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
877 session_write_close();
878 $handler->enable = false;
879 } );
880 $handler->enable = true;
883 $backend = $this->getBackend( static::getTestSysop()->getUser() );
884 TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
886 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
888 $manager = TestingAccessWrapper::newFromObject( $this->manager );
889 $request = RequestContext::getMain()->getRequest();
890 $manager->globalSession = $backend->getSession( $request );
891 $manager->globalSessionRequest = $request;
893 session_id( '' );
894 TestingAccessWrapper::newFromObject( $backend )->checkPHPSession();
895 $this->assertSame( $backend->getId(), session_id() );
896 session_write_close();
898 $backend2 = $this->getBackend(
899 User::newFromName( 'TestTakeOverGlobalSession' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
901 TestingAccessWrapper::newFromObject( $backend2 )->usePhpSessionHandling = true;
903 session_id( '' );
904 TestingAccessWrapper::newFromObject( $backend2 )->checkPHPSession();
905 $this->assertSame( '', session_id() );
908 public function testResetIdOfGlobalSession() {
909 if ( !PHPSessionHandler::isInstalled() ) {
910 PHPSessionHandler::install( SessionManager::singleton() );
912 if ( !PHPSessionHandler::isEnabled() ) {
913 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
914 $handler = TestingAccessWrapper::newFromObject( $staticAccess->instance );
915 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
916 session_write_close();
917 $handler->enable = false;
918 } );
919 $handler->enable = true;
922 $backend = $this->getBackend( User::newFromName( 'TestResetIdOfGlobalSession' ) );
923 TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
925 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
927 $manager = TestingAccessWrapper::newFromObject( $this->manager );
928 $request = RequestContext::getMain()->getRequest();
929 $manager->globalSession = $backend->getSession( $request );
930 $manager->globalSessionRequest = $request;
932 session_id( self::SESSIONID );
933 @session_start();
934 $_SESSION['foo'] = __METHOD__;
935 $backend->resetId();
936 $this->assertNotEquals( self::SESSIONID, $backend->getId() );
937 $this->assertSame( $backend->getId(), session_id() );
938 $this->assertArrayHasKey( 'foo', $_SESSION );
939 $this->assertSame( __METHOD__, $_SESSION['foo'] );
940 session_write_close();
943 public function testUnpersistOfGlobalSession() {
944 if ( !PHPSessionHandler::isInstalled() ) {
945 PHPSessionHandler::install( SessionManager::singleton() );
947 if ( !PHPSessionHandler::isEnabled() ) {
948 $staticAccess = TestingAccessWrapper::newFromClass( PHPSessionHandler::class );
949 $handler = TestingAccessWrapper::newFromObject( $staticAccess->instance );
950 $resetHandler = new ScopedCallback( static function () use ( $handler ) {
951 session_write_close();
952 $handler->enable = false;
953 } );
954 $handler->enable = true;
957 $backend = $this->getBackend( User::newFromName( 'TestUnpersistOfGlobalSession' ) );
958 $wrap = TestingAccessWrapper::newFromObject( $backend );
959 $wrap->usePhpSessionHandling = true;
960 $wrap->persist = true;
962 $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
964 $manager = TestingAccessWrapper::newFromObject( $this->manager );
965 $request = RequestContext::getMain()->getRequest();
966 $manager->globalSession = $backend->getSession( $request );
967 $manager->globalSessionRequest = $request;
969 session_id( self::SESSIONID . 'x' );
970 @session_start();
971 $backend->unpersist();
972 $this->assertSame( self::SESSIONID . 'x', session_id() );
973 session_write_close();
975 session_id( self::SESSIONID );
976 $wrap->persist = true;
977 $backend->unpersist();
978 $this->assertSame( '', session_id() );
981 public function testGetAllowedUserRights() {
982 $this->provider = $this->getMockBuilder( DummySessionProvider::class )
983 ->onlyMethods( [ 'getAllowedUserRights' ] )
984 ->getMock();
985 $this->provider->method( 'getAllowedUserRights' )
986 ->willReturn( [ 'foo', 'bar' ] );
988 $backend = $this->getBackend();
989 $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );