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