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