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