MediaWiki  1.31.0
AuthManagerTest.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Auth;
4 
5 use Config;
8 use Psr\Log\LoggerInterface;
9 use Psr\Log\LogLevel;
12 use Wikimedia\ScopedCallback;
13 use Wikimedia\TestingAccessWrapper;
14 
22  protected $request;
24  protected $config;
26  protected $logger;
27 
28  protected $preauthMocks = [];
29  protected $primaryauthMocks = [];
30  protected $secondaryauthMocks = [];
31 
33  protected $manager;
35  protected $managerPriv;
36 
37  protected function setUp() {
38  parent::setUp();
39 
40  $this->setMwGlobals( [ 'wgAuth' => null ] );
41  $this->stashMwGlobals( [ 'wgHooks' ] );
42  }
43 
50  protected function hook( $hook, $expect ) {
52  $mock = $this->getMockBuilder( __CLASS__ )
53  ->setMethods( [ "on$hook" ] )
54  ->getMock();
55  $wgHooks[$hook] = [ $mock ];
56  return $mock->expects( $expect )->method( "on$hook" );
57  }
58 
63  protected function unhook( $hook ) {
65  $wgHooks[$hook] = [];
66  }
67 
74  protected function message( $key, $params = [] ) {
75  if ( $key === null ) {
76  return null;
77  }
78  if ( $key instanceof \MessageSpecifier ) {
79  $params = $key->getParams();
80  $key = $key->getKey();
81  }
82  return new \Message( $key, $params, \Language::factory( 'en' ) );
83  }
84 
90  protected function initializeConfig() {
91  $config = [
92  'preauth' => [
93  ],
94  'primaryauth' => [
95  ],
96  'secondaryauth' => [
97  ],
98  ];
99 
100  foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
101  $key = $type . 'Mocks';
102  foreach ( $this->$key as $mock ) {
103  $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
104  return $mock;
105  } ];
106  }
107  }
108 
109  $this->config->set( 'AuthManagerConfig', $config );
110  $this->config->set( 'LanguageCode', 'en' );
111  $this->config->set( 'NewUserLog', false );
112  }
113 
118  protected function initializeManager( $regen = false ) {
119  if ( $regen || !$this->config ) {
120  $this->config = new \HashConfig();
121  }
122  if ( $regen || !$this->request ) {
123  $this->request = new \FauxRequest();
124  }
125  if ( !$this->logger ) {
126  $this->logger = new \TestLogger();
127  }
128 
129  if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
130  $this->initializeConfig();
131  }
132  $this->manager = new AuthManager( $this->request, $this->config );
133  $this->manager->setLogger( $this->logger );
134  $this->managerPriv = TestingAccessWrapper::newFromObject( $this->manager );
135  }
136 
143  protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
144  if ( !$this->config ) {
145  $this->config = new \HashConfig();
146  $this->initializeConfig();
147  }
148  $this->config->set( 'ObjectCacheSessionExpiry', 100 );
149 
150  $methods[] = '__toString';
151  $methods[] = 'describe';
152  if ( $canChangeUser !== null ) {
153  $methods[] = 'canChangeUser';
154  }
155  $provider = $this->getMockBuilder( \DummySessionProvider::class )
156  ->setMethods( $methods )
157  ->getMock();
158  $provider->expects( $this->any() )->method( '__toString' )
159  ->will( $this->returnValue( 'MockSessionProvider' ) );
160  $provider->expects( $this->any() )->method( 'describe' )
161  ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
162  if ( $canChangeUser !== null ) {
163  $provider->expects( $this->any() )->method( 'canChangeUser' )
164  ->will( $this->returnValue( $canChangeUser ) );
165  }
166  $this->config->set( 'SessionProviders', [
167  [ 'factory' => function () use ( $provider ) {
168  return $provider;
169  } ],
170  ] );
171 
172  $manager = new \MediaWiki\Session\SessionManager( [
173  'config' => $this->config,
174  'logger' => new \Psr\Log\NullLogger(),
175  'store' => new \HashBagOStuff(),
176  ] );
177  TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
178 
180 
181  if ( $this->request ) {
182  $manager->getSessionForRequest( $this->request );
183  }
184 
185  return [ $provider, $reset ];
186  }
187 
188  public function testSingleton() {
189  // Temporarily clear out the global singleton, if any, to test creating
190  // one.
191  $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
192  $rProp->setAccessible( true );
193  $old = $rProp->getValue();
194  $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
195  $rProp->setValue( null );
196 
197  $singleton = AuthManager::singleton();
198  $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
199  $this->assertSame( $singleton, AuthManager::singleton() );
200  $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
201  $this->assertSame(
202  \RequestContext::getMain()->getConfig(),
203  TestingAccessWrapper::newFromObject( $singleton )->config
204  );
205  }
206 
207  public function testCanAuthenticateNow() {
208  $this->initializeManager();
209 
210  list( $provider, $reset ) = $this->getMockSessionProvider( false );
211  $this->assertFalse( $this->manager->canAuthenticateNow() );
212  ScopedCallback::consume( $reset );
213 
214  list( $provider, $reset ) = $this->getMockSessionProvider( true );
215  $this->assertTrue( $this->manager->canAuthenticateNow() );
216  ScopedCallback::consume( $reset );
217  }
218 
219  public function testNormalizeUsername() {
220  $mocks = [
221  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
222  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
223  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
224  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
225  ];
226  foreach ( $mocks as $key => $mock ) {
227  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
228  }
229  $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
230  ->with( $this->identicalTo( 'XYZ' ) )
231  ->willReturn( 'Foo' );
232  $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
233  ->with( $this->identicalTo( 'XYZ' ) )
234  ->willReturn( 'Foo' );
235  $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236  ->with( $this->identicalTo( 'XYZ' ) )
237  ->willReturn( null );
238  $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239  ->with( $this->identicalTo( 'XYZ' ) )
240  ->willReturn( 'Bar!' );
241 
242  $this->primaryauthMocks = $mocks;
243 
244  $this->initializeManager();
245 
246  $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
247  }
248 
253  public function testSecuritySensitiveOperationStatus( $mutableSession ) {
254  $this->logger = new \Psr\Log\NullLogger();
255  $user = \User::newFromName( 'UTSysop' );
256  $provideUser = null;
257  $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
258 
259  list( $provider, $reset ) = $this->getMockSessionProvider(
260  $mutableSession, [ 'provideSessionInfo' ]
261  );
262  $provider->expects( $this->any() )->method( 'provideSessionInfo' )
263  ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
265  'provider' => $provider,
266  'id' => \DummySessionProvider::ID,
267  'persisted' => true,
268  'userInfo' => UserInfo::newFromUser( $provideUser, true )
269  ] );
270  } ) );
271  $this->initializeManager();
272 
273  $this->config->set( 'ReauthenticateTime', [] );
274  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
275  $provideUser = new \User;
276  $session = $provider->getManager()->getSessionForRequest( $this->request );
277  $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
278 
279  // Anonymous user => reauth
280  $session->set( 'AuthManager:lastAuthId', 0 );
281  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
282  $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
283 
284  $provideUser = $user;
285  $session = $provider->getManager()->getSessionForRequest( $this->request );
286  $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
287 
288  // Error for no default (only gets thrown for non-anonymous user)
289  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
290  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
291  try {
292  $this->manager->securitySensitiveOperationStatus( 'foo' );
293  $this->fail( 'Expected exception not thrown' );
294  } catch ( \UnexpectedValueException $ex ) {
295  $this->assertSame(
296  $mutableSession
297  ? '$wgReauthenticateTime lacks a default'
298  : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
299  $ex->getMessage()
300  );
301  }
302 
303  if ( $mutableSession ) {
304  $this->config->set( 'ReauthenticateTime', [
305  'test' => 100,
306  'test2' => -1,
307  'default' => 10,
308  ] );
309 
310  // Mismatched user ID
311  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
312  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
313  $this->assertSame(
314  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
315  );
316  $this->assertSame(
317  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
318  );
319  $this->assertSame(
320  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
321  );
322 
323  // Missing time
324  $session->set( 'AuthManager:lastAuthId', $user->getId() );
325  $session->set( 'AuthManager:lastAuthTimestamp', null );
326  $this->assertSame(
327  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
328  );
329  $this->assertSame(
330  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
331  );
332  $this->assertSame(
333  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
334  );
335 
336  // Recent enough to pass
337  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
338  $this->assertSame(
339  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
340  );
341 
342  // Not recent enough to pass
343  $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
344  $this->assertSame(
345  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
346  );
347  // But recent enough for the 'test' operation
348  $this->assertSame(
349  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
350  );
351  } else {
352  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
353  'test' => false,
354  'default' => true,
355  ] );
356 
357  $this->assertEquals(
358  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
359  );
360 
361  $this->assertEquals(
362  AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
363  );
364  }
365 
366  // Test hook, all three possible values
367  foreach ( [
369  AuthManager::SEC_REAUTH => $reauth,
371  ] as $hook => $expect ) {
372  $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
373  ->with(
374  $this->anything(),
375  $this->anything(),
376  $this->callback( function ( $s ) use ( $session ) {
377  return $s->getId() === $session->getId();
378  } ),
379  $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
380  )
381  ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
382  $v = $hook;
383  return true;
384  } ) );
385  $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
386  $this->assertEquals(
387  $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
388  );
389  $this->assertEquals(
390  $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
391  );
392  $this->unhook( 'SecuritySensitiveOperationStatus' );
393  }
394 
395  ScopedCallback::consume( $reset );
396  }
397 
398  public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
399  }
400 
401  public static function provideSecuritySensitiveOperationStatus() {
402  return [
403  [ true ],
404  [ false ],
405  ];
406  }
407 
414  public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
415  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
416  $mock1->expects( $this->any() )->method( 'getUniqueId' )
417  ->will( $this->returnValue( 'primary1' ) );
418  $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
419  ->with( $this->equalTo( 'UTSysop' ) )
420  ->will( $this->returnValue( $primary1Can ) );
421  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
422  $mock2->expects( $this->any() )->method( 'getUniqueId' )
423  ->will( $this->returnValue( 'primary2' ) );
424  $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425  ->with( $this->equalTo( 'UTSysop' ) )
426  ->will( $this->returnValue( $primary2Can ) );
427  $this->primaryauthMocks = [ $mock1, $mock2 ];
428 
429  $this->initializeManager( true );
430  $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
431  }
432 
433  public static function provideUserCanAuthenticate() {
434  return [
435  [ false, false, false ],
436  [ true, false, true ],
437  [ false, true, true ],
438  [ true, true, true ],
439  ];
440  }
441 
442  public function testRevokeAccessForUser() {
443  $this->initializeManager();
444 
445  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
446  $mock->expects( $this->any() )->method( 'getUniqueId' )
447  ->will( $this->returnValue( 'primary' ) );
448  $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
449  ->with( $this->equalTo( 'UTSysop' ) );
450  $this->primaryauthMocks = [ $mock ];
451 
452  $this->initializeManager( true );
453  $this->logger->setCollect( true );
454 
455  $this->manager->revokeAccessForUser( 'UTSysop' );
456 
457  $this->assertSame( [
458  [ LogLevel::INFO, 'Revoking access for {user}' ],
459  ], $this->logger->getBuffer() );
460  }
461 
462  public function testProviderCreation() {
463  $mocks = [
464  'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
465  'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
466  'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
467  ];
468  foreach ( $mocks as $key => $mock ) {
469  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
470  $mock->expects( $this->once() )->method( 'setLogger' );
471  $mock->expects( $this->once() )->method( 'setManager' );
472  $mock->expects( $this->once() )->method( 'setConfig' );
473  }
474  $this->preauthMocks = [ $mocks['pre'] ];
475  $this->primaryauthMocks = [ $mocks['primary'] ];
476  $this->secondaryauthMocks = [ $mocks['secondary'] ];
477 
478  // Normal operation
479  $this->initializeManager();
480  $this->assertSame(
481  $mocks['primary'],
482  $this->managerPriv->getAuthenticationProvider( 'primary' )
483  );
484  $this->assertSame(
485  $mocks['secondary'],
486  $this->managerPriv->getAuthenticationProvider( 'secondary' )
487  );
488  $this->assertSame(
489  $mocks['pre'],
490  $this->managerPriv->getAuthenticationProvider( 'pre' )
491  );
492  $this->assertSame(
493  [ 'pre' => $mocks['pre'] ],
494  $this->managerPriv->getPreAuthenticationProviders()
495  );
496  $this->assertSame(
497  [ 'primary' => $mocks['primary'] ],
498  $this->managerPriv->getPrimaryAuthenticationProviders()
499  );
500  $this->assertSame(
501  [ 'secondary' => $mocks['secondary'] ],
502  $this->managerPriv->getSecondaryAuthenticationProviders()
503  );
504 
505  // Duplicate IDs
506  $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
507  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
508  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
509  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
510  $this->preauthMocks = [ $mock1 ];
511  $this->primaryauthMocks = [ $mock2 ];
512  $this->secondaryauthMocks = [];
513  $this->initializeManager( true );
514  try {
515  $this->managerPriv->getAuthenticationProvider( 'Y' );
516  $this->fail( 'Expected exception not thrown' );
517  } catch ( \RuntimeException $ex ) {
518  $class1 = get_class( $mock1 );
519  $class2 = get_class( $mock2 );
520  $this->assertSame(
521  "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
522  );
523  }
524 
525  // Wrong classes
526  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
527  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
528  $class = get_class( $mock );
529  $this->preauthMocks = [ $mock ];
530  $this->primaryauthMocks = [ $mock ];
531  $this->secondaryauthMocks = [ $mock ];
532  $this->initializeManager( true );
533  try {
534  $this->managerPriv->getPreAuthenticationProviders();
535  $this->fail( 'Expected exception not thrown' );
536  } catch ( \RuntimeException $ex ) {
537  $this->assertSame(
538  "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
539  $ex->getMessage()
540  );
541  }
542  try {
543  $this->managerPriv->getPrimaryAuthenticationProviders();
544  $this->fail( 'Expected exception not thrown' );
545  } catch ( \RuntimeException $ex ) {
546  $this->assertSame(
547  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
548  $ex->getMessage()
549  );
550  }
551  try {
552  $this->managerPriv->getSecondaryAuthenticationProviders();
553  $this->fail( 'Expected exception not thrown' );
554  } catch ( \RuntimeException $ex ) {
555  $this->assertSame(
556  "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
557  $ex->getMessage()
558  );
559  }
560 
561  // Sorting
562  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
563  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
564  $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
565  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
566  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
567  $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
568  $this->preauthMocks = [];
569  $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
570  $this->secondaryauthMocks = [];
571  $this->initializeConfig();
572  $config = $this->config->get( 'AuthManagerConfig' );
573 
574  $this->initializeManager( false );
575  $this->assertSame(
576  [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
577  $this->managerPriv->getPrimaryAuthenticationProviders(),
578  'sanity check'
579  );
580 
581  $config['primaryauth']['A']['sort'] = 100;
582  $config['primaryauth']['C']['sort'] = -1;
583  $this->config->set( 'AuthManagerConfig', $config );
584  $this->initializeManager( false );
585  $this->assertSame(
586  [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
587  $this->managerPriv->getPrimaryAuthenticationProviders()
588  );
589  }
590 
591  public function testSetDefaultUserOptions() {
592  $this->initializeManager();
593 
595  $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
596  $context->setLanguage( 'de' );
597  $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
598 
599  $user = \User::newFromName( self::usernameForCreation() );
600  $user->addToDatabase();
601  $oldToken = $user->getToken();
602  $this->managerPriv->setDefaultUserOptions( $user, false );
603  $user->saveSettings();
604  $this->assertNotEquals( $oldToken, $user->getToken() );
605  $this->assertSame( 'zh', $user->getOption( 'language' ) );
606  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
607 
608  $user = \User::newFromName( self::usernameForCreation() );
609  $user->addToDatabase();
610  $oldToken = $user->getToken();
611  $this->managerPriv->setDefaultUserOptions( $user, true );
612  $user->saveSettings();
613  $this->assertNotEquals( $oldToken, $user->getToken() );
614  $this->assertSame( 'de', $user->getOption( 'language' ) );
615  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
616 
617  $this->setMwGlobals( 'wgContLang', \Language::factory( 'fr' ) );
618 
619  $user = \User::newFromName( self::usernameForCreation() );
620  $user->addToDatabase();
621  $oldToken = $user->getToken();
622  $this->managerPriv->setDefaultUserOptions( $user, true );
623  $user->saveSettings();
624  $this->assertNotEquals( $oldToken, $user->getToken() );
625  $this->assertSame( 'de', $user->getOption( 'language' ) );
626  $this->assertSame( null, $user->getOption( 'variant' ) );
627  }
628 
630  $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
631  $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
632  $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
633  $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
634  $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
635  $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
636  $this->primaryauthMocks = [ $mockA ];
637 
638  $this->logger = new \TestLogger( true );
639 
640  // Test without first initializing the configured providers
641  $this->initializeManager();
642  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
643  $this->assertSame(
644  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
645  );
646  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
647  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
648  $this->assertSame( [
649  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
650  ], $this->logger->getBuffer() );
651  $this->logger->clearBuffer();
652 
653  // Test with first initializing the configured providers
654  $this->initializeManager();
655  $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
656  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
657  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
658  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
659  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
660  $this->assertSame(
661  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
662  );
663  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
664  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
665  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
666  $this->assertNull(
667  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
668  );
669  $this->assertSame( [
670  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
671  [
672  LogLevel::WARNING,
673  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
674  ],
675  ], $this->logger->getBuffer() );
676  $this->logger->clearBuffer();
677 
678  // Test duplicate IDs
679  $this->initializeManager();
680  try {
681  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
682  $this->fail( 'Expected exception not thrown' );
683  } catch ( \RuntimeException $ex ) {
684  $class1 = get_class( $mockB );
685  $class2 = get_class( $mockB2 );
686  $this->assertSame(
687  "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
688  );
689  }
690 
691  // Wrong classes
692  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
693  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
694  $class = get_class( $mock );
695  try {
696  $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
697  $this->fail( 'Expected exception not thrown' );
698  } catch ( \RuntimeException $ex ) {
699  $this->assertSame(
700  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
701  $ex->getMessage()
702  );
703  }
704  }
705 
706  public function testBeginAuthentication() {
707  $this->initializeManager();
708 
709  // Immutable session
710  list( $provider, $reset ) = $this->getMockSessionProvider( false );
711  $this->hook( 'UserLoggedIn', $this->never() );
712  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
713  try {
714  $this->manager->beginAuthentication( [], 'http://localhost/' );
715  $this->fail( 'Expected exception not thrown' );
716  } catch ( \LogicException $ex ) {
717  $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
718  }
719  $this->unhook( 'UserLoggedIn' );
720  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
721  ScopedCallback::consume( $reset );
722  $this->initializeManager( true );
723 
724  // CreatedAccountAuthenticationRequest
725  $user = \User::newFromName( 'UTSysop' );
726  $reqs = [
727  new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
728  ];
729  $this->hook( 'UserLoggedIn', $this->never() );
730  try {
731  $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
732  $this->fail( 'Expected exception not thrown' );
733  } catch ( \LogicException $ex ) {
734  $this->assertSame(
735  'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
736  'that created the account',
737  $ex->getMessage()
738  );
739  }
740  $this->unhook( 'UserLoggedIn' );
741 
742  $this->request->getSession()->clear();
743  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
744  $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
745  $this->hook( 'UserLoggedIn', $this->once() )
746  ->with( $this->callback( function ( $u ) use ( $user ) {
747  return $user->getId() === $u->getId() && $user->getName() === $u->getName();
748  } ) );
749  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
750  $this->logger->setCollect( true );
751  $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
752  $this->logger->setCollect( false );
753  $this->unhook( 'UserLoggedIn' );
754  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
755  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
756  $this->assertSame( $user->getName(), $ret->username );
757  $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
758  $this->assertEquals(
759  time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
760  'timestamp ±1', 1
761  );
762  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
763  $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
764  $this->assertSame( [
765  [ LogLevel::INFO, 'Logging in {user} after account creation' ],
766  ], $this->logger->getBuffer() );
767  }
768 
769  public function testCreateFromLogin() {
770  $user = \User::newFromName( 'UTSysop' );
771  $req1 = $this->createMock( AuthenticationRequest::class );
772  $req2 = $this->createMock( AuthenticationRequest::class );
773  $req3 = $this->createMock( AuthenticationRequest::class );
774  $userReq = new UsernameAuthenticationRequest;
775  $userReq->username = 'UTDummy';
776 
777  $req1->returnToUrl = 'http://localhost/';
778  $req2->returnToUrl = 'http://localhost/';
779  $req3->returnToUrl = 'http://localhost/';
780  $req3->username = 'UTDummy';
781  $userReq->returnToUrl = 'http://localhost/';
782 
783  // Passing one into beginAuthentication(), and an immediate FAIL
784  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
785  $this->primaryauthMocks = [ $primary ];
786  $this->initializeManager( true );
788  $res->createRequest = $req1;
789  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
790  ->will( $this->returnValue( $res ) );
791  $createReq = new CreateFromLoginAuthenticationRequest(
792  null, [ $req2->getUniqueId() => $req2 ]
793  );
794  $this->logger->setCollect( true );
795  $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
796  $this->logger->setCollect( false );
797  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
798  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
799  $this->assertSame( $req1, $ret->createRequest->createRequest );
800  $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
801 
802  // UI, then FAIL in beginAuthentication()
803  $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
804  ->setMethods( [ 'continuePrimaryAuthentication' ] )
805  ->getMockForAbstractClass();
806  $this->primaryauthMocks = [ $primary ];
807  $this->initializeManager( true );
808  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
809  ->will( $this->returnValue(
810  AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
811  ) );
813  $res->createRequest = $req2;
814  $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
815  ->will( $this->returnValue( $res ) );
816  $this->logger->setCollect( true );
817  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
818  $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
819  $ret = $this->manager->continueAuthentication( [] );
820  $this->logger->setCollect( false );
821  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
822  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
823  $this->assertSame( $req2, $ret->createRequest->createRequest );
824  $this->assertEquals( [], $ret->createRequest->maybeLink );
825 
826  // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
827  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
828  $this->primaryauthMocks = [ $primary ];
829  $this->initializeManager( true );
830  $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
831  $createReq->returnToUrl = 'http://localhost/';
832  $createReq->username = 'UTDummy';
833  $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
834  $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
835  ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
836  ->will( $this->returnValue( $res ) );
837  $primary->expects( $this->any() )->method( 'accountCreationType' )
838  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
839  $this->logger->setCollect( true );
840  $ret = $this->manager->beginAccountCreation(
841  $user, [ $userReq, $createReq ], 'http://localhost/'
842  );
843  $this->logger->setCollect( false );
844  $this->assertSame( AuthenticationResponse::UI, $ret->status );
845  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
846  $this->assertNotNull( $state );
847  $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
848  $this->assertEquals( [ $req2 ], $state['maybeLink'] );
849  }
850 
859  public function testAuthentication(
860  StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
861  array $managerResponses, $link = false
862  ) {
863  $this->initializeManager();
864  $user = \User::newFromName( 'UTSysop' );
865  $id = $user->getId();
866  $name = $user->getName();
867 
868  // Set up lots of mocks...
870  $req->rememberMe = (bool)rand( 0, 1 );
871  $req->pre = $preResponse;
872  $req->primary = $primaryResponses;
873  $req->secondary = $secondaryResponses;
874  $mocks = [];
875  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
876  $class = ucfirst( $key ) . 'AuthenticationProvider';
877  $mocks[$key] = $this->getMockForAbstractClass(
878  "MediaWiki\\Auth\\$class", [], "Mock$class"
879  );
880  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
881  ->will( $this->returnValue( $key ) );
882  $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
883  $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
884  ->will( $this->returnValue( $key . '2' ) );
885  $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
886  $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
887  ->will( $this->returnValue( $key . '3' ) );
888  }
889  foreach ( $mocks as $mock ) {
890  $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
891  ->will( $this->returnValue( [] ) );
892  }
893 
894  $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
895  ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
896  $this->assertContains( $req, $reqs );
897  return $req->pre;
898  } ) );
899 
900  $ct = count( $req->primary );
901  $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
902  $this->assertContains( $req, $reqs );
903  return array_shift( $req->primary );
904  } );
905  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
906  ->method( 'beginPrimaryAuthentication' )
907  ->will( $callback );
908  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
909  ->method( 'continuePrimaryAuthentication' )
910  ->will( $callback );
911  if ( $link ) {
912  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
913  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
914  }
915 
916  $ct = count( $req->secondary );
917  $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
918  $this->assertSame( $id, $user->getId() );
919  $this->assertSame( $name, $user->getName() );
920  $this->assertContains( $req, $reqs );
921  return array_shift( $req->secondary );
922  } );
923  $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
924  ->method( 'beginSecondaryAuthentication' )
925  ->will( $callback );
926  $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
927  ->method( 'continueSecondaryAuthentication' )
928  ->will( $callback );
929 
931  $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
932  ->will( $this->returnValue( StatusValue::newGood() ) );
933  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
934  ->will( $this->returnValue( $abstain ) );
935  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
936  $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
937  ->will( $this->returnValue( $abstain ) );
938  $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
939  $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
940  ->will( $this->returnValue( $abstain ) );
941  $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
942 
943  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
944  $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
945  $this->secondaryauthMocks = [
946  $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
947  // So linking happens
949  ];
950  $this->initializeManager( true );
951  $this->logger->setCollect( true );
952 
953  $constraint = \PHPUnit_Framework_Assert::logicalOr(
954  $this->equalTo( AuthenticationResponse::PASS ),
955  $this->equalTo( AuthenticationResponse::FAIL )
956  );
957  $providers = array_filter(
958  array_merge(
959  $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
960  ),
961  function ( $p ) {
962  return is_callable( [ $p, 'expects' ] );
963  }
964  );
965  foreach ( $providers as $p ) {
966  $p->postCalled = false;
967  $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
968  ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
969  if ( $user !== null ) {
970  $this->assertInstanceOf( \User::class, $user );
971  $this->assertSame( 'UTSysop', $user->getName() );
972  }
973  $this->assertInstanceOf( AuthenticationResponse::class, $response );
974  $this->assertThat( $response->status, $constraint );
975  $p->postCalled = $response->status;
976  } );
977  }
978 
979  $session = $this->request->getSession();
980  $session->setRememberUser( !$req->rememberMe );
981 
982  foreach ( $managerResponses as $i => $response ) {
985  if ( $success ) {
986  $this->hook( 'UserLoggedIn', $this->once() )
987  ->with( $this->callback( function ( $user ) use ( $id, $name ) {
988  return $user->getId() === $id && $user->getName() === $name;
989  } ) );
990  } else {
991  $this->hook( 'UserLoggedIn', $this->never() );
992  }
993  if ( $success || (
994  $response instanceof AuthenticationResponse &&
996  $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
997  $response->message->getKey() !== 'authmanager-authn-no-primary'
998  )
999  ) {
1000  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1001  } else {
1002  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1003  }
1004 
1005  $ex = null;
1006  try {
1007  if ( !$i ) {
1008  $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1009  } else {
1010  $ret = $this->manager->continueAuthentication( [ $req ] );
1011  }
1012  if ( $response instanceof \Exception ) {
1013  $this->fail( 'Expected exception not thrown', "Response $i" );
1014  }
1015  } catch ( \Exception $ex ) {
1016  if ( !$response instanceof \Exception ) {
1017  throw $ex;
1018  }
1019  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1020  $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1021  "Response $i, exception, session state" );
1022  $this->unhook( 'UserLoggedIn' );
1023  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1024  return;
1025  }
1026 
1027  $this->unhook( 'UserLoggedIn' );
1028  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1029 
1030  $this->assertSame( 'http://localhost/', $req->returnToUrl );
1031 
1032  $ret->message = $this->message( $ret->message );
1033  $this->assertEquals( $response, $ret, "Response $i, response" );
1034  if ( $success ) {
1035  $this->assertSame( $id, $session->getUser()->getId(),
1036  "Response $i, authn" );
1037  } else {
1038  $this->assertSame( 0, $session->getUser()->getId(),
1039  "Response $i, authn" );
1040  }
1041  if ( $success || $response->status === AuthenticationResponse::FAIL ) {
1042  $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1043  "Response $i, session state" );
1044  foreach ( $providers as $p ) {
1045  $this->assertSame( $response->status, $p->postCalled,
1046  "Response $i, post-auth callback called" );
1047  }
1048  } else {
1049  $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1050  "Response $i, session state" );
1051  foreach ( $ret->neededRequests as $neededReq ) {
1052  $this->assertEquals( AuthManager::ACTION_LOGIN, $neededReq->action,
1053  "Response $i, neededRequest action" );
1054  }
1055  $this->assertEquals(
1056  $ret->neededRequests,
1057  $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
1058  "Response $i, continuation check"
1059  );
1060  foreach ( $providers as $p ) {
1061  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
1062  }
1063  }
1064 
1065  $state = $session->getSecret( 'AuthManager::authnState' );
1066  $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
1067  if ( $link && $response->status === AuthenticationResponse::RESTART ) {
1068  $this->assertEquals(
1069  $response->createRequest->maybeLink,
1070  $maybeLink,
1071  "Response $i, maybeLink"
1072  );
1073  } else {
1074  $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1075  }
1076  }
1077 
1078  if ( $success ) {
1079  $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1080  'rememberMe checkbox had effect' );
1081  } else {
1082  $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
1083  'rememberMe checkbox wasn\'t applied' );
1084  }
1085  }
1086 
1087  public function provideAuthentication() {
1088  $rememberReq = new RememberMeAuthenticationRequest;
1089  $rememberReq->action = AuthManager::ACTION_LOGIN;
1090 
1091  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1092  $req->foobar = 'baz';
1093  $restartResponse = AuthenticationResponse::newRestart(
1094  $this->message( 'authmanager-authn-no-local-user' )
1095  );
1096  $restartResponse->neededRequests = [ $rememberReq ];
1097 
1098  $restartResponse2Pass = AuthenticationResponse::newPass( null );
1099  $restartResponse2Pass->linkRequest = $req;
1100  $restartResponse2 = AuthenticationResponse::newRestart(
1101  $this->message( 'authmanager-authn-no-local-user-link' )
1102  );
1103  $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1104  null, [ $req->getUniqueId() => $req ]
1105  );
1106  $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
1107  $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
1108 
1109  $userName = 'UTSysop';
1110 
1111  return [
1112  'Failure in pre-auth' => [
1113  StatusValue::newFatal( 'fail-from-pre' ),
1114  [],
1115  [],
1116  [
1117  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
1119  $this->message( 'authmanager-authn-not-in-progress' )
1120  ),
1121  ]
1122  ],
1123  'Failure in primary' => [
1125  $tmp = [
1126  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1127  ],
1128  [],
1129  $tmp
1130  ],
1131  'All primary abstain' => [
1133  [
1135  ],
1136  [],
1137  [
1138  AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1139  ]
1140  ],
1141  'Primary UI, then redirect, then fail' => [
1143  $tmp = [
1144  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1145  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1146  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1147  ],
1148  [],
1149  $tmp
1150  ],
1151  'Primary redirect, then abstain' => [
1153  [
1155  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1156  ),
1158  ],
1159  [],
1160  [
1161  $tmp,
1162  new \DomainException(
1163  'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1164  )
1165  ]
1166  ],
1167  'Primary UI, then pass with no local user' => [
1169  [
1170  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1172  ],
1173  [],
1174  [
1175  $tmp,
1176  $restartResponse,
1177  ]
1178  ],
1179  'Primary UI, then pass with no local user (link type)' => [
1181  [
1182  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1183  $restartResponse2Pass,
1184  ],
1185  [],
1186  [
1187  $tmp,
1188  $restartResponse2,
1189  ],
1190  true
1191  ],
1192  'Primary pass with invalid username' => [
1194  [
1196  ],
1197  [],
1198  [
1199  new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1200  ]
1201  ],
1202  'Secondary fail' => [
1204  [
1205  AuthenticationResponse::newPass( $userName ),
1206  ],
1207  $tmp = [
1208  AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1209  ],
1210  $tmp
1211  ],
1212  'Secondary UI, then abstain' => [
1214  [
1215  AuthenticationResponse::newPass( $userName ),
1216  ],
1217  [
1218  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1220  ],
1221  [
1222  $tmp,
1223  AuthenticationResponse::newPass( $userName ),
1224  ]
1225  ],
1226  'Secondary pass' => [
1228  [
1229  AuthenticationResponse::newPass( $userName ),
1230  ],
1231  [
1233  ],
1234  [
1235  AuthenticationResponse::newPass( $userName ),
1236  ]
1237  ],
1238  ];
1239  }
1240 
1247  public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1248  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1249  $mock1->expects( $this->any() )->method( 'getUniqueId' )
1250  ->will( $this->returnValue( 'primary1' ) );
1251  $mock1->expects( $this->any() )->method( 'testUserExists' )
1252  ->with( $this->equalTo( 'UTSysop' ) )
1253  ->will( $this->returnValue( $primary1Exists ) );
1254  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1255  $mock2->expects( $this->any() )->method( 'getUniqueId' )
1256  ->will( $this->returnValue( 'primary2' ) );
1257  $mock2->expects( $this->any() )->method( 'testUserExists' )
1258  ->with( $this->equalTo( 'UTSysop' ) )
1259  ->will( $this->returnValue( $primary2Exists ) );
1260  $this->primaryauthMocks = [ $mock1, $mock2 ];
1261 
1262  $this->initializeManager( true );
1263  $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
1264  }
1265 
1266  public static function provideUserExists() {
1267  return [
1268  [ false, false, false ],
1269  [ true, false, true ],
1270  [ false, true, true ],
1271  [ true, true, true ],
1272  ];
1273  }
1274 
1281  public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1282  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1283 
1284  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1285  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1286  $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1287  ->with( $this->equalTo( $req ) )
1288  ->will( $this->returnValue( $primaryReturn ) );
1289  $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
1290  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1291  $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1292  ->with( $this->equalTo( $req ) )
1293  ->will( $this->returnValue( $secondaryReturn ) );
1294 
1295  $this->primaryauthMocks = [ $mock1 ];
1296  $this->secondaryauthMocks = [ $mock2 ];
1297  $this->initializeManager( true );
1298  $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
1299  }
1300 
1301  public static function provideAllowsAuthenticationDataChange() {
1302  $ignored = \Status::newGood( 'ignored' );
1303  $ignored->warning( 'authmanager-change-not-supported' );
1304 
1305  $okFromPrimary = StatusValue::newGood();
1306  $okFromPrimary->warning( 'warning-from-primary' );
1307  $okFromSecondary = StatusValue::newGood();
1308  $okFromSecondary->warning( 'warning-from-secondary' );
1309 
1310  return [
1311  [
1314  \Status::newGood(),
1315  ],
1316  [
1318  StatusValue::newGood( 'ignore' ),
1319  \Status::newGood(),
1320  ],
1321  [
1322  StatusValue::newGood( 'ignored' ),
1324  \Status::newGood(),
1325  ],
1326  [
1327  StatusValue::newGood( 'ignored' ),
1328  StatusValue::newGood( 'ignored' ),
1329  $ignored,
1330  ],
1331  [
1332  StatusValue::newFatal( 'fail from primary' ),
1334  \Status::newFatal( 'fail from primary' ),
1335  ],
1336  [
1337  $okFromPrimary,
1339  \Status::wrap( $okFromPrimary ),
1340  ],
1341  [
1343  StatusValue::newFatal( 'fail from secondary' ),
1344  \Status::newFatal( 'fail from secondary' ),
1345  ],
1346  [
1348  $okFromSecondary,
1349  \Status::wrap( $okFromSecondary ),
1350  ],
1351  ];
1352  }
1353 
1354  public function testChangeAuthenticationData() {
1355  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1356  $req->username = 'UTSysop';
1357 
1358  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1359  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1360  $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1361  ->with( $this->equalTo( $req ) );
1362  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1363  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1364  $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1365  ->with( $this->equalTo( $req ) );
1366 
1367  $this->primaryauthMocks = [ $mock1, $mock2 ];
1368  $this->initializeManager( true );
1369  $this->logger->setCollect( true );
1370  $this->manager->changeAuthenticationData( $req );
1371  $this->assertSame( [
1372  [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
1373  ], $this->logger->getBuffer() );
1374  }
1375 
1376  public function testCanCreateAccounts() {
1377  $types = [
1381  ];
1382 
1383  foreach ( $types as $type => $can ) {
1384  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1385  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1386  $mock->expects( $this->any() )->method( 'accountCreationType' )
1387  ->will( $this->returnValue( $type ) );
1388  $this->primaryauthMocks = [ $mock ];
1389  $this->initializeManager( true );
1390  $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
1391  }
1392  }
1393 
1396 
1397  $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1398 
1399  $this->initializeManager( true );
1400 
1401  $wgGroupPermissions['*']['createaccount'] = true;
1402  $this->assertEquals(
1403  \Status::newGood(),
1404  $this->manager->checkAccountCreatePermissions( new \User )
1405  );
1406 
1407  $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1408  $readOnlyMode->setReason( 'Because' );
1409  $this->assertEquals(
1410  \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1411  $this->manager->checkAccountCreatePermissions( new \User )
1412  );
1413  $readOnlyMode->setReason( false );
1414 
1415  $wgGroupPermissions['*']['createaccount'] = false;
1416  $status = $this->manager->checkAccountCreatePermissions( new \User );
1417  $this->assertFalse( $status->isOK() );
1418  $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1419  $wgGroupPermissions['*']['createaccount'] = true;
1420 
1421  $user = \User::newFromName( 'UTBlockee' );
1422  if ( $user->getID() == 0 ) {
1423  $user->addToDatabase();
1424  \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1425  $user->saveSettings();
1426  }
1427  $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1428  if ( $oldBlock ) {
1429  // An old block will prevent our new one from saving.
1430  $oldBlock->delete();
1431  }
1432  $blockOptions = [
1433  'address' => 'UTBlockee',
1434  'user' => $user->getID(),
1435  'by' => $this->getTestSysop()->getUser()->getId(),
1436  'reason' => __METHOD__,
1437  'expiry' => time() + 100500,
1438  'createAccount' => true,
1439  ];
1440  $block = new \Block( $blockOptions );
1441  $block->insert();
1442  $status = $this->manager->checkAccountCreatePermissions( $user );
1443  $this->assertFalse( $status->isOK() );
1444  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1445 
1446  $blockOptions = [
1447  'address' => '127.0.0.0/24',
1448  'by' => $this->getTestSysop()->getUser()->getId(),
1449  'reason' => __METHOD__,
1450  'expiry' => time() + 100500,
1451  'createAccount' => true,
1452  ];
1453  $block = new \Block( $blockOptions );
1454  $block->insert();
1455  $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1456  $status = $this->manager->checkAccountCreatePermissions( new \User );
1457  $this->assertFalse( $status->isOK() );
1458  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1459  ScopedCallback::consume( $scopeVariable );
1460 
1461  $this->setMwGlobals( [
1462  'wgEnableDnsBlacklist' => true,
1463  'wgDnsBlacklistUrls' => [
1464  'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1465  ],
1466  'wgProxyWhitelist' => [],
1467  ] );
1468  $status = $this->manager->checkAccountCreatePermissions( new \User );
1469  $this->assertFalse( $status->isOK() );
1470  $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1471  $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1472  $status = $this->manager->checkAccountCreatePermissions( new \User );
1473  $this->assertTrue( $status->isGood() );
1474  }
1475 
1480  private static function usernameForCreation( $uniq = '' ) {
1481  $i = 0;
1482  do {
1483  $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1484  } while ( \User::newFromName( $username )->getId() !== 0 );
1485  return $username;
1486  }
1487 
1488  public function testCanCreateAccount() {
1490  $this->initializeManager();
1491 
1492  $this->assertEquals(
1493  \Status::newFatal( 'authmanager-create-disabled' ),
1494  $this->manager->canCreateAccount( $username )
1495  );
1496 
1497  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1498  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1499  $mock->expects( $this->any() )->method( 'accountCreationType' )
1500  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1501  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1502  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1503  ->will( $this->returnValue( StatusValue::newGood() ) );
1504  $this->primaryauthMocks = [ $mock ];
1505  $this->initializeManager( true );
1506 
1507  $this->assertEquals(
1508  \Status::newFatal( 'userexists' ),
1509  $this->manager->canCreateAccount( $username )
1510  );
1511 
1512  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1513  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1514  $mock->expects( $this->any() )->method( 'accountCreationType' )
1515  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1516  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1517  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1518  ->will( $this->returnValue( StatusValue::newGood() ) );
1519  $this->primaryauthMocks = [ $mock ];
1520  $this->initializeManager( true );
1521 
1522  $this->assertEquals(
1523  \Status::newFatal( 'noname' ),
1524  $this->manager->canCreateAccount( $username . '<>' )
1525  );
1526 
1527  $this->assertEquals(
1528  \Status::newFatal( 'userexists' ),
1529  $this->manager->canCreateAccount( 'UTSysop' )
1530  );
1531 
1532  $this->assertEquals(
1533  \Status::newGood(),
1534  $this->manager->canCreateAccount( $username )
1535  );
1536 
1537  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1538  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1539  $mock->expects( $this->any() )->method( 'accountCreationType' )
1540  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1541  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1542  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1543  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1544  $this->primaryauthMocks = [ $mock ];
1545  $this->initializeManager( true );
1546 
1547  $this->assertEquals(
1548  \Status::newFatal( 'fail' ),
1549  $this->manager->canCreateAccount( $username )
1550  );
1551  }
1552 
1553  public function testBeginAccountCreation() {
1554  $creator = \User::newFromName( 'UTSysop' );
1555  $userReq = new UsernameAuthenticationRequest;
1556  $this->logger = new \TestLogger( false, function ( $message, $level ) {
1557  return $level === LogLevel::DEBUG ? null : $message;
1558  } );
1559  $this->initializeManager();
1560 
1561  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1562  $this->hook( 'LocalUserCreated', $this->never() );
1563  try {
1564  $this->manager->beginAccountCreation(
1565  $creator, [], 'http://localhost/'
1566  );
1567  $this->fail( 'Expected exception not thrown' );
1568  } catch ( \LogicException $ex ) {
1569  $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1570  }
1571  $this->unhook( 'LocalUserCreated' );
1572  $this->assertNull(
1573  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1574  );
1575 
1576  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1577  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1578  $mock->expects( $this->any() )->method( 'accountCreationType' )
1579  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1580  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1581  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1582  ->will( $this->returnValue( StatusValue::newGood() ) );
1583  $this->primaryauthMocks = [ $mock ];
1584  $this->initializeManager( true );
1585 
1586  $this->hook( 'LocalUserCreated', $this->never() );
1587  $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
1588  $this->unhook( 'LocalUserCreated' );
1589  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1590  $this->assertSame( 'noname', $ret->message->getKey() );
1591 
1592  $this->hook( 'LocalUserCreated', $this->never() );
1593  $userReq->username = self::usernameForCreation();
1594  $userReq2 = new UsernameAuthenticationRequest;
1595  $userReq2->username = $userReq->username . 'X';
1596  $ret = $this->manager->beginAccountCreation(
1597  $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1598  );
1599  $this->unhook( 'LocalUserCreated' );
1600  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1601  $this->assertSame( 'noname', $ret->message->getKey() );
1602 
1603  $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1604  $readOnlyMode->setReason( 'Because' );
1605  $this->hook( 'LocalUserCreated', $this->never() );
1606  $userReq->username = self::usernameForCreation();
1607  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1608  $this->unhook( 'LocalUserCreated' );
1609  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1610  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1611  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1612  $readOnlyMode->setReason( false );
1613 
1614  $this->hook( 'LocalUserCreated', $this->never() );
1615  $userReq->username = self::usernameForCreation();
1616  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1617  $this->unhook( 'LocalUserCreated' );
1618  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1619  $this->assertSame( 'userexists', $ret->message->getKey() );
1620 
1621  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1622  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1623  $mock->expects( $this->any() )->method( 'accountCreationType' )
1624  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1625  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1626  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1627  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1628  $this->primaryauthMocks = [ $mock ];
1629  $this->initializeManager( true );
1630 
1631  $this->hook( 'LocalUserCreated', $this->never() );
1632  $userReq->username = self::usernameForCreation();
1633  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1634  $this->unhook( 'LocalUserCreated' );
1635  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1636  $this->assertSame( 'fail', $ret->message->getKey() );
1637 
1638  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1639  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1640  $mock->expects( $this->any() )->method( 'accountCreationType' )
1641  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1642  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1643  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1644  ->will( $this->returnValue( StatusValue::newGood() ) );
1645  $this->primaryauthMocks = [ $mock ];
1646  $this->initializeManager( true );
1647 
1648  $this->hook( 'LocalUserCreated', $this->never() );
1649  $userReq->username = self::usernameForCreation() . '<>';
1650  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1651  $this->unhook( 'LocalUserCreated' );
1652  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1653  $this->assertSame( 'noname', $ret->message->getKey() );
1654 
1655  $this->hook( 'LocalUserCreated', $this->never() );
1656  $userReq->username = $creator->getName();
1657  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1658  $this->unhook( 'LocalUserCreated' );
1659  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1660  $this->assertSame( 'userexists', $ret->message->getKey() );
1661 
1662  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1663  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1664  $mock->expects( $this->any() )->method( 'accountCreationType' )
1665  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1666  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1667  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1668  ->will( $this->returnValue( StatusValue::newGood() ) );
1669  $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1670  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1671  $this->primaryauthMocks = [ $mock ];
1672  $this->initializeManager( true );
1673 
1674  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1675  ->setMethods( [ 'populateUser' ] )
1676  ->getMock();
1677  $req->expects( $this->any() )->method( 'populateUser' )
1678  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1679  $userReq->username = self::usernameForCreation();
1680  $ret = $this->manager->beginAccountCreation(
1681  $creator, [ $userReq, $req ], 'http://localhost/'
1682  );
1683  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1684  $this->assertSame( 'populatefail', $ret->message->getKey() );
1685 
1687  $userReq->username = self::usernameForCreation();
1688 
1689  $ret = $this->manager->beginAccountCreation(
1690  $creator, [ $userReq, $req ], 'http://localhost/'
1691  );
1692  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1693  $this->assertSame( 'fail', $ret->message->getKey() );
1694 
1695  $this->manager->beginAccountCreation(
1696  \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1697  );
1698  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1699  $this->assertSame( 'fail', $ret->message->getKey() );
1700  }
1701 
1702  public function testContinueAccountCreation() {
1703  $creator = \User::newFromName( 'UTSysop' );
1705  $this->logger = new \TestLogger( false, function ( $message, $level ) {
1706  return $level === LogLevel::DEBUG ? null : $message;
1707  } );
1708  $this->initializeManager();
1709 
1710  $session = [
1711  'userid' => 0,
1712  'username' => $username,
1713  'creatorid' => 0,
1714  'creatorname' => $username,
1715  'reqs' => [],
1716  'primary' => null,
1717  'primaryResponse' => null,
1718  'secondary' => [],
1719  'ranPreTests' => true,
1720  ];
1721 
1722  $this->hook( 'LocalUserCreated', $this->never() );
1723  try {
1724  $this->manager->continueAccountCreation( [] );
1725  $this->fail( 'Expected exception not thrown' );
1726  } catch ( \LogicException $ex ) {
1727  $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1728  }
1729  $this->unhook( 'LocalUserCreated' );
1730 
1731  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1732  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1733  $mock->expects( $this->any() )->method( 'accountCreationType' )
1734  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1735  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1736  $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1737  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1738  );
1739  $this->primaryauthMocks = [ $mock ];
1740  $this->initializeManager( true );
1741 
1742  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1743  $this->hook( 'LocalUserCreated', $this->never() );
1744  $ret = $this->manager->continueAccountCreation( [] );
1745  $this->unhook( 'LocalUserCreated' );
1746  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1747  $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1748 
1749  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1750  [ 'username' => "$username<>" ] + $session );
1751  $this->hook( 'LocalUserCreated', $this->never() );
1752  $ret = $this->manager->continueAccountCreation( [] );
1753  $this->unhook( 'LocalUserCreated' );
1754  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1755  $this->assertSame( 'noname', $ret->message->getKey() );
1756  $this->assertNull(
1757  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1758  );
1759 
1760  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1761  $this->hook( 'LocalUserCreated', $this->never() );
1763  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1764  $ret = $this->manager->continueAccountCreation( [] );
1765  unset( $lock );
1766  $this->unhook( 'LocalUserCreated' );
1767  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1768  $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1769  // This error shouldn't remove the existing session, because the
1770  // raced-with process "owns" it.
1771  $this->assertSame(
1772  $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1773  );
1774 
1775  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1776  [ 'username' => $creator->getName() ] + $session );
1777  $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1778  $readOnlyMode->setReason( 'Because' );
1779  $this->hook( 'LocalUserCreated', $this->never() );
1780  $ret = $this->manager->continueAccountCreation( [] );
1781  $this->unhook( 'LocalUserCreated' );
1782  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1783  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1784  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1785  $readOnlyMode->setReason( false );
1786 
1787  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1788  [ 'username' => $creator->getName() ] + $session );
1789  $this->hook( 'LocalUserCreated', $this->never() );
1790  $ret = $this->manager->continueAccountCreation( [] );
1791  $this->unhook( 'LocalUserCreated' );
1792  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1793  $this->assertSame( 'userexists', $ret->message->getKey() );
1794  $this->assertNull(
1795  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1796  );
1797 
1798  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1799  [ 'userid' => $creator->getId() ] + $session );
1800  $this->hook( 'LocalUserCreated', $this->never() );
1801  try {
1802  $ret = $this->manager->continueAccountCreation( [] );
1803  $this->fail( 'Expected exception not thrown' );
1804  } catch ( \UnexpectedValueException $ex ) {
1805  $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1806  }
1807  $this->unhook( 'LocalUserCreated' );
1808  $this->assertNull(
1809  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1810  );
1811 
1812  $id = $creator->getId();
1813  $name = $creator->getName();
1814  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1815  [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1816  $this->hook( 'LocalUserCreated', $this->never() );
1817  try {
1818  $ret = $this->manager->continueAccountCreation( [] );
1819  $this->fail( 'Expected exception not thrown' );
1820  } catch ( \UnexpectedValueException $ex ) {
1821  $this->assertEquals(
1822  "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
1823  );
1824  }
1825  $this->unhook( 'LocalUserCreated' );
1826  $this->assertNull(
1827  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1828  );
1829 
1830  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1831  ->setMethods( [ 'populateUser' ] )
1832  ->getMock();
1833  $req->expects( $this->any() )->method( 'populateUser' )
1834  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1835  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1836  [ 'reqs' => [ $req ] ] + $session );
1837  $ret = $this->manager->continueAccountCreation( [] );
1838  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1839  $this->assertSame( 'populatefail', $ret->message->getKey() );
1840  $this->assertNull(
1841  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1842  );
1843  }
1844 
1854  public function testAccountCreation(
1855  StatusValue $preTest, $primaryTest, $secondaryTest,
1856  array $primaryResponses, array $secondaryResponses, array $managerResponses
1857  ) {
1858  $creator = \User::newFromName( 'UTSysop' );
1860 
1861  $this->initializeManager();
1862 
1863  // Set up lots of mocks...
1864  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1865  $req->preTest = $preTest;
1866  $req->primaryTest = $primaryTest;
1867  $req->secondaryTest = $secondaryTest;
1868  $req->primary = $primaryResponses;
1869  $req->secondary = $secondaryResponses;
1870  $mocks = [];
1871  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1872  $class = ucfirst( $key ) . 'AuthenticationProvider';
1873  $mocks[$key] = $this->getMockForAbstractClass(
1874  "MediaWiki\\Auth\\$class", [], "Mock$class"
1875  );
1876  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1877  ->will( $this->returnValue( $key ) );
1878  $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1879  ->will( $this->returnValue( StatusValue::newGood() ) );
1880  $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1881  ->will( $this->returnCallback(
1882  function ( $user, $creatorIn, $reqs )
1883  use ( $username, $creator, $req, $key )
1884  {
1885  $this->assertSame( $username, $user->getName() );
1886  $this->assertSame( $creator->getId(), $creatorIn->getId() );
1887  $this->assertSame( $creator->getName(), $creatorIn->getName() );
1888  $foundReq = false;
1889  foreach ( $reqs as $r ) {
1890  $this->assertSame( $username, $r->username );
1891  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1892  }
1893  $this->assertTrue( $foundReq, '$reqs contains $req' );
1894  $k = $key . 'Test';
1895  return $req->$k;
1896  }
1897  ) );
1898 
1899  for ( $i = 2; $i <= 3; $i++ ) {
1900  $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1901  $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1902  ->will( $this->returnValue( $key . $i ) );
1903  $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1904  ->will( $this->returnValue( StatusValue::newGood() ) );
1905  $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1906  ->will( $this->returnValue( StatusValue::newGood() ) );
1907  }
1908  }
1909 
1910  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1911  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1912  $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1913  ->will( $this->returnValue( false ) );
1914  $ct = count( $req->primary );
1915  $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1916  $this->assertSame( $username, $user->getName() );
1917  $this->assertSame( 'UTSysop', $creator->getName() );
1918  $foundReq = false;
1919  foreach ( $reqs as $r ) {
1920  $this->assertSame( $username, $r->username );
1921  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1922  }
1923  $this->assertTrue( $foundReq, '$reqs contains $req' );
1924  return array_shift( $req->primary );
1925  } );
1926  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1927  ->method( 'beginPrimaryAccountCreation' )
1928  ->will( $callback );
1929  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1930  ->method( 'continuePrimaryAccountCreation' )
1931  ->will( $callback );
1932 
1933  $ct = count( $req->secondary );
1934  $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1935  $this->assertSame( $username, $user->getName() );
1936  $this->assertSame( 'UTSysop', $creator->getName() );
1937  $foundReq = false;
1938  foreach ( $reqs as $r ) {
1939  $this->assertSame( $username, $r->username );
1940  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1941  }
1942  $this->assertTrue( $foundReq, '$reqs contains $req' );
1943  return array_shift( $req->secondary );
1944  } );
1945  $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1946  ->method( 'beginSecondaryAccountCreation' )
1947  ->will( $callback );
1948  $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1949  ->method( 'continueSecondaryAccountCreation' )
1950  ->will( $callback );
1951 
1953  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1954  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
1955  $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1956  ->will( $this->returnValue( false ) );
1957  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1958  ->will( $this->returnValue( $abstain ) );
1959  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1960  $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1961  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
1962  $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1963  ->will( $this->returnValue( false ) );
1964  $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1965  $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1966  $mocks['secondary2']->expects( $this->atMost( 1 ) )
1967  ->method( 'beginSecondaryAccountCreation' )
1968  ->will( $this->returnValue( $abstain ) );
1969  $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1970  $mocks['secondary3']->expects( $this->atMost( 1 ) )
1971  ->method( 'beginSecondaryAccountCreation' )
1972  ->will( $this->returnValue( $abstain ) );
1973  $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1974 
1975  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
1976  $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1977  $this->secondaryauthMocks = [
1978  $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1979  ];
1980 
1981  $this->logger = new \TestLogger( true, function ( $message, $level ) {
1982  return $level === LogLevel::DEBUG ? null : $message;
1983  } );
1984  $expectLog = [];
1985  $this->initializeManager( true );
1986 
1987  $constraint = \PHPUnit_Framework_Assert::logicalOr(
1988  $this->equalTo( AuthenticationResponse::PASS ),
1989  $this->equalTo( AuthenticationResponse::FAIL )
1990  );
1991  $providers = array_merge(
1992  $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
1993  );
1994  foreach ( $providers as $p ) {
1995  $p->postCalled = false;
1996  $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
1997  ->willReturnCallback( function ( $user, $creator, $response )
1998  use ( $constraint, $p, $username )
1999  {
2000  $this->assertInstanceOf( \User::class, $user );
2001  $this->assertSame( $username, $user->getName() );
2002  $this->assertSame( 'UTSysop', $creator->getName() );
2003  $this->assertInstanceOf( AuthenticationResponse::class, $response );
2004  $this->assertThat( $response->status, $constraint );
2005  $p->postCalled = $response->status;
2006  } );
2007  }
2008 
2009  // We're testing with $wgNewUserLog = false, so assert that it worked
2010  $dbw = wfGetDB( DB_MASTER );
2011  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2012 
2013  $first = true;
2014  $created = false;
2015  foreach ( $managerResponses as $i => $response ) {
2016  $success = $response instanceof AuthenticationResponse &&
2018  if ( $i === 'created' ) {
2019  $created = true;
2020  $this->hook( 'LocalUserCreated', $this->once() )
2021  ->with(
2022  $this->callback( function ( $user ) use ( $username ) {
2023  return $user->getName() === $username;
2024  } ),
2025  $this->equalTo( false )
2026  );
2027  $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2028  } else {
2029  $this->hook( 'LocalUserCreated', $this->never() );
2030  }
2031 
2032  $ex = null;
2033  try {
2034  if ( $first ) {
2035  $userReq = new UsernameAuthenticationRequest;
2036  $userReq->username = $username;
2037  $ret = $this->manager->beginAccountCreation(
2038  $creator, [ $userReq, $req ], 'http://localhost/'
2039  );
2040  } else {
2041  $ret = $this->manager->continueAccountCreation( [ $req ] );
2042  }
2043  if ( $response instanceof \Exception ) {
2044  $this->fail( 'Expected exception not thrown', "Response $i" );
2045  }
2046  } catch ( \Exception $ex ) {
2047  if ( !$response instanceof \Exception ) {
2048  throw $ex;
2049  }
2050  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2051  $this->assertNull(
2052  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2053  "Response $i, exception, session state"
2054  );
2055  $this->unhook( 'LocalUserCreated' );
2056  return;
2057  }
2058 
2059  $this->unhook( 'LocalUserCreated' );
2060 
2061  $this->assertSame( 'http://localhost/', $req->returnToUrl );
2062 
2063  if ( $success ) {
2064  $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2065  $this->assertContains(
2066  $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2067  "Response $i, login marker"
2068  );
2069 
2070  $expectLog[] = [
2071  LogLevel::INFO,
2072  "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2073  ];
2074 
2075  // Set some fields in the expected $response that we couldn't
2076  // know in provideAccountCreation().
2077  $response->username = $username;
2078  $response->loginRequest = $ret->loginRequest;
2079  } else {
2080  $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
2081  $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
2082  "Response $i, login marker" );
2083  }
2084  $ret->message = $this->message( $ret->message );
2085  $this->assertEquals( $response, $ret, "Response $i, response" );
2086  if ( $success || $response->status === AuthenticationResponse::FAIL ) {
2087  $this->assertNull(
2088  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2089  "Response $i, session state"
2090  );
2091  foreach ( $providers as $p ) {
2092  $this->assertSame( $response->status, $p->postCalled,
2093  "Response $i, post-auth callback called" );
2094  }
2095  } else {
2096  $this->assertNotNull(
2097  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2098  "Response $i, session state"
2099  );
2100  foreach ( $ret->neededRequests as $neededReq ) {
2101  $this->assertEquals( AuthManager::ACTION_CREATE, $neededReq->action,
2102  "Response $i, neededRequest action" );
2103  }
2104  $this->assertEquals(
2105  $ret->neededRequests,
2106  $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
2107  "Response $i, continuation check"
2108  );
2109  foreach ( $providers as $p ) {
2110  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
2111  }
2112  }
2113 
2114  if ( $created ) {
2115  $this->assertNotEquals( 0, \User::idFromName( $username ) );
2116  } else {
2117  $this->assertEquals( 0, \User::idFromName( $username ) );
2118  }
2119 
2120  $first = false;
2121  }
2122 
2123  $this->assertSame( $expectLog, $this->logger->getBuffer() );
2124 
2125  $this->assertSame(
2126  $maxLogId,
2127  $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2128  );
2129  }
2130 
2131  public function provideAccountCreation() {
2132  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2133  $good = StatusValue::newGood();
2134 
2135  return [
2136  'Pre-creation test fail in pre' => [
2137  StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
2138  [],
2139  [],
2140  [
2141  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
2142  ]
2143  ],
2144  'Pre-creation test fail in primary' => [
2145  $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
2146  [],
2147  [],
2148  [
2149  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2150  ]
2151  ],
2152  'Pre-creation test fail in secondary' => [
2153  $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
2154  [],
2155  [],
2156  [
2157  AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
2158  ]
2159  ],
2160  'Failure in primary' => [
2161  $good, $good, $good,
2162  $tmp = [
2163  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2164  ],
2165  [],
2166  $tmp
2167  ],
2168  'All primary abstain' => [
2169  $good, $good, $good,
2170  [
2172  ],
2173  [],
2174  [
2175  AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
2176  ]
2177  ],
2178  'Primary UI, then redirect, then fail' => [
2179  $good, $good, $good,
2180  $tmp = [
2181  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2182  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2183  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2184  ],
2185  [],
2186  $tmp
2187  ],
2188  'Primary redirect, then abstain' => [
2189  $good, $good, $good,
2190  [
2192  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2193  ),
2195  ],
2196  [],
2197  [
2198  $tmp,
2199  new \DomainException(
2200  'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2201  )
2202  ]
2203  ],
2204  'Primary UI, then pass; secondary abstain' => [
2205  $good, $good, $good,
2206  [
2207  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2209  ],
2210  [
2212  ],
2213  [
2214  $tmp1,
2215  'created' => AuthenticationResponse::newPass( '' ),
2216  ]
2217  ],
2218  'Primary pass; secondary UI then pass' => [
2219  $good, $good, $good,
2220  [
2222  ],
2223  [
2224  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2226  ],
2227  [
2228  'created' => $tmp1,
2230  ]
2231  ],
2232  'Primary pass; secondary fail' => [
2233  $good, $good, $good,
2234  [
2236  ],
2237  [
2238  AuthenticationResponse::newFail( $this->message( '...' ) ),
2239  ],
2240  [
2241  'created' => new \DomainException(
2242  'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2243  'Secondary providers are not allowed to fail account creation, ' .
2244  'that should have been done via testForAccountCreation().'
2245  )
2246  ]
2247  ],
2248  ];
2249  }
2250 
2256  public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2257  $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
2259 
2260  $this->initializeManager();
2261 
2262  // Set up lots of mocks...
2263  $mock = $this->getMockForAbstractClass(
2265  );
2266  $mock->expects( $this->any() )->method( 'getUniqueId' )
2267  ->will( $this->returnValue( 'primary' ) );
2268  $mock->expects( $this->any() )->method( 'testUserForCreation' )
2269  ->will( $this->returnValue( StatusValue::newGood() ) );
2270  $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2271  ->will( $this->returnValue( StatusValue::newGood() ) );
2272  $mock->expects( $this->any() )->method( 'accountCreationType' )
2273  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2274  $mock->expects( $this->any() )->method( 'testUserExists' )
2275  ->will( $this->returnValue( false ) );
2276  $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2277  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
2278  $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2279  ->will( $this->returnValue( $logSubtype ) );
2280 
2281  $this->primaryauthMocks = [ $mock ];
2282  $this->initializeManager( true );
2283  $this->logger->setCollect( true );
2284 
2285  $this->config->set( 'NewUserLog', true );
2286 
2287  $dbw = wfGetDB( DB_MASTER );
2288  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2289 
2290  $userReq = new UsernameAuthenticationRequest;
2291  $userReq->username = $username;
2292  $reasonReq = new CreationReasonAuthenticationRequest;
2293  $reasonReq->reason = $this->toString();
2294  $ret = $this->manager->beginAccountCreation(
2295  $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2296  );
2297 
2298  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
2299 
2301  $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2302  $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2303 
2305  $rows = iterator_to_array( $dbw->select(
2306  $data['tables'],
2307  $data['fields'],
2308  [
2309  'log_id > ' . (int)$maxLogId,
2310  'log_type' => 'newusers'
2311  ] + $data['conds'],
2312  __METHOD__,
2313  $data['options'],
2314  $data['join_conds']
2315  ) );
2316  $this->assertCount( 1, $rows );
2317  $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2318 
2319  $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
2320  $this->assertSame(
2321  $isAnon ? $user->getId() : $creator->getId(),
2322  $entry->getPerformer()->getId()
2323  );
2324  $this->assertSame(
2325  $isAnon ? $user->getName() : $creator->getName(),
2326  $entry->getPerformer()->getName()
2327  );
2328  $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2329  $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2330  $this->assertSame( $this->toString(), $entry->getComment() );
2331  }
2332 
2333  public static function provideAccountCreationLogging() {
2334  return [
2335  [ true, null ],
2336  [ true, 'foobar' ],
2337  [ false, null ],
2338  [ false, 'byemail' ],
2339  ];
2340  }
2341 
2342  public function testAutoAccountCreation() {
2344 
2345  // PHPUnit seems to have a bug where it will call the ->with()
2346  // callbacks for our hooks again after the test is run (WTF?), which
2347  // breaks here because $username no longer matches $user by the end of
2348  // the testing.
2349  $workaroundPHPUnitBug = false;
2350 
2352  $this->initializeManager();
2353 
2354  $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2355  $wgGroupPermissions['*']['createaccount'] = true;
2356  $wgGroupPermissions['*']['autocreateaccount'] = false;
2357 
2358  \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
2359  $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2360 
2361  // Set up lots of mocks...
2362  $mocks = [];
2363  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2364  $class = ucfirst( $key ) . 'AuthenticationProvider';
2365  $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2366  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2367  ->will( $this->returnValue( $key ) );
2368  }
2369 
2370  $good = StatusValue::newGood();
2371  $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2372  return $workaroundPHPUnitBug || $user->getName() === $username;
2373  } );
2374 
2375  $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2376  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2377  ->will( $this->onConsecutiveCalls(
2378  StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
2379  StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2380  $good, // backoff test
2381  $good, // addToDatabase fails test
2382  $good, // addToDatabase throws test
2383  $good, // addToDatabase exists test
2384  $good, $good, $good // success
2385  ) );
2386 
2387  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2388  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2389  $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2390  ->will( $this->returnValue( true ) );
2391  $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2392  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2393  ->will( $this->onConsecutiveCalls(
2394  StatusValue::newFatal( 'fail-in-primary' ), $good,
2395  $good, // backoff test
2396  $good, // addToDatabase fails test
2397  $good, // addToDatabase throws test
2398  $good, // addToDatabase exists test
2399  $good, $good, $good
2400  ) );
2401  $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2402  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2403 
2404  $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2405  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2406  ->will( $this->onConsecutiveCalls(
2407  StatusValue::newFatal( 'fail-in-secondary' ),
2408  $good, // backoff test
2409  $good, // addToDatabase fails test
2410  $good, // addToDatabase throws test
2411  $good, // addToDatabase exists test
2412  $good, $good, $good
2413  ) );
2414  $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2415  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2416 
2417  $this->preauthMocks = [ $mocks['pre'] ];
2418  $this->primaryauthMocks = [ $mocks['primary'] ];
2419  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2420  $this->initializeManager( true );
2421  $session = $this->request->getSession();
2422 
2423  $logger = new \TestLogger( true, function ( $m ) {
2424  $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2425  return $m;
2426  } );
2427  $this->manager->setLogger( $logger );
2428 
2429  try {
2430  $user = \User::newFromName( 'UTSysop' );
2431  $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2432  $this->fail( 'Expected exception not thrown' );
2433  } catch ( \InvalidArgumentException $ex ) {
2434  $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2435  }
2436 
2437  // First, check an existing user
2438  $session->clear();
2439  $user = \User::newFromName( 'UTSysop' );
2440  $this->hook( 'LocalUserCreated', $this->never() );
2441  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2442  $this->unhook( 'LocalUserCreated' );
2443  $expect = \Status::newGood();
2444  $expect->warning( 'userexists' );
2445  $this->assertEquals( $expect, $ret );
2446  $this->assertNotEquals( 0, $user->getId() );
2447  $this->assertSame( 'UTSysop', $user->getName() );
2448  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2449  $this->assertSame( [
2450  [ LogLevel::DEBUG, '{username} already exists locally' ],
2451  ], $logger->getBuffer() );
2452  $logger->clearBuffer();
2453 
2454  $session->clear();
2455  $user = \User::newFromName( 'UTSysop' );
2456  $this->hook( 'LocalUserCreated', $this->never() );
2457  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2458  $this->unhook( 'LocalUserCreated' );
2459  $expect = \Status::newGood();
2460  $expect->warning( 'userexists' );
2461  $this->assertEquals( $expect, $ret );
2462  $this->assertNotEquals( 0, $user->getId() );
2463  $this->assertSame( 'UTSysop', $user->getName() );
2464  $this->assertEquals( 0, $session->getUser()->getId() );
2465  $this->assertSame( [
2466  [ LogLevel::DEBUG, '{username} already exists locally' ],
2467  ], $logger->getBuffer() );
2468  $logger->clearBuffer();
2469 
2470  // Wiki is read-only
2471  $session->clear();
2472  $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
2473  $readOnlyMode->setReason( 'Because' );
2475  $this->hook( 'LocalUserCreated', $this->never() );
2476  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2477  $this->unhook( 'LocalUserCreated' );
2478  $this->assertEquals( \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2479  $this->assertEquals( 0, $user->getId() );
2480  $this->assertNotEquals( $username, $user->getName() );
2481  $this->assertEquals( 0, $session->getUser()->getId() );
2482  $this->assertSame( [
2483  [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2484  ], $logger->getBuffer() );
2485  $logger->clearBuffer();
2486  $readOnlyMode->setReason( false );
2487 
2488  // Session blacklisted
2489  $session->clear();
2490  $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2492  $this->hook( 'LocalUserCreated', $this->never() );
2493  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2494  $this->unhook( 'LocalUserCreated' );
2495  $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2496  $this->assertEquals( 0, $user->getId() );
2497  $this->assertNotEquals( $username, $user->getName() );
2498  $this->assertEquals( 0, $session->getUser()->getId() );
2499  $this->assertSame( [
2500  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2501  ], $logger->getBuffer() );
2502  $logger->clearBuffer();
2503 
2504  $session->clear();
2505  $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2507  $this->hook( 'LocalUserCreated', $this->never() );
2508  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2509  $this->unhook( 'LocalUserCreated' );
2510  $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2511  $this->assertEquals( 0, $user->getId() );
2512  $this->assertNotEquals( $username, $user->getName() );
2513  $this->assertEquals( 0, $session->getUser()->getId() );
2514  $this->assertSame( [
2515  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2516  ], $logger->getBuffer() );
2517  $logger->clearBuffer();
2518 
2519  // Uncreatable name
2520  $session->clear();
2521  $user = \User::newFromName( $username . '@' );
2522  $this->hook( 'LocalUserCreated', $this->never() );
2523  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2524  $this->unhook( 'LocalUserCreated' );
2525  $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2526  $this->assertEquals( 0, $user->getId() );
2527  $this->assertNotEquals( $username . '@', $user->getId() );
2528  $this->assertEquals( 0, $session->getUser()->getId() );
2529  $this->assertSame( [
2530  [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2531  ], $logger->getBuffer() );
2532  $logger->clearBuffer();
2533  $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2534 
2535  // IP unable to create accounts
2536  $wgGroupPermissions['*']['createaccount'] = false;
2537  $wgGroupPermissions['*']['autocreateaccount'] = false;
2538  $session->clear();
2540  $this->hook( 'LocalUserCreated', $this->never() );
2541  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2542  $this->unhook( 'LocalUserCreated' );
2543  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2544  $this->assertEquals( 0, $user->getId() );
2545  $this->assertNotEquals( $username, $user->getName() );
2546  $this->assertEquals( 0, $session->getUser()->getId() );
2547  $this->assertSame( [
2548  [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2549  ], $logger->getBuffer() );
2550  $logger->clearBuffer();
2551  $this->assertSame(
2552  'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2553  );
2554 
2555  // Test that both permutations of permissions are allowed
2556  // (this hits the two "ok" entries in $mocks['pre'])
2557  $wgGroupPermissions['*']['createaccount'] = false;
2558  $wgGroupPermissions['*']['autocreateaccount'] = true;
2559  $session->clear();
2561  $this->hook( 'LocalUserCreated', $this->never() );
2562  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2563  $this->unhook( 'LocalUserCreated' );
2564  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2565 
2566  $wgGroupPermissions['*']['createaccount'] = true;
2567  $wgGroupPermissions['*']['autocreateaccount'] = false;
2568  $session->clear();
2570  $this->hook( 'LocalUserCreated', $this->never() );
2571  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2572  $this->unhook( 'LocalUserCreated' );
2573  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2574  $logger->clearBuffer();
2575 
2576  // Test lock fail
2577  $session->clear();
2579  $this->hook( 'LocalUserCreated', $this->never() );
2581  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2582  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2583  unset( $lock );
2584  $this->unhook( 'LocalUserCreated' );
2585  $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2586  $this->assertEquals( 0, $user->getId() );
2587  $this->assertNotEquals( $username, $user->getName() );
2588  $this->assertEquals( 0, $session->getUser()->getId() );
2589  $this->assertSame( [
2590  [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2591  ], $logger->getBuffer() );
2592  $logger->clearBuffer();
2593 
2594  // Test pre-authentication provider fail
2595  $session->clear();
2597  $this->hook( 'LocalUserCreated', $this->never() );
2598  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2599  $this->unhook( 'LocalUserCreated' );
2600  $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2601  $this->assertEquals( 0, $user->getId() );
2602  $this->assertNotEquals( $username, $user->getName() );
2603  $this->assertEquals( 0, $session->getUser()->getId() );
2604  $this->assertSame( [
2605  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2606  ], $logger->getBuffer() );
2607  $logger->clearBuffer();
2608  $this->assertEquals(
2609  StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2610  );
2611 
2612  $session->clear();
2614  $this->hook( 'LocalUserCreated', $this->never() );
2615  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2616  $this->unhook( 'LocalUserCreated' );
2617  $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2618  $this->assertEquals( 0, $user->getId() );
2619  $this->assertNotEquals( $username, $user->getName() );
2620  $this->assertEquals( 0, $session->getUser()->getId() );
2621  $this->assertSame( [
2622  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2623  ], $logger->getBuffer() );
2624  $logger->clearBuffer();
2625  $this->assertEquals(
2626  StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2627  );
2628 
2629  $session->clear();
2631  $this->hook( 'LocalUserCreated', $this->never() );
2632  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2633  $this->unhook( 'LocalUserCreated' );
2634  $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2635  $this->assertEquals( 0, $user->getId() );
2636  $this->assertNotEquals( $username, $user->getName() );
2637  $this->assertEquals( 0, $session->getUser()->getId() );
2638  $this->assertSame( [
2639  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2640  ], $logger->getBuffer() );
2641  $logger->clearBuffer();
2642  $this->assertEquals(
2643  StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2644  );
2645 
2646  // Test backoff
2648  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2649  $cache->set( $backoffKey, true );
2650  $session->clear();
2652  $this->hook( 'LocalUserCreated', $this->never() );
2653  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2654  $this->unhook( 'LocalUserCreated' );
2655  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2656  $this->assertEquals( 0, $user->getId() );
2657  $this->assertNotEquals( $username, $user->getName() );
2658  $this->assertEquals( 0, $session->getUser()->getId() );
2659  $this->assertSame( [
2660  [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2661  ], $logger->getBuffer() );
2662  $logger->clearBuffer();
2663  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2664  $cache->delete( $backoffKey );
2665 
2666  // Test addToDatabase fails
2667  $session->clear();
2668  $user = $this->getMockBuilder( \User::class )
2669  ->setMethods( [ 'addToDatabase' ] )->getMock();
2670  $user->expects( $this->once() )->method( 'addToDatabase' )
2671  ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2672  $user->setName( $username );
2673  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2674  $this->assertEquals( \Status::newFatal( 'because' ), $ret );
2675  $this->assertEquals( 0, $user->getId() );
2676  $this->assertNotEquals( $username, $user->getName() );
2677  $this->assertEquals( 0, $session->getUser()->getId() );
2678  $this->assertSame( [
2679  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2680  [ LogLevel::ERROR, '{username} failed with message {msg}' ],
2681  ], $logger->getBuffer() );
2682  $logger->clearBuffer();
2683  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2684 
2685  // Test addToDatabase throws an exception
2687  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2688  $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2689  $session->clear();
2690  $user = $this->getMockBuilder( \User::class )
2691  ->setMethods( [ 'addToDatabase' ] )->getMock();
2692  $user->expects( $this->once() )->method( 'addToDatabase' )
2693  ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2694  $user->setName( $username );
2695  try {
2696  $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2697  $this->fail( 'Expected exception not thrown' );
2698  } catch ( \Exception $ex ) {
2699  $this->assertSame( 'Excepted', $ex->getMessage() );
2700  }
2701  $this->assertEquals( 0, $user->getId() );
2702  $this->assertEquals( 0, $session->getUser()->getId() );
2703  $this->assertSame( [
2704  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2705  [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2706  ], $logger->getBuffer() );
2707  $logger->clearBuffer();
2708  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2709  $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2710  $cache->delete( $backoffKey );
2711 
2712  // Test addToDatabase fails because the user already exists.
2713  $session->clear();
2714  $user = $this->getMockBuilder( \User::class )
2715  ->setMethods( [ 'addToDatabase' ] )->getMock();
2716  $user->expects( $this->once() )->method( 'addToDatabase' )
2717  ->will( $this->returnCallback( function () use ( $username, &$user ) {
2718  $oldUser = \User::newFromName( $username );
2719  $status = $oldUser->addToDatabase();
2720  $this->assertTrue( $status->isOK(), 'sanity check' );
2721  $user->setId( $oldUser->getId() );
2722  return \Status::newFatal( 'userexists' );
2723  } ) );
2724  $user->setName( $username );
2725  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2726  $expect = \Status::newGood();
2727  $expect->warning( 'userexists' );
2728  $this->assertEquals( $expect, $ret );
2729  $this->assertNotEquals( 0, $user->getId() );
2730  $this->assertEquals( $username, $user->getName() );
2731  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2732  $this->assertSame( [
2733  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2734  [ LogLevel::INFO, '{username} already exists locally (race)' ],
2735  ], $logger->getBuffer() );
2736  $logger->clearBuffer();
2737  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2738 
2739  // Success!
2740  $session->clear();
2743  $this->hook( 'AuthPluginAutoCreate', $this->once() )
2744  ->with( $callback );
2745  $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2746  get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2747  $this->hook( 'LocalUserCreated', $this->once() )
2748  ->with( $callback, $this->equalTo( true ) );
2749  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2750  $this->unhook( 'LocalUserCreated' );
2751  $this->unhook( 'AuthPluginAutoCreate' );
2752  $this->assertEquals( \Status::newGood(), $ret );
2753  $this->assertNotEquals( 0, $user->getId() );
2754  $this->assertEquals( $username, $user->getName() );
2755  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2756  $this->assertSame( [
2757  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2758  ], $logger->getBuffer() );
2759  $logger->clearBuffer();
2760 
2761  $dbw = wfGetDB( DB_MASTER );
2762  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2763  $session->clear();
2766  $this->hook( 'LocalUserCreated', $this->once() )
2767  ->with( $callback, $this->equalTo( true ) );
2768  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2769  $this->unhook( 'LocalUserCreated' );
2770  $this->assertEquals( \Status::newGood(), $ret );
2771  $this->assertNotEquals( 0, $user->getId() );
2772  $this->assertEquals( $username, $user->getName() );
2773  $this->assertEquals( 0, $session->getUser()->getId() );
2774  $this->assertSame( [
2775  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2776  ], $logger->getBuffer() );
2777  $logger->clearBuffer();
2778  $this->assertSame(
2779  $maxLogId,
2780  $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2781  );
2782 
2783  $this->config->set( 'NewUserLog', true );
2784  $session->clear();
2787  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2788  $this->assertEquals( \Status::newGood(), $ret );
2789  $logger->clearBuffer();
2790 
2792  $rows = iterator_to_array( $dbw->select(
2793  $data['tables'],
2794  $data['fields'],
2795  [
2796  'log_id > ' . (int)$maxLogId,
2797  'log_type' => 'newusers'
2798  ] + $data['conds'],
2799  __METHOD__,
2800  $data['options'],
2801  $data['join_conds']
2802  ) );
2803  $this->assertCount( 1, $rows );
2804  $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2805 
2806  $this->assertSame( 'autocreate', $entry->getSubtype() );
2807  $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2808  $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2809  $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2810  $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2811 
2812  $workaroundPHPUnitBug = true;
2813  }
2814 
2821  public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2822  $makeReq = function ( $key ) use ( $action ) {
2823  $req = $this->createMock( AuthenticationRequest::class );
2824  $req->expects( $this->any() )->method( 'getUniqueId' )
2825  ->will( $this->returnValue( $key ) );
2827  $req->key = $key;
2828  return $req;
2829  };
2830  $cmpReqs = function ( $a, $b ) {
2831  $ret = strcmp( get_class( $a ), get_class( $b ) );
2832  if ( !$ret ) {
2833  $ret = strcmp( $a->key, $b->key );
2834  }
2835  return $ret;
2836  };
2837 
2838  $good = StatusValue::newGood();
2839 
2840  $mocks = [];
2841  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2842  $class = ucfirst( $key ) . 'AuthenticationProvider';
2843  $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2844  ->setMethods( [
2845  'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2846  ] )
2847  ->getMockForAbstractClass();
2848  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2849  ->will( $this->returnValue( $key ) );
2850  $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2851  ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2852  return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2853  } ) );
2854  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2855  ->will( $this->returnValue( $good ) );
2856  }
2857 
2858  $primaries = [];
2859  foreach ( [
2863  ] as $type ) {
2864  $class = 'PrimaryAuthenticationProvider';
2865  $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2866  ->setMethods( [
2867  'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2868  'providerAllowsAuthenticationDataChange',
2869  ] )
2870  ->getMockForAbstractClass();
2871  $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2872  ->will( $this->returnValue( "primary-$type" ) );
2873  $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2874  ->will( $this->returnValue( $type ) );
2875  $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2876  ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2877  return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2878  } ) );
2879  $mocks["primary-$type"]->expects( $this->any() )
2880  ->method( 'providerAllowsAuthenticationDataChange' )
2881  ->will( $this->returnValue( $good ) );
2882  $this->primaryauthMocks[] = $mocks["primary-$type"];
2883  }
2884 
2885  $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider::class )
2886  ->setMethods( [
2887  'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2888  'providerAllowsAuthenticationDataChange',
2889  ] )
2890  ->getMockForAbstractClass();
2891  $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2892  ->will( $this->returnValue( 'primary2' ) );
2893  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2894  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2895  $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2896  ->will( $this->returnValue( [] ) );
2897  $mocks['primary2']->expects( $this->any() )
2898  ->method( 'providerAllowsAuthenticationDataChange' )
2899  ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2900  return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2901  } ) );
2902  $this->primaryauthMocks[] = $mocks['primary2'];
2903 
2904  $this->preauthMocks = [ $mocks['pre'] ];
2905  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2906  $this->initializeManager( true );
2907 
2908  if ( $state ) {
2909  if ( isset( $state['continueRequests'] ) ) {
2910  $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2911  }
2913  $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2914  } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2915  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2916  } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2917  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2918  }
2919  }
2920 
2921  $expectReqs = array_map( $makeReq, $expect );
2922  if ( $action === AuthManager::ACTION_LOGIN ) {
2924  $req->action = $action;
2926  $expectReqs[] = $req;
2927  } elseif ( $action === AuthManager::ACTION_CREATE ) {
2929  $req->action = $action;
2930  $expectReqs[] = $req;
2932  $req->action = $action;
2934  $expectReqs[] = $req;
2935  }
2936  usort( $expectReqs, $cmpReqs );
2937 
2938  $actual = $this->manager->getAuthenticationRequests( $action );
2939  foreach ( $actual as $req ) {
2940  // Don't test this here.
2942  }
2943  usort( $actual, $cmpReqs );
2944 
2945  $this->assertEquals( $expectReqs, $actual );
2946 
2947  // Test CreationReasonAuthenticationRequest gets returned
2948  if ( $action === AuthManager::ACTION_CREATE ) {
2950  $req->action = $action;
2952  $expectReqs[] = $req;
2953  usort( $expectReqs, $cmpReqs );
2954 
2955  $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2956  foreach ( $actual as $req ) {
2957  // Don't test this here.
2959  }
2960  usort( $actual, $cmpReqs );
2961 
2962  $this->assertEquals( $expectReqs, $actual );
2963  }
2964  }
2965 
2966  public static function provideGetAuthenticationRequests() {
2967  return [
2968  [
2970  [ 'pre-login', 'primary-none-login', 'primary-create-login',
2971  'primary-link-login', 'secondary-login', 'generic' ],
2972  ],
2973  [
2975  [ 'pre-create', 'primary-none-create', 'primary-create-create',
2976  'primary-link-create', 'secondary-create', 'generic' ],
2977  ],
2978  [
2980  [ 'primary-link-link', 'generic' ],
2981  ],
2982  [
2984  [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2985  'secondary-change' ],
2986  ],
2987  [
2989  [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2990  'secondary-remove' ],
2991  ],
2992  [
2994  [ 'primary-link-remove' ],
2995  ],
2996  [
2998  [],
2999  ],
3000  [
3002  $reqs = [ 'continue-login', 'foo', 'bar' ],
3003  [
3004  'continueRequests' => $reqs,
3005  ],
3006  ],
3007  [
3009  [],
3010  ],
3011  [
3013  $reqs = [ 'continue-create', 'foo', 'bar' ],
3014  [
3015  'continueRequests' => $reqs,
3016  ],
3017  ],
3018  [
3020  [],
3021  ],
3022  [
3024  $reqs = [ 'continue-link', 'foo', 'bar' ],
3025  [
3026  'continueRequests' => $reqs,
3027  ],
3028  ],
3029  ];
3030  }
3031 
3033  $makeReq = function ( $key, $required ) {
3034  $req = $this->createMock( AuthenticationRequest::class );
3035  $req->expects( $this->any() )->method( 'getUniqueId' )
3036  ->will( $this->returnValue( $key ) );
3037  $req->action = AuthManager::ACTION_LOGIN;
3038  $req->key = $key;
3039  $req->required = $required;
3040  return $req;
3041  };
3042  $cmpReqs = function ( $a, $b ) {
3043  $ret = strcmp( get_class( $a ), get_class( $b ) );
3044  if ( !$ret ) {
3045  $ret = strcmp( $a->key, $b->key );
3046  }
3047  return $ret;
3048  };
3049 
3050  $good = StatusValue::newGood();
3051 
3052  $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3053  $primary1->expects( $this->any() )->method( 'getUniqueId' )
3054  ->will( $this->returnValue( 'primary1' ) );
3055  $primary1->expects( $this->any() )->method( 'accountCreationType' )
3056  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3057  $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3058  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3059  return [
3060  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3061  $makeReq( "required", AuthenticationRequest::REQUIRED ),
3062  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3063  $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3064  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3065  $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3066  ];
3067  } ) );
3068 
3069  $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3070  $primary2->expects( $this->any() )->method( 'getUniqueId' )
3071  ->will( $this->returnValue( 'primary2' ) );
3072  $primary2->expects( $this->any() )->method( 'accountCreationType' )
3073  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3074  $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3075  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3076  return [
3077  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3078  $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3079  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3080  ];
3081  } ) );
3082 
3083  $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3084  $secondary->expects( $this->any() )->method( 'getUniqueId' )
3085  ->will( $this->returnValue( 'secondary' ) );
3086  $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3087  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3088  return [
3089  $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3090  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3091  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3092  ];
3093  } ) );
3094 
3095  $rememberReq = new RememberMeAuthenticationRequest;
3096  $rememberReq->action = AuthManager::ACTION_LOGIN;
3097 
3098  $this->primaryauthMocks = [ $primary1, $primary2 ];
3099  $this->secondaryauthMocks = [ $secondary ];
3100  $this->initializeManager( true );
3101 
3102  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3103  $expected = [
3104  $rememberReq,
3105  $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
3106  $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3107  $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3108  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3109  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3110  $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3111  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3112  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3113  ];
3114  usort( $actual, $cmpReqs );
3115  usort( $expected, $cmpReqs );
3116  $this->assertEquals( $expected, $actual );
3117 
3118  $this->primaryauthMocks = [ $primary1 ];
3119  $this->secondaryauthMocks = [ $secondary ];
3120  $this->initializeManager( true );
3121 
3122  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3123  $expected = [
3124  $rememberReq,
3125  $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
3126  $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3127  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3128  $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3129  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3130  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3131  ];
3132  usort( $actual, $cmpReqs );
3133  usort( $expected, $cmpReqs );
3134  $this->assertEquals( $expected, $actual );
3135  }
3136 
3137  public function testAllowsPropertyChange() {
3138  $mocks = [];
3139  foreach ( [ 'primary', 'secondary' ] as $key ) {
3140  $class = ucfirst( $key ) . 'AuthenticationProvider';
3141  $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3142  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3143  ->will( $this->returnValue( $key ) );
3144  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3145  ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3146  return $prop !== $key;
3147  } ) );
3148  }
3149 
3150  $this->primaryauthMocks = [ $mocks['primary'] ];
3151  $this->secondaryauthMocks = [ $mocks['secondary'] ];
3152  $this->initializeManager( true );
3153 
3154  $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3155  $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3156  $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3157  }
3158 
3159  public function testAutoCreateOnLogin() {
3161 
3162  $req = $this->createMock( AuthenticationRequest::class );
3163 
3164  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3165  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3166  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3167  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3168  $mock->expects( $this->any() )->method( 'accountCreationType' )
3169  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3170  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3171  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3172  ->will( $this->returnValue( StatusValue::newGood() ) );
3173 
3174  $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3175  $mock2->expects( $this->any() )->method( 'getUniqueId' )
3176  ->will( $this->returnValue( 'secondary' ) );
3177  $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3178  $this->returnValue(
3179  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3180  )
3181  );
3182  $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3183  ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3184  $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3185  ->will( $this->returnValue( StatusValue::newGood() ) );
3186 
3187  $this->primaryauthMocks = [ $mock ];
3188  $this->secondaryauthMocks = [ $mock2 ];
3189  $this->initializeManager( true );
3190  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3191  $session = $this->request->getSession();
3192  $session->clear();
3193 
3194  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3195  'sanity check' );
3196 
3197  $callback = $this->callback( function ( $user ) use ( $username ) {
3198  return $user->getName() === $username;
3199  } );
3200 
3201  $this->hook( 'UserLoggedIn', $this->never() );
3202  $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3203  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3204  $this->unhook( 'LocalUserCreated' );
3205  $this->unhook( 'UserLoggedIn' );
3206  $this->assertSame( AuthenticationResponse::UI, $ret->status );
3207 
3208  $id = (int)\User::newFromName( $username )->getId();
3209  $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3210  $this->assertSame( 0, $session->getUser()->getId() );
3211 
3212  $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3213  $this->hook( 'LocalUserCreated', $this->never() );
3214  $ret = $this->manager->continueAuthentication( [] );
3215  $this->unhook( 'LocalUserCreated' );
3216  $this->unhook( 'UserLoggedIn' );
3217  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3218  $this->assertSame( $username, $ret->username );
3219  $this->assertSame( $id, $session->getUser()->getId() );
3220  }
3221 
3222  public function testAutoCreateFailOnLogin() {
3224 
3225  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3226  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3227  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3228  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3229  $mock->expects( $this->any() )->method( 'accountCreationType' )
3230  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3231  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3232  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3233  ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3234 
3235  $this->primaryauthMocks = [ $mock ];
3236  $this->initializeManager( true );
3237  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3238  $session = $this->request->getSession();
3239  $session->clear();
3240 
3241  $this->assertSame( 0, $session->getUser()->getId(),
3242  'sanity check' );
3243  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3244  'sanity check' );
3245 
3246  $this->hook( 'UserLoggedIn', $this->never() );
3247  $this->hook( 'LocalUserCreated', $this->never() );
3248  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3249  $this->unhook( 'LocalUserCreated' );
3250  $this->unhook( 'UserLoggedIn' );
3251  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3252  $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3253 
3254  $this->assertSame( 0, \User::newFromName( $username )->getId() );
3255  $this->assertSame( 0, $session->getUser()->getId() );
3256  }
3257 
3258  public function testAuthenticationSessionData() {
3259  $this->initializeManager( true );
3260 
3261  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3262  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3263  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3264  $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3265  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3266  $this->manager->removeAuthenticationSessionData( 'foo' );
3267  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3268  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3269  $this->manager->removeAuthenticationSessionData( 'bar' );
3270  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3271 
3272  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3273  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3274  $this->manager->removeAuthenticationSessionData( null );
3275  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3276  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3277  }
3278 
3279  public function testCanLinkAccounts() {
3280  $types = [
3284  ];
3285 
3286  foreach ( $types as $type => $can ) {
3287  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3288  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3289  $mock->expects( $this->any() )->method( 'accountCreationType' )
3290  ->will( $this->returnValue( $type ) );
3291  $this->primaryauthMocks = [ $mock ];
3292  $this->initializeManager( true );
3293  $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3294  }
3295  }
3296 
3297  public function testBeginAccountLink() {
3298  $user = \User::newFromName( 'UTSysop' );
3299  $this->initializeManager();
3300 
3301  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3302  try {
3303  $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3304  $this->fail( 'Expected exception not thrown' );
3305  } catch ( \LogicException $ex ) {
3306  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3307  }
3308  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3309 
3310  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3311  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3312  $mock->expects( $this->any() )->method( 'accountCreationType' )
3313  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3314  $this->primaryauthMocks = [ $mock ];
3315  $this->initializeManager( true );
3316 
3317  $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3318  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3319  $this->assertSame( 'noname', $ret->message->getKey() );
3320 
3321  $ret = $this->manager->beginAccountLink(
3322  \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3323  );
3324  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3325  $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3326  }
3327 
3328  public function testContinueAccountLink() {
3329  $user = \User::newFromName( 'UTSysop' );
3330  $this->initializeManager();
3331 
3332  $session = [
3333  'userid' => $user->getId(),
3334  'username' => $user->getName(),
3335  'primary' => 'X',
3336  ];
3337 
3338  try {
3339  $this->manager->continueAccountLink( [] );
3340  $this->fail( 'Expected exception not thrown' );
3341  } catch ( \LogicException $ex ) {
3342  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3343  }
3344 
3345  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3346  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3347  $mock->expects( $this->any() )->method( 'accountCreationType' )
3348  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3349  $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3350  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3351  );
3352  $this->primaryauthMocks = [ $mock ];
3353  $this->initializeManager( true );
3354 
3355  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3356  $ret = $this->manager->continueAccountLink( [] );
3357  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3358  $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3359 
3360  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3361  [ 'username' => $user->getName() . '<>' ] + $session );
3362  $ret = $this->manager->continueAccountLink( [] );
3363  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3364  $this->assertSame( 'noname', $ret->message->getKey() );
3365  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3366 
3367  $id = $user->getId();
3368  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3369  [ 'userid' => $id + 1 ] + $session );
3370  try {
3371  $ret = $this->manager->continueAccountLink( [] );
3372  $this->fail( 'Expected exception not thrown' );
3373  } catch ( \UnexpectedValueException $ex ) {
3374  $this->assertEquals(
3375  "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
3376  $ex->getMessage()
3377  );
3378  }
3379  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3380  }
3381 
3388  public function testAccountLink(
3389  StatusValue $preTest, array $primaryResponses, array $managerResponses
3390  ) {
3391  $user = \User::newFromName( 'UTSysop' );
3392 
3393  $this->initializeManager();
3394 
3395  // Set up lots of mocks...
3396  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3397  $req->primary = $primaryResponses;
3398  $mocks = [];
3399 
3400  foreach ( [ 'pre', 'primary' ] as $key ) {
3401  $class = ucfirst( $key ) . 'AuthenticationProvider';
3402  $mocks[$key] = $this->getMockForAbstractClass(
3403  "MediaWiki\\Auth\\$class", [], "Mock$class"
3404  );
3405  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3406  ->will( $this->returnValue( $key ) );
3407 
3408  for ( $i = 2; $i <= 3; $i++ ) {
3409  $mocks[$key . $i] = $this->getMockForAbstractClass(
3410  "MediaWiki\\Auth\\$class", [], "Mock$class"
3411  );
3412  $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3413  ->will( $this->returnValue( $key . $i ) );
3414  }
3415  }
3416 
3417  $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3418  ->will( $this->returnCallback(
3419  function ( $u )
3420  use ( $user, $preTest )
3421  {
3422  $this->assertSame( $user->getId(), $u->getId() );
3423  $this->assertSame( $user->getName(), $u->getName() );
3424  return $preTest;
3425  }
3426  ) );
3427 
3428  $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3429  ->will( $this->returnValue( StatusValue::newGood() ) );
3430 
3431  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3432  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3433  $ct = count( $req->primary );
3434  $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3435  $this->assertSame( $user->getId(), $u->getId() );
3436  $this->assertSame( $user->getName(), $u->getName() );
3437  $foundReq = false;
3438  foreach ( $reqs as $r ) {
3439  $this->assertSame( $user->getName(), $r->username );
3440  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3441  }
3442  $this->assertTrue( $foundReq, '$reqs contains $req' );
3443  return array_shift( $req->primary );
3444  } );
3445  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3446  ->method( 'beginPrimaryAccountLink' )
3447  ->will( $callback );
3448  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3449  ->method( 'continuePrimaryAccountLink' )
3450  ->will( $callback );
3451 
3453  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3454  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3455  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3456  ->will( $this->returnValue( $abstain ) );
3457  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3458  $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3459  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3460  $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3461  $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3462 
3463  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3464  $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3465  $this->logger = new \TestLogger( true, function ( $message, $level ) {
3466  return $level === LogLevel::DEBUG ? null : $message;
3467  } );
3468  $this->initializeManager( true );
3469 
3470  $constraint = \PHPUnit_Framework_Assert::logicalOr(
3471  $this->equalTo( AuthenticationResponse::PASS ),
3472  $this->equalTo( AuthenticationResponse::FAIL )
3473  );
3474  $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3475  foreach ( $providers as $p ) {
3476  $p->postCalled = false;
3477  $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3478  ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3479  $this->assertInstanceOf( \User::class, $user );
3480  $this->assertSame( 'UTSysop', $user->getName() );
3481  $this->assertInstanceOf( AuthenticationResponse::class, $response );
3482  $this->assertThat( $response->status, $constraint );
3483  $p->postCalled = $response->status;
3484  } );
3485  }
3486 
3487  $first = true;
3488  $created = false;
3489  $expectLog = [];
3490  foreach ( $managerResponses as $i => $response ) {
3491  if ( $response instanceof AuthenticationResponse &&
3493  ) {
3494  $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3495  }
3496 
3497  $ex = null;
3498  try {
3499  if ( $first ) {
3500  $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3501  } else {
3502  $ret = $this->manager->continueAccountLink( [ $req ] );
3503  }
3504  if ( $response instanceof \Exception ) {
3505  $this->fail( 'Expected exception not thrown', "Response $i" );
3506  }
3507  } catch ( \Exception $ex ) {
3508  if ( !$response instanceof \Exception ) {
3509  throw $ex;
3510  }
3511  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3512  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3513  "Response $i, exception, session state" );
3514  return;
3515  }
3516 
3517  $this->assertSame( 'http://localhost/', $req->returnToUrl );
3518 
3519  $ret->message = $this->message( $ret->message );
3520  $this->assertEquals( $response, $ret, "Response $i, response" );
3521  if ( $response->status === AuthenticationResponse::PASS ||
3523  ) {
3524  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3525  "Response $i, session state" );
3526  foreach ( $providers as $p ) {
3527  $this->assertSame( $response->status, $p->postCalled,
3528  "Response $i, post-auth callback called" );
3529  }
3530  } else {
3531  $this->assertNotNull(
3532  $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3533  "Response $i, session state"
3534  );
3535  foreach ( $ret->neededRequests as $neededReq ) {
3536  $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
3537  "Response $i, neededRequest action" );
3538  }
3539  $this->assertEquals(
3540  $ret->neededRequests,
3541  $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3542  "Response $i, continuation check"
3543  );
3544  foreach ( $providers as $p ) {
3545  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3546  }
3547  }
3548 
3549  $first = false;
3550  }
3551 
3552  $this->assertSame( $expectLog, $this->logger->getBuffer() );
3553  }
3554 
3555  public function provideAccountLink() {
3556  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3557  $good = StatusValue::newGood();
3558 
3559  return [
3560  'Pre-link test fail in pre' => [
3561  StatusValue::newFatal( 'fail-from-pre' ),
3562  [],
3563  [
3564  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3565  ]
3566  ],
3567  'Failure in primary' => [
3568  $good,
3569  $tmp = [
3570  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3571  ],
3572  $tmp
3573  ],
3574  'All primary abstain' => [
3575  $good,
3576  [
3578  ],
3579  [
3580  AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3581  ]
3582  ],
3583  'Primary UI, then redirect, then fail' => [
3584  $good,
3585  $tmp = [
3586  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3587  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3588  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3589  ],
3590  $tmp
3591  ],
3592  'Primary redirect, then abstain' => [
3593  $good,
3594  [
3596  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3597  ),
3599  ],
3600  [
3601  $tmp,
3602  new \DomainException(
3603  'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3604  )
3605  ]
3606  ],
3607  'Primary UI, then pass' => [
3608  $good,
3609  [
3610  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3612  ],
3613  [
3614  $tmp1,
3616  ]
3617  ],
3618  'Primary pass' => [
3619  $good,
3620  [
3622  ],
3623  [
3625  ]
3626  ],
3627  ];
3628  }
3629 }
MediaWiki\Auth\AuthenticationRequest\OPTIONAL
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
Definition: AuthenticationRequest.php:40
MediaWiki\Auth\AuthManagerTest\testBeginAuthentication
testBeginAuthentication()
Definition: AuthManagerTest.php:706
MediaWiki\Auth\AuthManagerTest\testSetDefaultUserOptions
testSetDefaultUserOptions()
Definition: AuthManagerTest.php:591
$user
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 account $user
Definition: hooks.txt:244
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_CREATE
const TYPE_CREATE
Provider can create accounts.
Definition: PrimaryAuthenticationProvider.php:77
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:42
MediaWiki\$action
String $action
Cache what action this request is.
Definition: MediaWiki.php:48
MediaWiki\Auth\AuthManager\SEC_REAUTH
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
Definition: AuthManager.php:109
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_NONE
const TYPE_NONE
Provider cannot create or link to accounts.
Definition: PrimaryAuthenticationProvider.php:81
MediaWikiTestCase\stashMwGlobals
stashMwGlobals( $globalKeys)
Stashes the global, will be restored in tearDown()
Definition: MediaWikiTestCase.php:730
false
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
ObjectCache\getLocalClusterInstance
static getLocalClusterInstance()
Get the main cluster-local cache object.
Definition: ObjectCache.php:367
HashBagOStuff
Simple store for keeping values in an associative array for the current process.
Definition: HashBagOStuff.php:31
MediaWiki\Auth\AuthManagerTest\provideAllowsAuthenticationDataChange
static provideAllowsAuthenticationDataChange()
Definition: AuthManagerTest.php:1301
MediaWiki\Auth\AuthManager\ACTION_UNLINK
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
Definition: AuthManager.php:104
MediaWiki\Auth\AuthManager\AUTOCREATE_SOURCE_SESSION
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
Definition: AuthManager.php:114
MediaWiki\Auth\AuthManagerTest\testForcePrimaryAuthenticationProviders
testForcePrimaryAuthenticationProviders()
Definition: AuthManagerTest.php:629
captcha-old.count
count
Definition: captcha-old.py:249
MediaWiki\Auth\AuthManagerTest\testCanCreateAccount
testCanCreateAccount()
Definition: AuthManagerTest.php:1488
MediaWiki\Session\UserInfo
Object holding data about a session's user.
Definition: UserInfo.php:51
MediaWiki\Auth\AuthManagerTest\testGetAuthenticationRequestsRequired
testGetAuthenticationRequestsRequired()
Definition: AuthManagerTest.php:3032
MediaWiki\Auth\AuthManagerTest\initializeConfig
initializeConfig()
Initialize the AuthManagerConfig variable in $this->config.
Definition: AuthManagerTest.php:90
MediaWiki\Auth\AuthManagerTest\testAccountLink
testAccountLink(StatusValue $preTest, array $primaryResponses, array $managerResponses)
provideAccountLink
Definition: AuthManagerTest.php:3388
MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
Links third-party authentication to the user's account.
Definition: ConfirmLinkSecondaryAuthenticationProvider.php:18
DatabaseLogEntry\getSelectQueryData
static getSelectQueryData()
Returns array of information that is needed for querying log entries.
Definition: LogEntry.php:172
MessageSpecifier
Definition: MessageSpecifier.php:21
use
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to use
Definition: MIT-LICENSE.txt:10
MediaWiki\Auth\AuthManagerTest\testAuthenticationSessionData
testAuthenticationSessionData()
Definition: AuthManagerTest.php:3258
ObjectCache\$instances
static BagOStuff[] $instances
Map of (id => BagOStuff)
Definition: ObjectCache.php:82
anything
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as MediaWiki does not conform to normal Unix filesystem layout Hopefully we ll offer direct support for standard layouts in the but for now *any change to the location of files is unsupported *Moving things and leaving symlinks will *probably *not break anything
Definition: distributors.txt:39
$req
this hook is for auditing only $req
Definition: hooks.txt:990
MediaWiki\Auth\PrimaryAuthenticationProvider\TYPE_LINK
const TYPE_LINK
Provider can link to existing accounts elsewhere.
Definition: PrimaryAuthenticationProvider.php:79
MediaWiki\Auth\CreatedAccountAuthenticationRequest
Returned from account creation to allow for logging into the created account.
Definition: CreatedAccountAuthenticationRequest.php:29
StatusValue\newFatal
static newFatal( $message)
Factory function for fatal errors.
Definition: StatusValue.php:68
$params
$params
Definition: styleTest.css.php:40
Block\newFromTarget
static newFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
Given a target and the target's type, get an existing Block object if possible.
Definition: Block.php:1173
MediaWiki\Auth\AuthManagerTest\testCanAuthenticateNow
testCanAuthenticateNow()
Definition: AuthManagerTest.php:207
MediaWiki\Session\TestUtils\setSessionManagerSingleton
static setSessionManagerSingleton(SessionManager $manager=null)
Override the singleton for unit testing.
Definition: TestUtils.php:18
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:591
MediaWiki\Auth\AuthenticationResponse\RESTART
const RESTART
Indicates that third-party authentication succeeded but no user exists.
Definition: AuthenticationResponse.php:49
MediaWiki\Auth\AuthManagerTest\testProviderCreation
testProviderCreation()
Definition: AuthManagerTest.php:462
MediaWiki\Auth\AuthManagerTest\provideUserCanAuthenticate
static provideUserCanAuthenticate()
Definition: AuthManagerTest.php:433
$s
$s
Definition: mergeMessageFileList.php:187
MediaWiki\Auth\AuthManagerTest\testContinueAccountLink
testContinueAccountLink()
Definition: AuthManagerTest.php:3328
MediaWiki\Auth\AuthManagerTest\provideAccountLink
provideAccountLink()
Definition: AuthManagerTest.php:3555
MediaWiki\Auth\AuthManagerTest\testSingleton
testSingleton()
Definition: AuthManagerTest.php:188
MediaWiki\Auth\AuthManagerTest\provideUserExists
static provideUserExists()
Definition: AuthManagerTest.php:1266
$res
$res
Definition: database.txt:21
$name
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:302
MediaWiki\Auth\AuthManagerTest\$manager
AuthManager $manager
Definition: AuthManagerTest.php:33
$success
$success
Definition: NoLocalSettings.php:42
User
User
Definition: All_system_messages.txt:425
MediaWiki\Auth\AuthManagerTest\testCheckAccountCreatePermissions
testCheckAccountCreatePermissions()
Definition: AuthManagerTest.php:1394
MediaWiki\Auth\AuthManager\ACTION_LOGIN_CONTINUE
const ACTION_LOGIN_CONTINUE
Continue a login process that was interrupted by the need for user input or communication with an ext...
Definition: AuthManager.php:88
MediaWiki\Session\UserInfo\newFromUser
static newFromUser(User $user, $verified=false)
Create an instance from an existing User object.
Definition: UserInfo.php:116
MediaWiki\Auth\AuthManager\SEC_FAIL
const SEC_FAIL
Security-sensitive should not be performed.
Definition: AuthManager.php:111
php
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
MediaWiki\Auth\AuthManagerTest\testContinueAccountCreation
testContinueAccountCreation()
Definition: AuthManagerTest.php:1702
MediaWiki\Auth\AuthManagerTest\unhook
unhook( $hook)
Unsets a hook.
Definition: AuthManagerTest.php:63
MediaWiki\Auth\AuthenticationResponse\newAbstain
static newAbstain()
Definition: AuthenticationResponse.php:170
MediaWiki\Auth\AuthManagerTest\testAccountCreation
testAccountCreation(StatusValue $preTest, $primaryTest, $secondaryTest, array $primaryResponses, array $secondaryResponses, array $managerResponses)
provideAccountCreation
Definition: AuthManagerTest.php:1854
MediaWiki\Auth\AuthManagerTest\testCanCreateAccounts
testCanCreateAccounts()
Definition: AuthManagerTest.php:1376
Config
Interface for configuration instances.
Definition: Config.php:28
MediaWiki\Auth\AuthenticationRequest\PRIMARY_REQUIRED
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
Definition: AuthenticationRequest.php:51
MediaWiki\Auth\AuthenticationResponse\UI
const UI
Indicates that the authentication needs further user input of some sort.
Definition: AuthenticationResponse.php:55
wfMemcKey
wfMemcKey()
Make a cache key for the local wiki.
Definition: GlobalFunctions.php:2700
MediaWiki\Auth\AuthManagerTest\provideAuthentication
provideAuthentication()
Definition: AuthManagerTest.php:1087
MediaWiki\Auth\AuthManager\ACTION_LINK_CONTINUE
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
Definition: AuthManager.php:98
DatabaseLogEntry\newFromRow
static newFromRow( $row)
Constructs new LogEntry from database result row.
Definition: LogEntry.php:207
MediaWiki\Auth\CreationReasonAuthenticationRequest
Authentication request for the reason given for account creation.
Definition: CreationReasonAuthenticationRequest.php:9
MediaWiki\Auth\AuthManagerTest\testAllowsAuthenticationDataChange
testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect)
provideAllowsAuthenticationDataChange
Definition: AuthManagerTest.php:1281
MediaWiki\Auth\AuthManagerTest\usernameForCreation
static usernameForCreation( $uniq='')
Definition: AuthManagerTest.php:1480
MediaWiki\Auth\AuthManagerTest\$preauthMocks
$preauthMocks
Definition: AuthManagerTest.php:28
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:55
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2800
MediaWikiTestCase\setMwGlobals
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
Definition: MediaWikiTestCase.php:678
MediaWiki\Auth\AuthManagerTest\$managerPriv
TestingAccessWrapper $managerPriv
Definition: AuthManagerTest.php:35
MediaWikiTestCase
Definition: MediaWikiTestCase.php:17
MediaWiki\Auth\AuthManagerTest\message
message( $key, $params=[])
Ensure a value is a clean Message object.
Definition: AuthManagerTest.php:74
MediaWiki\Auth\AuthManagerTest\testAccountCreationLogging
testAccountCreationLogging( $isAnon, $logSubtype)
provideAccountCreationLogging
Definition: AuthManagerTest.php:2256
MediaWiki\Auth\AuthManagerTest\testAutoCreateOnLogin
testAutoCreateOnLogin()
Definition: AuthManagerTest.php:3159
MediaWiki\Auth\AuthenticationResponse
This is a value object to hold authentication response data.
Definition: AuthenticationResponse.php:37
MediaWiki\Auth\AuthManagerTest\testNormalizeUsername
testNormalizeUsername()
Definition: AuthManagerTest.php:219
MediaWikiTestCase\hideDeprecated
hideDeprecated( $function)
Don't throw a warning if $function is deprecated and called later.
Definition: MediaWikiTestCase.php:1675
MediaWiki\Auth\CreateFromLoginAuthenticationRequest
This transfers state between the login and account creation flows.
Definition: CreateFromLoginAuthenticationRequest.php:34
MediaWiki
A helper class for throttling authentication attempts.
request
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging a wrapping ErrorException instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:2163
$time
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1795
global
when a variable name is used in a it is silently declared as a new masking the global
Definition: design.txt:93
MediaWiki\Auth\UsernameAuthenticationRequest
AuthenticationRequest to ensure something with a username is present.
Definition: UsernameAuthenticationRequest.php:29
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MediaWiki\Auth\AuthManagerTest\testRevokeAccessForUser
testRevokeAccessForUser()
Definition: AuthManagerTest.php:442
MediaWiki\Auth\UserDataAuthenticationRequest
This represents additional user data requested on the account creation form.
Definition: UserDataAuthenticationRequest.php:34
MediaWiki\Auth\AuthenticationResponse\FAIL
const FAIL
Indicates that the authentication failed.
Definition: AuthenticationResponse.php:42
MediaWiki\Auth\AuthManagerTest\testBeginAccountLink
testBeginAccountLink()
Definition: AuthManagerTest.php:3297
list
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
will
</td >< td > &</td >< td > t want your writing to be edited mercilessly and redistributed at will
Definition: All_system_messages.txt:914
MediaWiki\Auth\AuthManagerTest\$secondaryauthMocks
$secondaryauthMocks
Definition: AuthManagerTest.php:30
MediaWiki\Auth\AuthManagerTest\testUserCanAuthenticate
testUserCanAuthenticate( $primary1Can, $primary2Can, $expect)
provideUserCanAuthenticate
Definition: AuthManagerTest.php:414
MediaWiki\Auth\AuthManagerTest\testGetAuthenticationRequests
testGetAuthenticationRequests( $action, $expect, $state=[])
provideGetAuthenticationRequests
Definition: AuthManagerTest.php:2821
MediaWiki\Auth\AuthManager\ACTION_CREATE_CONTINUE
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
Definition: AuthManager.php:93
MediaWiki\Auth\AuthManagerTest\provideGetAuthenticationRequests
static provideGetAuthenticationRequests()
Definition: AuthManagerTest.php:2966
any
they could even be mouse clicks or menu items whatever suits your program You should also get your if any
Definition: COPYING.txt:326
MediaWiki\Auth\AuthManager\ACTION_CREATE
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:90
MediaWiki\Auth\AuthManagerTest
AuthManager Database MediaWiki\Auth\AuthManager.
Definition: AuthManagerTest.php:20
MediaWiki\Auth\AuthManagerTest\provideAccountCreationLogging
static provideAccountCreationLogging()
Definition: AuthManagerTest.php:2333
TestUser\setPasswordForUser
static setPasswordForUser(User $user, $password)
Set the password on a testing user.
Definition: TestUser.php:127
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
MediaWiki\Auth\AuthManagerTest\getMockSessionProvider
getMockSessionProvider( $canChangeUser=null, array $methods=[])
Setup SessionManager with a mock session provider.
Definition: AuthManagerTest.php:143
MediaWiki\Auth\AuthManager\ACTION_CHANGE
const ACTION_CHANGE
Change a user's credentials.
Definition: AuthManager.php:100
MediaWikiTestCase\getTestSysop
static getTestSysop()
Convenience method for getting an immutable admin test user.
Definition: MediaWikiTestCase.php:177
MediaWiki\Auth\AuthManagerTest\$config
Config $config
Definition: AuthManagerTest.php:24
$ret
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1987
MediaWiki\Auth\AuthManager\ACTION_LINK
const ACTION_LINK
Link an existing user to a third-party account.
Definition: AuthManager.php:95
MediaWiki\Auth\AuthManagerTest\$primaryauthMocks
$primaryauthMocks
Definition: AuthManagerTest.php:29
MediaWiki\Auth\AuthManagerTest\testUserExists
testUserExists( $primary1Exists, $primary2Exists, $expect)
provideUserExists
Definition: AuthManagerTest.php:1247
MediaWiki\Auth\AuthManagerTest\testAuthentication
testAuthentication(StatusValue $preResponse, array $primaryResponses, array $secondaryResponses, array $managerResponses, $link=false)
provideAuthentication
Definition: AuthManagerTest.php:859
MediaWiki\Auth\RememberMeAuthenticationRequest
This is an authentication request added by AuthManager to show a "remember me" checkbox.
Definition: RememberMeAuthenticationRequest.php:33
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:434
MediaWiki\Auth\AuthManagerTest\testAllowsPropertyChange
testAllowsPropertyChange()
Definition: AuthManagerTest.php:3137
$response
this hook is for auditing only $response
Definition: hooks.txt:783
MediaWiki\Auth\AuthManager
This serves as the entry point to the authentication system.
Definition: AuthManager.php:83
WebRequest
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:38
MediaWiki\Auth\AuthManager\ACTION_REMOVE
const ACTION_REMOVE
Remove a user's credentials.
Definition: AuthManager.php:102
MediaWiki\Session\SessionInfo
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
MediaWiki\Auth\AuthManager\SEC_OK
const SEC_OK
Security-sensitive operations are ok.
Definition: AuthManager.php:107
MediaWiki\Auth\AuthManagerTest\onSecuritySensitiveOperationStatus
onSecuritySensitiveOperationStatus(&$status, $operation, $session, $time)
Definition: AuthManagerTest.php:398
$cache
$cache
Definition: mcc.php:33
$rows
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition: hooks.txt:2604
MediaWiki\Auth\AuthManager\singleton
static singleton()
Get the global AuthManager.
Definition: AuthManager.php:147
MediaWiki\Auth\AuthManagerTest\$request
WebRequest $request
Definition: AuthManagerTest.php:22
User\idFromName
static idFromName( $name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:883
MediaWiki\Auth\AuthenticationResponse\newFail
static newFail(Message $msg)
Definition: AuthenticationResponse.php:146
$wgHooks
$wgHooks['ArticleShow'][]
Definition: hooks.txt:108
MediaWiki\Auth\AuthenticationResponse\newRedirect
static newRedirect(array $reqs, $redirectTarget, $redirectApiData=null)
Definition: AuthenticationResponse.php:206
as
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
MediaWiki\Auth\AuthManagerTest\provideSecuritySensitiveOperationStatus
static provideSecuritySensitiveOperationStatus()
Definition: AuthManagerTest.php:401
MediaWiki\Auth\AuthManagerTest\testChangeAuthenticationData
testChangeAuthenticationData()
Definition: AuthManagerTest.php:1354
$link
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3005
MediaWiki\Auth\AuthManager\ACTION_LOGIN
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:85
true
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1987
MediaWiki\Auth\AuthManagerTest\provideAccountCreation
provideAccountCreation()
Definition: AuthManagerTest.php:2131
Language\factory
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:183
$status
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1255
wfMessage
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
MediaWiki\Auth\AuthManagerTest\setUp
setUp()
Definition: AuthManagerTest.php:37
$wgGroupPermissions
$wgGroupPermissions['sysop']['replacetext']
Definition: ReplaceText.php:56
MediaWiki\Auth\AuthManagerTest\testAutoCreateFailOnLogin
testAutoCreateFailOnLogin()
Definition: AuthManagerTest.php:3222
class
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
DummySessionProvider\ID
const ID
Definition: DummySessionProvider.php:14
MediaWiki\Auth\AuthManagerTest\testAutoAccountCreation
testAutoAccountCreation()
Definition: AuthManagerTest.php:2342
MediaWiki\Auth\AuthenticationResponse\PASS
const PASS
Indicates that the authentication succeeded.
Definition: AuthenticationResponse.php:39
MediaWiki\Auth\AuthManagerTest\hook
hook( $hook, $expect)
Sets a mock on a hook.
Definition: AuthManagerTest.php:50
MediaWiki\$context
IContextSource $context
Definition: MediaWiki.php:38
MediaWiki\Auth\AuthenticationResponse\newRestart
static newRestart(Message $msg)
Definition: AuthenticationResponse.php:159
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:53
MediaWiki\Session\SessionInfo\MIN_PRIORITY
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
$username
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:783
MediaWiki\Auth
Definition: AbstractAuthenticationProvider.php:22
MediaWiki\Auth\AuthenticationResponse\newPass
static newPass( $username=null)
Definition: AuthenticationResponse.php:134
MediaWiki\Auth\AuthenticationResponse\newUI
static newUI(array $reqs, Message $msg, $msgtype='warning')
Definition: AuthenticationResponse.php:183
MediaWiki\Auth\AuthManagerTest\testCreateFromLogin
testCreateFromLogin()
Definition: AuthManagerTest.php:769
MediaWiki\Auth\AuthManagerTest\initializeManager
initializeManager( $regen=false)
Initialize $this->manager.
Definition: AuthManagerTest.php:118
MediaWiki\Auth\AuthManagerTest\$logger
LoggerInterface $logger
Definition: AuthManagerTest.php:26
MediaWiki\Auth\AuthManagerTest\testSecuritySensitiveOperationStatus
testSecuritySensitiveOperationStatus( $mutableSession)
provideSecuritySensitiveOperationStatus
Definition: AuthManagerTest.php:253
MediaWiki\Auth\AuthManagerTest\testBeginAccountCreation
testBeginAccountCreation()
Definition: AuthManagerTest.php:1553
MediaWiki\Auth\AuthenticationRequest\REQUIRED
const REQUIRED
Indicates that the request is required for authentication to proceed.
Definition: AuthenticationRequest.php:46
IContextSource\getLanguage
getLanguage()
array
the array() calling protocol came about after MediaWiki 1.4rc1.
MediaWiki\Auth\AuthManagerTest\testCanLinkAccounts
testCanLinkAccounts()
Definition: AuthManagerTest.php:3279
$type
$type
Definition: testCompression.php:48