MediaWiki  1.28.0
SessionBackendTest.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Session;
4 
6 use User;
7 
14  const SESSIONID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
15 
16  protected $manager;
17  protected $config;
18  protected $provider;
19  protected $store;
20 
21  protected $onSessionMetadataCalled = false;
22 
28  protected function getBackend( User $user = null, $id = null ) {
29  if ( !$this->config ) {
30  $this->config = new \HashConfig();
31  $this->manager = null;
32  }
33  if ( !$this->store ) {
34  $this->store = new TestBagOStuff();
35  $this->manager = null;
36  }
37 
38  $logger = new \Psr\Log\NullLogger();
39  if ( !$this->manager ) {
40  $this->manager = new SessionManager( [
41  'store' => $this->store,
42  'logger' => $logger,
43  'config' => $this->config,
44  ] );
45  }
46 
47  if ( !$this->provider ) {
48  $this->provider = new \DummySessionProvider();
49  }
50  $this->provider->setLogger( $logger );
51  $this->provider->setConfig( $this->config );
52  $this->provider->setManager( $this->manager );
53 
55  'provider' => $this->provider,
56  'id' => $id ?: self::SESSIONID,
57  'persisted' => true,
58  'userInfo' => UserInfo::newFromUser( $user ?: new User, true ),
59  'idIsSafe' => true,
60  ] );
61  $id = new SessionId( $info->getId() );
62 
63  $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
64  $priv = \TestingAccessWrapper::newFromObject( $backend );
65  $priv->persist = false;
66  $priv->requests = [ 100 => new \FauxRequest() ];
67  $priv->requests[100]->setSessionId( $id );
68  $priv->usePhpSessionHandling = false;
69 
71  $manager->allSessionBackends = [ $backend->getId() => $backend ] + $manager->allSessionBackends;
72  $manager->allSessionIds = [ $backend->getId() => $id ] + $manager->allSessionIds;
73  $manager->sessionProviders = [ (string)$this->provider => $this->provider ];
74 
75  return $backend;
76  }
77 
78  public function testConstructor() {
79  // Set variables
80  $this->getBackend();
81 
83  'provider' => $this->provider,
84  'id' => self::SESSIONID,
85  'persisted' => true,
86  'userInfo' => UserInfo::newFromName( 'UTSysop', false ),
87  'idIsSafe' => true,
88  ] );
89  $id = new SessionId( $info->getId() );
90  $logger = new \Psr\Log\NullLogger();
91  try {
92  new SessionBackend( $id, $info, $this->store, $logger, 10 );
93  $this->fail( 'Expected exception not thrown' );
94  } catch ( \InvalidArgumentException $ex ) {
95  $this->assertSame(
96  "Refusing to create session for unverified user {$info->getUserInfo()}",
97  $ex->getMessage()
98  );
99  }
100 
101  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
102  'id' => self::SESSIONID,
103  'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
104  'idIsSafe' => true,
105  ] );
106  $id = new SessionId( $info->getId() );
107  try {
108  new SessionBackend( $id, $info, $this->store, $logger, 10 );
109  $this->fail( 'Expected exception not thrown' );
110  } catch ( \InvalidArgumentException $ex ) {
111  $this->assertSame( 'Cannot create session without a provider', $ex->getMessage() );
112  }
113 
114  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
115  'provider' => $this->provider,
116  'id' => self::SESSIONID,
117  'persisted' => true,
118  'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
119  'idIsSafe' => true,
120  ] );
121  $id = new SessionId( '!' . $info->getId() );
122  try {
123  new SessionBackend( $id, $info, $this->store, $logger, 10 );
124  $this->fail( 'Expected exception not thrown' );
125  } catch ( \InvalidArgumentException $ex ) {
126  $this->assertSame(
127  'SessionId and SessionInfo don\'t match',
128  $ex->getMessage()
129  );
130  }
131 
132  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
133  'provider' => $this->provider,
134  'id' => self::SESSIONID,
135  'persisted' => true,
136  'userInfo' => UserInfo::newFromName( 'UTSysop', true ),
137  'idIsSafe' => true,
138  ] );
139  $id = new SessionId( $info->getId() );
140  $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
141  $this->assertSame( self::SESSIONID, $backend->getId() );
142  $this->assertSame( $id, $backend->getSessionId() );
143  $this->assertSame( $this->provider, $backend->getProvider() );
144  $this->assertInstanceOf( 'User', $backend->getUser() );
145  $this->assertSame( 'UTSysop', $backend->getUser()->getName() );
146  $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
147  $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
148  $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
149 
150  $expire = time() + 100;
151  $this->store->setSessionMeta( self::SESSIONID, [ 'expires' => $expire ], 2 );
152 
153  $info = new SessionInfo( SessionInfo::MIN_PRIORITY, [
154  'provider' => $this->provider,
155  'id' => self::SESSIONID,
156  'persisted' => true,
157  'forceHTTPS' => true,
158  'metadata' => [ 'foo' ],
159  'idIsSafe' => true,
160  ] );
161  $id = new SessionId( $info->getId() );
162  $backend = new SessionBackend( $id, $info, $this->store, $logger, 10 );
163  $this->assertSame( self::SESSIONID, $backend->getId() );
164  $this->assertSame( $id, $backend->getSessionId() );
165  $this->assertSame( $this->provider, $backend->getProvider() );
166  $this->assertInstanceOf( 'User', $backend->getUser() );
167  $this->assertTrue( $backend->getUser()->isAnon() );
168  $this->assertSame( $info->wasPersisted(), $backend->isPersistent() );
169  $this->assertSame( $info->wasRemembered(), $backend->shouldRememberUser() );
170  $this->assertSame( $info->forceHTTPS(), $backend->shouldForceHTTPS() );
171  $this->assertSame( $expire, \TestingAccessWrapper::newFromObject( $backend )->expires );
172  $this->assertSame( [ 'foo' ], $backend->getProviderMetadata() );
173  }
174 
175  public function testSessionStuff() {
176  $backend = $this->getBackend();
177  $priv = \TestingAccessWrapper::newFromObject( $backend );
178  $priv->requests = []; // Remove dummy session
179 
181 
182  $request1 = new \FauxRequest();
183  $session1 = $backend->getSession( $request1 );
184  $request2 = new \FauxRequest();
185  $session2 = $backend->getSession( $request2 );
186 
187  $this->assertInstanceOf( Session::class, $session1 );
188  $this->assertInstanceOf( Session::class, $session2 );
189  $this->assertSame( 2, count( $priv->requests ) );
190 
191  $index = \TestingAccessWrapper::newFromObject( $session1 )->index;
192 
193  $this->assertSame( $request1, $backend->getRequest( $index ) );
194  $this->assertSame( null, $backend->suggestLoginUsername( $index ) );
195  $request1->setCookie( 'UserName', 'Example' );
196  $this->assertSame( 'Example', $backend->suggestLoginUsername( $index ) );
197 
198  $session1 = null;
199  $this->assertSame( 1, count( $priv->requests ) );
200  $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
201  $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
202  try {
203  $backend->getRequest( $index );
204  $this->fail( 'Expected exception not thrown' );
205  } catch ( \InvalidArgumentException $ex ) {
206  $this->assertSame( 'Invalid session index', $ex->getMessage() );
207  }
208  try {
209  $backend->suggestLoginUsername( $index );
210  $this->fail( 'Expected exception not thrown' );
211  } catch ( \InvalidArgumentException $ex ) {
212  $this->assertSame( 'Invalid session index', $ex->getMessage() );
213  }
214 
215  $session2 = null;
216  $this->assertSame( 0, count( $priv->requests ) );
217  $this->assertArrayNotHasKey( $backend->getId(), $manager->allSessionBackends );
218  $this->assertArrayHasKey( $backend->getId(), $manager->allSessionIds );
219  }
220 
221  public function testSetProviderMetadata() {
222  $backend = $this->getBackend();
223  $priv = \TestingAccessWrapper::newFromObject( $backend );
224  $priv->providerMetadata = [ 'dummy' ];
225 
226  try {
227  $backend->setProviderMetadata( 'foo' );
228  $this->fail( 'Expected exception not thrown' );
229  } catch ( \InvalidArgumentException $ex ) {
230  $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
231  }
232 
233  try {
234  $backend->setProviderMetadata( (object)[] );
235  $this->fail( 'Expected exception not thrown' );
236  } catch ( \InvalidArgumentException $ex ) {
237  $this->assertSame( '$metadata must be an array or null', $ex->getMessage() );
238  }
239 
240  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
241  $backend->setProviderMetadata( [ 'dummy' ] );
242  $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
243 
244  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
245  $backend->setProviderMetadata( [ 'test' ] );
246  $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
247  $this->assertSame( [ 'test' ], $backend->getProviderMetadata() );
248  $this->store->deleteSession( self::SESSIONID );
249 
250  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
251  $backend->setProviderMetadata( null );
252  $this->assertNotFalse( $this->store->getSession( self::SESSIONID ) );
253  $this->assertSame( null, $backend->getProviderMetadata() );
254  $this->store->deleteSession( self::SESSIONID );
255  }
256 
257  public function testResetId() {
258  $id = session_id();
259 
260  $builder = $this->getMockBuilder( 'DummySessionProvider' )
261  ->setMethods( [ 'persistsSessionId', 'sessionIdWasReset' ] );
262 
263  $this->provider = $builder->getMock();
264  $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
265  ->will( $this->returnValue( false ) );
266  $this->provider->expects( $this->never() )->method( 'sessionIdWasReset' );
267  $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
269  $sessionId = $backend->getSessionId();
270  $backend->resetId();
271  $this->assertSame( self::SESSIONID, $backend->getId() );
272  $this->assertSame( $backend->getId(), $sessionId->getId() );
273  $this->assertSame( $id, session_id() );
274  $this->assertSame( $backend, $manager->allSessionBackends[self::SESSIONID] );
275 
276  $this->provider = $builder->getMock();
277  $this->provider->expects( $this->any() )->method( 'persistsSessionId' )
278  ->will( $this->returnValue( true ) );
279  $backend = $this->getBackend();
280  $this->provider->expects( $this->once() )->method( 'sessionIdWasReset' )
281  ->with( $this->identicalTo( $backend ), $this->identicalTo( self::SESSIONID ) );
283  $sessionId = $backend->getSessionId();
284  $backend->resetId();
285  $this->assertNotEquals( self::SESSIONID, $backend->getId() );
286  $this->assertSame( $backend->getId(), $sessionId->getId() );
287  $this->assertInternalType( 'array', $this->store->getSession( $backend->getId() ) );
288  $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
289  $this->assertSame( $id, session_id() );
290  $this->assertArrayNotHasKey( self::SESSIONID, $manager->allSessionBackends );
291  $this->assertArrayHasKey( $backend->getId(), $manager->allSessionBackends );
292  $this->assertSame( $backend, $manager->allSessionBackends[$backend->getId()] );
293  }
294 
295  public function testPersist() {
296  $this->provider = $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
297  $this->provider->expects( $this->once() )->method( 'persistSession' );
298  $backend = $this->getBackend();
299  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
300  $backend->save(); // This one shouldn't call $provider->persistSession()
301 
302  $backend->persist();
303  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
304 
305  $this->provider = null;
306  $backend = $this->getBackend();
307  $wrap = \TestingAccessWrapper::newFromObject( $backend );
308  $wrap->persist = true;
309  $wrap->expires = 0;
310  $backend->persist();
311  $this->assertNotEquals( 0, $wrap->expires );
312  }
313 
314  public function testUnpersist() {
315  $this->provider = $this->getMock( 'DummySessionProvider', [ 'unpersistSession' ] );
316  $this->provider->expects( $this->once() )->method( 'unpersistSession' );
317  $backend = $this->getBackend();
318  $wrap = \TestingAccessWrapper::newFromObject( $backend );
319  $wrap->store = new \CachedBagOStuff( $this->store );
320  $wrap->persist = true;
321  $wrap->dataDirty = true;
322 
323  $backend->save(); // This one shouldn't call $provider->persistSession(), but should save
324  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
325  $this->assertNotFalse( $this->store->getSession( self::SESSIONID ), 'sanity check' );
326 
327  $backend->unpersist();
328  $this->assertFalse( $backend->isPersistent() );
329  $this->assertFalse( $this->store->getSession( self::SESSIONID ) );
330  $this->assertNotFalse( $wrap->store->get( wfMemcKey( 'MWSession', self::SESSIONID ) ) );
331  }
332 
333  public function testRememberUser() {
334  $backend = $this->getBackend();
335 
336  $remembered = $backend->shouldRememberUser();
337  $backend->setRememberUser( !$remembered );
338  $this->assertNotEquals( $remembered, $backend->shouldRememberUser() );
339  $backend->setRememberUser( $remembered );
340  $this->assertEquals( $remembered, $backend->shouldRememberUser() );
341  }
342 
343  public function testForceHTTPS() {
344  $backend = $this->getBackend();
345 
346  $force = $backend->shouldForceHTTPS();
347  $backend->setForceHTTPS( !$force );
348  $this->assertNotEquals( $force, $backend->shouldForceHTTPS() );
349  $backend->setForceHTTPS( $force );
350  $this->assertEquals( $force, $backend->shouldForceHTTPS() );
351  }
352 
353  public function testLoggedOutTimestamp() {
354  $backend = $this->getBackend();
355 
356  $backend->setLoggedOutTimestamp( 42 );
357  $this->assertSame( 42, $backend->getLoggedOutTimestamp() );
358  $backend->setLoggedOutTimestamp( '123' );
359  $this->assertSame( 123, $backend->getLoggedOutTimestamp() );
360  }
361 
362  public function testSetUser() {
363  $user = static::getTestSysop()->getUser();
364 
365  $this->provider = $this->getMock( 'DummySessionProvider', [ 'canChangeUser' ] );
366  $this->provider->expects( $this->any() )->method( 'canChangeUser' )
367  ->will( $this->returnValue( false ) );
368  $backend = $this->getBackend();
369  $this->assertFalse( $backend->canSetUser() );
370  try {
371  $backend->setUser( $user );
372  $this->fail( 'Expected exception not thrown' );
373  } catch ( \BadMethodCallException $ex ) {
374  $this->assertSame(
375  'Cannot set user on this session; check $session->canSetUser() first',
376  $ex->getMessage()
377  );
378  }
379  $this->assertNotSame( $user, $backend->getUser() );
380 
381  $this->provider = null;
382  $backend = $this->getBackend();
383  $this->assertTrue( $backend->canSetUser() );
384  $this->assertNotSame( $user, $backend->getUser(), 'sanity check' );
385  $backend->setUser( $user );
386  $this->assertSame( $user, $backend->getUser() );
387  }
388 
389  public function testDirty() {
390  $backend = $this->getBackend();
391  $priv = \TestingAccessWrapper::newFromObject( $backend );
392  $priv->dataDirty = false;
393  $backend->dirty();
394  $this->assertTrue( $priv->dataDirty );
395  }
396 
397  public function testGetData() {
398  $backend = $this->getBackend();
399  $data = $backend->getData();
400  $this->assertSame( [], $data );
401  $this->assertTrue( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
402  $data['???'] = '!!!';
403  $this->assertSame( [ '???' => '!!!' ], $data );
404 
405  $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
406  $this->store->setSessionData( self::SESSIONID, $testData );
407  $backend = $this->getBackend();
408  $this->assertSame( $testData, $backend->getData() );
409  $this->assertFalse( \TestingAccessWrapper::newFromObject( $backend )->dataDirty );
410  }
411 
412  public function testAddData() {
413  $backend = $this->getBackend();
414  $priv = \TestingAccessWrapper::newFromObject( $backend );
415 
416  $priv->data = [ 'foo' => 1 ];
417  $priv->dataDirty = false;
418  $backend->addData( [ 'foo' => 1 ] );
419  $this->assertSame( [ 'foo' => 1 ], $priv->data );
420  $this->assertFalse( $priv->dataDirty );
421 
422  $priv->data = [ 'foo' => 1 ];
423  $priv->dataDirty = false;
424  $backend->addData( [ 'foo' => '1' ] );
425  $this->assertSame( [ 'foo' => '1' ], $priv->data );
426  $this->assertTrue( $priv->dataDirty );
427 
428  $priv->data = [ 'foo' => 1 ];
429  $priv->dataDirty = false;
430  $backend->addData( [ 'bar' => 2 ] );
431  $this->assertSame( [ 'foo' => 1, 'bar' => 2 ], $priv->data );
432  $this->assertTrue( $priv->dataDirty );
433  }
434 
435  public function testDelaySave() {
436  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
437  $backend = $this->getBackend();
438  $priv = \TestingAccessWrapper::newFromObject( $backend );
439  $priv->persist = true;
440 
441  // Saves happen normally when no delay is in effect
442  $this->onSessionMetadataCalled = false;
443  $priv->metaDirty = true;
444  $backend->save();
445  $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
446 
447  $this->onSessionMetadataCalled = false;
448  $priv->metaDirty = true;
449  $priv->autosave();
450  $this->assertTrue( $this->onSessionMetadataCalled, 'sanity check' );
451 
452  $delay = $backend->delaySave();
453 
454  // Autosave doesn't happen when no delay is in effect
455  $this->onSessionMetadataCalled = false;
456  $priv->metaDirty = true;
457  $priv->autosave();
458  $this->assertFalse( $this->onSessionMetadataCalled );
459 
460  // Save still does happen when no delay is in effect
461  $priv->save();
462  $this->assertTrue( $this->onSessionMetadataCalled );
463 
464  // Save happens when delay is consumed
465  $this->onSessionMetadataCalled = false;
466  $priv->metaDirty = true;
467  \Wikimedia\ScopedCallback::consume( $delay );
468  $this->assertTrue( $this->onSessionMetadataCalled );
469 
470  // Test multiple delays
471  $delay1 = $backend->delaySave();
472  $delay2 = $backend->delaySave();
473  $delay3 = $backend->delaySave();
474  $this->onSessionMetadataCalled = false;
475  $priv->metaDirty = true;
476  $priv->autosave();
477  $this->assertFalse( $this->onSessionMetadataCalled );
478  \Wikimedia\ScopedCallback::consume( $delay3 );
479  $this->assertFalse( $this->onSessionMetadataCalled );
480  \Wikimedia\ScopedCallback::consume( $delay1 );
481  $this->assertFalse( $this->onSessionMetadataCalled );
482  \Wikimedia\ScopedCallback::consume( $delay2 );
483  $this->assertTrue( $this->onSessionMetadataCalled );
484  }
485 
486  public function testSave() {
487  $user = static::getTestSysop()->getUser();
488  $this->store = new TestBagOStuff();
489  $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
490 
491  $neverHook = $this->getMock( __CLASS__, [ 'onSessionMetadata' ] );
492  $neverHook->expects( $this->never() )->method( 'onSessionMetadata' );
493 
494  $builder = $this->getMockBuilder( 'DummySessionProvider' )
495  ->setMethods( [ 'persistSession', 'unpersistSession' ] );
496 
497  $neverProvider = $builder->getMock();
498  $neverProvider->expects( $this->never() )->method( 'persistSession' );
499  $neverProvider->expects( $this->never() )->method( 'unpersistSession' );
500 
501  // Not persistent or dirty
502  $this->provider = $neverProvider;
503  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
504  $this->store->setSessionData( self::SESSIONID, $testData );
505  $backend = $this->getBackend( $user );
506  $this->store->deleteSession( self::SESSIONID );
507  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
508  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
509  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
510  $backend->save();
511  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
512 
513  // (but does unpersist if forced)
514  $this->provider = $builder->getMock();
515  $this->provider->expects( $this->never() )->method( 'persistSession' );
516  $this->provider->expects( $this->atLeastOnce() )->method( 'unpersistSession' );
517  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
518  $this->store->setSessionData( self::SESSIONID, $testData );
519  $backend = $this->getBackend( $user );
520  $this->store->deleteSession( self::SESSIONID );
521  \TestingAccessWrapper::newFromObject( $backend )->persist = false;
522  \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
523  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
524  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
525  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
526  $backend->save();
527  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
528 
529  // (but not to a WebRequest associated with a different session)
530  $this->provider = $neverProvider;
531  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
532  $this->store->setSessionData( self::SESSIONID, $testData );
533  $backend = $this->getBackend( $user );
534  \TestingAccessWrapper::newFromObject( $backend )->requests[100]
535  ->setSessionId( new SessionId( 'x' ) );
536  $this->store->deleteSession( self::SESSIONID );
537  \TestingAccessWrapper::newFromObject( $backend )->persist = false;
538  \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
539  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
540  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
541  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
542  $backend->save();
543  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
544 
545  // Not persistent, but dirty
546  $this->provider = $neverProvider;
547  $this->onSessionMetadataCalled = false;
548  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
549  $this->store->setSessionData( self::SESSIONID, $testData );
550  $backend = $this->getBackend( $user );
551  $this->store->deleteSession( self::SESSIONID );
552  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
553  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
554  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
555  $backend->save();
556  $this->assertTrue( $this->onSessionMetadataCalled );
557  $blob = $this->store->getSession( self::SESSIONID );
558  $this->assertInternalType( 'array', $blob );
559  $this->assertArrayHasKey( 'metadata', $blob );
560  $metadata = $blob['metadata'];
561  $this->assertInternalType( 'array', $metadata );
562  $this->assertArrayHasKey( '???', $metadata );
563  $this->assertSame( '!!!', $metadata['???'] );
564  $this->assertFalse( $this->store->getSessionFromBackend( self::SESSIONID ),
565  'making sure it didn\'t save to backend' );
566 
567  // Persistent, not dirty
568  $this->provider = $neverProvider;
569  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
570  $this->store->setSessionData( self::SESSIONID, $testData );
571  $backend = $this->getBackend( $user );
572  $this->store->deleteSession( self::SESSIONID );
573  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
574  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
575  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
576  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
577  $backend->save();
578  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
579 
580  // (but will persist if forced)
581  $this->provider = $builder->getMock();
582  $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
583  $this->provider->expects( $this->never() )->method( 'unpersistSession' );
584  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
585  $this->store->setSessionData( self::SESSIONID, $testData );
586  $backend = $this->getBackend( $user );
587  $this->store->deleteSession( self::SESSIONID );
588  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
589  \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
590  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
591  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
592  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
593  $backend->save();
594  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
595 
596  // Persistent and dirty
597  $this->provider = $neverProvider;
598  $this->onSessionMetadataCalled = false;
599  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
600  $this->store->setSessionData( self::SESSIONID, $testData );
601  $backend = $this->getBackend( $user );
602  $this->store->deleteSession( self::SESSIONID );
603  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
604  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
605  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
606  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
607  $backend->save();
608  $this->assertTrue( $this->onSessionMetadataCalled );
609  $blob = $this->store->getSession( self::SESSIONID );
610  $this->assertInternalType( 'array', $blob );
611  $this->assertArrayHasKey( 'metadata', $blob );
612  $metadata = $blob['metadata'];
613  $this->assertInternalType( 'array', $metadata );
614  $this->assertArrayHasKey( '???', $metadata );
615  $this->assertSame( '!!!', $metadata['???'] );
616  $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
617  'making sure it did save to backend' );
618 
619  // (also persists if forced)
620  $this->provider = $builder->getMock();
621  $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
622  $this->provider->expects( $this->never() )->method( 'unpersistSession' );
623  $this->onSessionMetadataCalled = false;
624  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
625  $this->store->setSessionData( self::SESSIONID, $testData );
626  $backend = $this->getBackend( $user );
627  $this->store->deleteSession( self::SESSIONID );
628  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
629  \TestingAccessWrapper::newFromObject( $backend )->forcePersist = true;
630  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
631  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
632  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
633  $backend->save();
634  $this->assertTrue( $this->onSessionMetadataCalled );
635  $blob = $this->store->getSession( self::SESSIONID );
636  $this->assertInternalType( 'array', $blob );
637  $this->assertArrayHasKey( 'metadata', $blob );
638  $metadata = $blob['metadata'];
639  $this->assertInternalType( 'array', $metadata );
640  $this->assertArrayHasKey( '???', $metadata );
641  $this->assertSame( '!!!', $metadata['???'] );
642  $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
643  'making sure it did save to backend' );
644 
645  // (also persists if metadata dirty)
646  $this->provider = $builder->getMock();
647  $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
648  $this->provider->expects( $this->never() )->method( 'unpersistSession' );
649  $this->onSessionMetadataCalled = false;
650  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
651  $this->store->setSessionData( self::SESSIONID, $testData );
652  $backend = $this->getBackend( $user );
653  $this->store->deleteSession( self::SESSIONID );
654  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
655  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
656  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
657  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
658  $backend->save();
659  $this->assertTrue( $this->onSessionMetadataCalled );
660  $blob = $this->store->getSession( self::SESSIONID );
661  $this->assertInternalType( 'array', $blob );
662  $this->assertArrayHasKey( 'metadata', $blob );
663  $metadata = $blob['metadata'];
664  $this->assertInternalType( 'array', $metadata );
665  $this->assertArrayHasKey( '???', $metadata );
666  $this->assertSame( '!!!', $metadata['???'] );
667  $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
668  'making sure it did save to backend' );
669 
670  // Not marked dirty, but dirty data
671  // (e.g. indirect modification from ArrayAccess::offsetGet)
672  $this->provider = $neverProvider;
673  $this->onSessionMetadataCalled = false;
674  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
675  $this->store->setSessionData( self::SESSIONID, $testData );
676  $backend = $this->getBackend( $user );
677  $this->store->deleteSession( self::SESSIONID );
678  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
679  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
680  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = false;
681  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = false;
682  \TestingAccessWrapper::newFromObject( $backend )->dataHash = 'Doesn\'t match';
683  $backend->save();
684  $this->assertTrue( $this->onSessionMetadataCalled );
685  $blob = $this->store->getSession( self::SESSIONID );
686  $this->assertInternalType( 'array', $blob );
687  $this->assertArrayHasKey( 'metadata', $blob );
688  $metadata = $blob['metadata'];
689  $this->assertInternalType( 'array', $metadata );
690  $this->assertArrayHasKey( '???', $metadata );
691  $this->assertSame( '!!!', $metadata['???'] );
692  $this->assertNotSame( false, $this->store->getSessionFromBackend( self::SESSIONID ),
693  'making sure it did save to backend' );
694 
695  // Bad hook
696  $this->provider = null;
697  $mockHook = $this->getMock( __CLASS__, [ 'onSessionMetadata' ] );
698  $mockHook->expects( $this->any() )->method( 'onSessionMetadata' )
699  ->will( $this->returnCallback(
700  function ( SessionBackend $backend, array &$metadata, array $requests ) {
701  $metadata['userId']++;
702  }
703  ) );
704  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $mockHook ] ] );
705  $this->store->setSessionData( self::SESSIONID, $testData );
706  $backend = $this->getBackend( $user );
707  $backend->dirty();
708  try {
709  $backend->save();
710  $this->fail( 'Expected exception not thrown' );
711  } catch ( \UnexpectedValueException $ex ) {
712  $this->assertSame(
713  'SessionMetadata hook changed metadata key "userId"',
714  $ex->getMessage()
715  );
716  }
717 
718  // SessionManager::preventSessionsForUser
719  \TestingAccessWrapper::newFromObject( $this->manager )->preventUsers = [
720  $user->getName() => true,
721  ];
722  $this->provider = $neverProvider;
723  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $neverHook ] ] );
724  $this->store->setSessionData( self::SESSIONID, $testData );
725  $backend = $this->getBackend( $user );
726  $this->store->deleteSession( self::SESSIONID );
727  \TestingAccessWrapper::newFromObject( $backend )->persist = true;
728  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
729  \TestingAccessWrapper::newFromObject( $backend )->metaDirty = true;
730  \TestingAccessWrapper::newFromObject( $backend )->dataDirty = true;
731  $backend->save();
732  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
733  }
734 
735  public function testRenew() {
736  $user = static::getTestSysop()->getUser();
737  $this->store = new TestBagOStuff();
738  $testData = [ 'foo' => 'foo!', 'bar', [ 'baz', null ] ];
739 
740  // Not persistent
741  $this->provider = $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
742  $this->provider->expects( $this->never() )->method( 'persistSession' );
743  $this->onSessionMetadataCalled = false;
744  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
745  $this->store->setSessionData( self::SESSIONID, $testData );
746  $backend = $this->getBackend( $user );
747  $this->store->deleteSession( self::SESSIONID );
748  $wrap = \TestingAccessWrapper::newFromObject( $backend );
749  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
750  $wrap->metaDirty = false;
751  $wrap->dataDirty = false;
752  $wrap->forcePersist = false;
753  $wrap->expires = 0;
754  $backend->renew();
755  $this->assertTrue( $this->onSessionMetadataCalled );
756  $blob = $this->store->getSession( self::SESSIONID );
757  $this->assertInternalType( 'array', $blob );
758  $this->assertArrayHasKey( 'metadata', $blob );
759  $metadata = $blob['metadata'];
760  $this->assertInternalType( 'array', $metadata );
761  $this->assertArrayHasKey( '???', $metadata );
762  $this->assertSame( '!!!', $metadata['???'] );
763  $this->assertNotEquals( 0, $wrap->expires );
764 
765  // Persistent
766  $this->provider = $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
767  $this->provider->expects( $this->atLeastOnce() )->method( 'persistSession' );
768  $this->onSessionMetadataCalled = false;
769  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
770  $this->store->setSessionData( self::SESSIONID, $testData );
771  $backend = $this->getBackend( $user );
772  $this->store->deleteSession( self::SESSIONID );
773  $wrap = \TestingAccessWrapper::newFromObject( $backend );
774  $wrap->persist = true;
775  $this->assertTrue( $backend->isPersistent(), 'sanity check' );
776  $wrap->metaDirty = false;
777  $wrap->dataDirty = false;
778  $wrap->forcePersist = false;
779  $wrap->expires = 0;
780  $backend->renew();
781  $this->assertTrue( $this->onSessionMetadataCalled );
782  $blob = $this->store->getSession( self::SESSIONID );
783  $this->assertInternalType( 'array', $blob );
784  $this->assertArrayHasKey( 'metadata', $blob );
785  $metadata = $blob['metadata'];
786  $this->assertInternalType( 'array', $metadata );
787  $this->assertArrayHasKey( '???', $metadata );
788  $this->assertSame( '!!!', $metadata['???'] );
789  $this->assertNotEquals( 0, $wrap->expires );
790 
791  // Not persistent, not expiring
792  $this->provider = $this->getMock( 'DummySessionProvider', [ 'persistSession' ] );
793  $this->provider->expects( $this->never() )->method( 'persistSession' );
794  $this->onSessionMetadataCalled = false;
795  $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'SessionMetadata' => [ $this ] ] );
796  $this->store->setSessionData( self::SESSIONID, $testData );
797  $backend = $this->getBackend( $user );
798  $this->store->deleteSession( self::SESSIONID );
799  $wrap = \TestingAccessWrapper::newFromObject( $backend );
800  $this->assertFalse( $backend->isPersistent(), 'sanity check' );
801  $wrap->metaDirty = false;
802  $wrap->dataDirty = false;
803  $wrap->forcePersist = false;
804  $expires = time() + $wrap->lifetime + 100;
805  $wrap->expires = $expires;
806  $backend->renew();
807  $this->assertFalse( $this->onSessionMetadataCalled );
808  $this->assertFalse( $this->store->getSession( self::SESSIONID ), 'making sure it didn\'t save' );
809  $this->assertEquals( $expires, $wrap->expires );
810  }
811 
812  public function onSessionMetadata( SessionBackend $backend, array &$metadata, array $requests ) {
813  $this->onSessionMetadataCalled = true;
814  $metadata['???'] = '!!!';
815  }
816 
817  public function testTakeOverGlobalSession() {
820  }
821  if ( !PHPSessionHandler::isEnabled() ) {
822  $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
823  $rProp->setAccessible( true );
824  $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
825  $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
826  session_write_close();
827  $handler->enable = false;
828  } );
829  $handler->enable = true;
830  }
831 
832  $backend = $this->getBackend( static::getTestSysop()->getUser() );
833  \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
834 
835  $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
836 
838  $request = \RequestContext::getMain()->getRequest();
839  $manager->globalSession = $backend->getSession( $request );
840  $manager->globalSessionRequest = $request;
841 
842  session_id( '' );
843  \TestingAccessWrapper::newFromObject( $backend )->checkPHPSession();
844  $this->assertSame( $backend->getId(), session_id() );
845  session_write_close();
846 
847  $backend2 = $this->getBackend(
848  User::newFromName( 'UTSysop' ), 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
849  );
850  \TestingAccessWrapper::newFromObject( $backend2 )->usePhpSessionHandling = true;
851 
852  session_id( '' );
853  \TestingAccessWrapper::newFromObject( $backend2 )->checkPHPSession();
854  $this->assertSame( '', session_id() );
855  }
856 
857  public function testResetIdOfGlobalSession() {
860  }
861  if ( !PHPSessionHandler::isEnabled() ) {
862  $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
863  $rProp->setAccessible( true );
864  $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
865  $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
866  session_write_close();
867  $handler->enable = false;
868  } );
869  $handler->enable = true;
870  }
871 
872  $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
873  \TestingAccessWrapper::newFromObject( $backend )->usePhpSessionHandling = true;
874 
875  $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
876 
878  $request = \RequestContext::getMain()->getRequest();
879  $manager->globalSession = $backend->getSession( $request );
880  $manager->globalSessionRequest = $request;
881 
882  session_id( self::SESSIONID );
883  \MediaWiki\quietCall( 'session_start' );
884  $_SESSION['foo'] = __METHOD__;
885  $backend->resetId();
886  $this->assertNotEquals( self::SESSIONID, $backend->getId() );
887  $this->assertSame( $backend->getId(), session_id() );
888  $this->assertArrayHasKey( 'foo', $_SESSION );
889  $this->assertSame( __METHOD__, $_SESSION['foo'] );
890  session_write_close();
891  }
892 
893  public function testUnpersistOfGlobalSession() {
896  }
897  if ( !PHPSessionHandler::isEnabled() ) {
898  $rProp = new \ReflectionProperty( PHPSessionHandler::class, 'instance' );
899  $rProp->setAccessible( true );
900  $handler = \TestingAccessWrapper::newFromObject( $rProp->getValue() );
901  $resetHandler = new \Wikimedia\ScopedCallback( function () use ( $handler ) {
902  session_write_close();
903  $handler->enable = false;
904  } );
905  $handler->enable = true;
906  }
907 
908  $backend = $this->getBackend( User::newFromName( 'UTSysop' ) );
909  $wrap = \TestingAccessWrapper::newFromObject( $backend );
910  $wrap->usePhpSessionHandling = true;
911  $wrap->persist = true;
912 
913  $resetSingleton = TestUtils::setSessionManagerSingleton( $this->manager );
914 
916  $request = \RequestContext::getMain()->getRequest();
917  $manager->globalSession = $backend->getSession( $request );
918  $manager->globalSessionRequest = $request;
919 
920  session_id( self::SESSIONID . 'x' );
921  \MediaWiki\quietCall( 'session_start' );
922  $backend->unpersist();
923  $this->assertSame( self::SESSIONID . 'x', session_id() );
924 
925  session_id( self::SESSIONID );
926  $wrap->persist = true;
927  $backend->unpersist();
928  $this->assertSame( '', session_id() );
929  }
930 
931  public function testGetAllowedUserRights() {
932  $this->provider = $this->getMockBuilder( 'DummySessionProvider' )
933  ->setMethods( [ 'getAllowedUserRights' ] )
934  ->getMock();
935  $this->provider->expects( $this->any() )->method( 'getAllowedUserRights' )
936  ->will( $this->returnValue( [ 'foo', 'bar' ] ) );
937 
938  $backend = $this->getBackend();
939  $this->assertSame( [ 'foo', 'bar' ], $backend->getAllowedUserRights() );
940  }
941 
942 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:525
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
mergeMwGlobalArrayValue($name, $values)
Merges the given values into a MW global array variable.
This is the actual workhorse for Session.
the array() calling protocol came about after MediaWiki 1.4rc1.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static newFromUser(User $user, $verified=false)
Create an instance from an existing User object.
Definition: UserInfo.php:116
static setSessionManagerSingleton(SessionManager $manager=null)
Override the singleton for unit testing.
Definition: TestUtils.php:17
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:177
MediaWiki s SiteStore can be cached and stored in a flat in a json format If the SiteStore is frequently the file cache may provide a performance benefit over a database store
Definition: sitescache.txt:1
BagOStuff with utility functions for MediaWiki\\Session\\* testing.
static getMain()
Static methods.
static install(SessionManager $manager)
Install a session handler for the current web request.
getBackend(User $user=null, $id=null)
Returns a non-persistent backend that thinks it has at least one session active.
isPersistent()
Indicate whether this session is persisted across requests.
Session Database MediaWiki\Session\SessionBackend.
Allows to change the fields on the form that will be generated are created Can be used to omit specific feeds from being outputted You must not use this hook to add use OutputPage::addFeedLink() instead.&$feedLinks hooks can tweak the array to change how login etc forms should look $requests
Definition: hooks.txt:302
static isInstalled()
Test whether the handler is installed.
static singleton()
Get the global SessionManager.
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
onSessionMetadata(SessionBackend $backend, array &$metadata, array $requests)
save($closing=false)
Save the session.
static isEnabled()
Test whether the handler is installed and enabled.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
error also a ContextSource you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2573
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
Value object holding the session ID in a manner that can be globally updated.
Definition: SessionId.php:38
wfMemcKey()
Make a cache key for the local wiki.
This serves as the entry point to the MediaWiki session handling system.
static newFromObject($object)
Return the same object, without access restrictions.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:802
static newFromName($name, $verified=false)
Create an instance for a logged-in user by name.
Definition: UserInfo.php:102
Value object returned by SessionProvider.
Definition: SessionInfo.php:34