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