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