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