MediaWiki  1.27.1
AuthManagerTest.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Auth;
4 
9 
17  protected $request;
19  protected $config;
21  protected $logger;
22 
23  protected $preauthMocks = [];
24  protected $primaryauthMocks = [];
25  protected $secondaryauthMocks = [];
26 
28  protected $manager;
30  protected $managerPriv;
31 
32  protected function setUp() {
33  parent::setUp();
34 
35  $this->setMwGlobals( [ 'wgAuth' => null ] );
36  $this->stashMwGlobals( [ 'wgHooks' ] );
37  }
38 
45  protected function hook( $hook, $expect ) {
47  $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
48  $wgHooks[$hook] = [ $mock ];
49  return $mock->expects( $expect )->method( "on$hook" );
50  }
51 
56  protected function unhook( $hook ) {
58  $wgHooks[$hook] = [];
59  }
60 
67  protected function message( $key, $params = [] ) {
68  if ( $key === null ) {
69  return null;
70  }
71  if ( $key instanceof \MessageSpecifier ) {
72  $params = $key->getParams();
73  $key = $key->getKey();
74  }
75  return new \Message( $key, $params, \Language::factory( 'en' ) );
76  }
77 
83  protected function initializeConfig() {
84  $config = [
85  'preauth' => [
86  ],
87  'primaryauth' => [
88  ],
89  'secondaryauth' => [
90  ],
91  ];
92 
93  foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
94  $key = $type . 'Mocks';
95  foreach ( $this->$key as $mock ) {
96  $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
97  return $mock;
98  } ];
99  }
100  }
101 
102  $this->config->set( 'AuthManagerConfig', $config );
103  $this->config->set( 'LanguageCode', 'en' );
104  $this->config->set( 'NewUserLog', false );
105  }
106 
111  protected function initializeManager( $regen = false ) {
112  if ( $regen || !$this->config ) {
113  $this->config = new \HashConfig();
114  }
115  if ( $regen || !$this->request ) {
116  $this->request = new \FauxRequest();
117  }
118  if ( !$this->logger ) {
119  $this->logger = new \TestLogger();
120  }
121 
122  if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
123  $this->initializeConfig();
124  }
125  $this->manager = new AuthManager( $this->request, $this->config );
126  $this->manager->setLogger( $this->logger );
127  $this->managerPriv = \TestingAccessWrapper::newFromObject( $this->manager );
128  }
129 
136  protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
137  if ( !$this->config ) {
138  $this->config = new \HashConfig();
139  $this->initializeConfig();
140  }
141  $this->config->set( 'ObjectCacheSessionExpiry', 100 );
142 
143  $methods[] = '__toString';
144  $methods[] = 'describe';
145  if ( $canChangeUser !== null ) {
146  $methods[] = 'canChangeUser';
147  }
148  $provider = $this->getMockBuilder( 'DummySessionProvider' )
149  ->setMethods( $methods )
150  ->getMock();
151  $provider->expects( $this->any() )->method( '__toString' )
152  ->will( $this->returnValue( 'MockSessionProvider' ) );
153  $provider->expects( $this->any() )->method( 'describe' )
154  ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
155  if ( $canChangeUser !== null ) {
156  $provider->expects( $this->any() )->method( 'canChangeUser' )
157  ->will( $this->returnValue( $canChangeUser ) );
158  }
159  $this->config->set( 'SessionProviders', [
160  [ 'factory' => function () use ( $provider ) {
161  return $provider;
162  } ],
163  ] );
164 
165  $manager = new \MediaWiki\Session\SessionManager( [
166  'config' => $this->config,
167  'logger' => new \Psr\Log\NullLogger(),
168  'store' => new \HashBagOStuff(),
169  ] );
170  \TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
171 
173 
174  if ( $this->request ) {
175  $manager->getSessionForRequest( $this->request );
176  }
177 
178  return [ $provider, $reset ];
179  }
180 
181  public function testSingleton() {
182  // Temporarily clear out the global singleton, if any, to test creating
183  // one.
184  $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
185  $rProp->setAccessible( true );
186  $old = $rProp->getValue();
187  $cb = new \ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
188  $rProp->setValue( null );
189 
190  $singleton = AuthManager::singleton();
191  $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
192  $this->assertSame( $singleton, AuthManager::singleton() );
193  $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
194  $this->assertSame(
195  \RequestContext::getMain()->getConfig(),
196  \TestingAccessWrapper::newFromObject( $singleton )->config
197  );
198  }
199 
200  public function testCanAuthenticateNow() {
201  $this->initializeManager();
202 
203  list( $provider, $reset ) = $this->getMockSessionProvider( false );
204  $this->assertFalse( $this->manager->canAuthenticateNow() );
205  \ScopedCallback::consume( $reset );
206 
207  list( $provider, $reset ) = $this->getMockSessionProvider( true );
208  $this->assertTrue( $this->manager->canAuthenticateNow() );
209  \ScopedCallback::consume( $reset );
210  }
211 
212  public function testNormalizeUsername() {
213  $mocks = [
214  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
215  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
216  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
217  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
218  ];
219  foreach ( $mocks as $key => $mock ) {
220  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
221  }
222  $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
223  ->with( $this->identicalTo( 'XYZ' ) )
224  ->willReturn( 'Foo' );
225  $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
226  ->with( $this->identicalTo( 'XYZ' ) )
227  ->willReturn( 'Foo' );
228  $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
229  ->with( $this->identicalTo( 'XYZ' ) )
230  ->willReturn( null );
231  $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
232  ->with( $this->identicalTo( 'XYZ' ) )
233  ->willReturn( 'Bar!' );
234 
235  $this->primaryauthMocks = $mocks;
236 
237  $this->initializeManager();
238 
239  $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
240  }
241 
246  public function testSecuritySensitiveOperationStatus( $mutableSession ) {
247  $this->logger = new \Psr\Log\NullLogger();
248  $user = \User::newFromName( 'UTSysop' );
249  $provideUser = null;
250  $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
251 
252  list( $provider, $reset ) = $this->getMockSessionProvider(
253  $mutableSession, [ 'provideSessionInfo' ]
254  );
255  $provider->expects( $this->any() )->method( 'provideSessionInfo' )
256  ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
258  'provider' => $provider,
259  'id' => \DummySessionProvider::ID,
260  'persisted' => true,
261  'userInfo' => UserInfo::newFromUser( $provideUser, true )
262  ] );
263  } ) );
264  $this->initializeManager();
265 
266  $this->config->set( 'ReauthenticateTime', [] );
267  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
268  $provideUser = new \User;
269  $session = $provider->getManager()->getSessionForRequest( $this->request );
270  $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
271 
272  // Anonymous user => reauth
273  $session->set( 'AuthManager:lastAuthId', 0 );
274  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
275  $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
276 
277  $provideUser = $user;
278  $session = $provider->getManager()->getSessionForRequest( $this->request );
279  $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
280 
281  // Error for no default (only gets thrown for non-anonymous user)
282  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
283  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
284  try {
285  $this->manager->securitySensitiveOperationStatus( 'foo' );
286  $this->fail( 'Expected exception not thrown' );
287  } catch ( \UnexpectedValueException $ex ) {
288  $this->assertSame(
289  $mutableSession
290  ? '$wgReauthenticateTime lacks a default'
291  : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
292  $ex->getMessage()
293  );
294  }
295 
296  if ( $mutableSession ) {
297  $this->config->set( 'ReauthenticateTime', [
298  'test' => 100,
299  'test2' => -1,
300  'default' => 10,
301  ] );
302 
303  // Mismatched user ID
304  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
305  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
306  $this->assertSame(
307  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
308  );
309  $this->assertSame(
310  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
311  );
312  $this->assertSame(
313  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
314  );
315 
316  // Missing time
317  $session->set( 'AuthManager:lastAuthId', $user->getId() );
318  $session->set( 'AuthManager:lastAuthTimestamp', null );
319  $this->assertSame(
320  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
321  );
322  $this->assertSame(
323  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
324  );
325  $this->assertSame(
326  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
327  );
328 
329  // Recent enough to pass
330  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
331  $this->assertSame(
332  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
333  );
334 
335  // Not recent enough to pass
336  $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
337  $this->assertSame(
338  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
339  );
340  // But recent enough for the 'test' operation
341  $this->assertSame(
342  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
343  );
344  } else {
345  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
346  'test' => false,
347  'default' => true,
348  ] );
349 
350  $this->assertEquals(
351  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
352  );
353 
354  $this->assertEquals(
355  AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
356  );
357  }
358 
359  // Test hook, all three possible values
360  foreach ( [
362  AuthManager::SEC_REAUTH => $reauth,
364  ] as $hook => $expect ) {
365  $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
366  ->with(
367  $this->anything(),
368  $this->anything(),
369  $this->callback( function ( $s ) use ( $session ) {
370  return $s->getId() === $session->getId();
371  } ),
372  $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
373  )
374  ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
375  $v = $hook;
376  return true;
377  } ) );
378  $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
379  $this->assertEquals(
380  $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
381  );
382  $this->assertEquals(
383  $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
384  );
385  $this->unhook( 'SecuritySensitiveOperationStatus' );
386  }
387 
388  \ScopedCallback::consume( $reset );
389  }
390 
391  public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
392  }
393 
394  public static function provideSecuritySensitiveOperationStatus() {
395  return [
396  [ true ],
397  [ false ],
398  ];
399  }
400 
407  public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
408  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
409  $mock1->expects( $this->any() )->method( 'getUniqueId' )
410  ->will( $this->returnValue( 'primary1' ) );
411  $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
412  ->with( $this->equalTo( 'UTSysop' ) )
413  ->will( $this->returnValue( $primary1Can ) );
414  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
415  $mock2->expects( $this->any() )->method( 'getUniqueId' )
416  ->will( $this->returnValue( 'primary2' ) );
417  $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
418  ->with( $this->equalTo( 'UTSysop' ) )
419  ->will( $this->returnValue( $primary2Can ) );
420  $this->primaryauthMocks = [ $mock1, $mock2 ];
421 
422  $this->initializeManager( true );
423  $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
424  }
425 
426  public static function provideUserCanAuthenticate() {
427  return [
428  [ false, false, false ],
429  [ true, false, true ],
430  [ false, true, true ],
431  [ true, true, true ],
432  ];
433  }
434 
435  public function testRevokeAccessForUser() {
436  $this->initializeManager();
437 
438  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
439  $mock->expects( $this->any() )->method( 'getUniqueId' )
440  ->will( $this->returnValue( 'primary' ) );
441  $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
442  ->with( $this->equalTo( 'UTSysop' ) );
443  $this->primaryauthMocks = [ $mock ];
444 
445  $this->initializeManager( true );
446  $this->logger->setCollect( true );
447 
448  $this->manager->revokeAccessForUser( 'UTSysop' );
449 
450  $this->assertSame( [
451  [ LogLevel::INFO, 'Revoking access for {user}' ],
452  ], $this->logger->getBuffer() );
453  }
454 
455  public function testProviderCreation() {
456  $mocks = [
457  'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
458  'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
459  'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
460  ];
461  foreach ( $mocks as $key => $mock ) {
462  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
463  $mock->expects( $this->once() )->method( 'setLogger' );
464  $mock->expects( $this->once() )->method( 'setManager' );
465  $mock->expects( $this->once() )->method( 'setConfig' );
466  }
467  $this->preauthMocks = [ $mocks['pre'] ];
468  $this->primaryauthMocks = [ $mocks['primary'] ];
469  $this->secondaryauthMocks = [ $mocks['secondary'] ];
470 
471  // Normal operation
472  $this->initializeManager();
473  $this->assertSame(
474  $mocks['primary'],
475  $this->managerPriv->getAuthenticationProvider( 'primary' )
476  );
477  $this->assertSame(
478  $mocks['secondary'],
479  $this->managerPriv->getAuthenticationProvider( 'secondary' )
480  );
481  $this->assertSame(
482  $mocks['pre'],
483  $this->managerPriv->getAuthenticationProvider( 'pre' )
484  );
485  $this->assertSame(
486  [ 'pre' => $mocks['pre'] ],
487  $this->managerPriv->getPreAuthenticationProviders()
488  );
489  $this->assertSame(
490  [ 'primary' => $mocks['primary'] ],
491  $this->managerPriv->getPrimaryAuthenticationProviders()
492  );
493  $this->assertSame(
494  [ 'secondary' => $mocks['secondary'] ],
495  $this->managerPriv->getSecondaryAuthenticationProviders()
496  );
497 
498  // Duplicate IDs
499  $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
500  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
501  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
502  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
503  $this->preauthMocks = [ $mock1 ];
504  $this->primaryauthMocks = [ $mock2 ];
505  $this->secondaryauthMocks = [];
506  $this->initializeManager( true );
507  try {
508  $this->managerPriv->getAuthenticationProvider( 'Y' );
509  $this->fail( 'Expected exception not thrown' );
510  } catch ( \RuntimeException $ex ) {
511  $class1 = get_class( $mock1 );
512  $class2 = get_class( $mock2 );
513  $this->assertSame(
514  "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
515  );
516  }
517 
518  // Wrong classes
519  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
520  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
521  $class = get_class( $mock );
522  $this->preauthMocks = [ $mock ];
523  $this->primaryauthMocks = [ $mock ];
524  $this->secondaryauthMocks = [ $mock ];
525  $this->initializeManager( true );
526  try {
527  $this->managerPriv->getPreAuthenticationProviders();
528  $this->fail( 'Expected exception not thrown' );
529  } catch ( \RuntimeException $ex ) {
530  $this->assertSame(
531  "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
532  $ex->getMessage()
533  );
534  }
535  try {
536  $this->managerPriv->getPrimaryAuthenticationProviders();
537  $this->fail( 'Expected exception not thrown' );
538  } catch ( \RuntimeException $ex ) {
539  $this->assertSame(
540  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
541  $ex->getMessage()
542  );
543  }
544  try {
545  $this->managerPriv->getSecondaryAuthenticationProviders();
546  $this->fail( 'Expected exception not thrown' );
547  } catch ( \RuntimeException $ex ) {
548  $this->assertSame(
549  "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
550  $ex->getMessage()
551  );
552  }
553 
554  // Sorting
555  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
556  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
557  $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
558  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
559  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
560  $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
561  $this->preauthMocks = [];
562  $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
563  $this->secondaryauthMocks = [];
564  $this->initializeConfig();
565  $config = $this->config->get( 'AuthManagerConfig' );
566 
567  $this->initializeManager( false );
568  $this->assertSame(
569  [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
570  $this->managerPriv->getPrimaryAuthenticationProviders(),
571  'sanity check'
572  );
573 
574  $config['primaryauth']['A']['sort'] = 100;
575  $config['primaryauth']['C']['sort'] = -1;
576  $this->config->set( 'AuthManagerConfig', $config );
577  $this->initializeManager( false );
578  $this->assertSame(
579  [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
580  $this->managerPriv->getPrimaryAuthenticationProviders()
581  );
582  }
583 
584  public function testSetDefaultUserOptions() {
585  $this->initializeManager();
586 
588  $reset = new \ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
589  $context->setLanguage( 'de' );
590  $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
591 
592  $user = \User::newFromName( self::usernameForCreation() );
593  $user->addToDatabase();
594  $oldToken = $user->getToken();
595  $this->managerPriv->setDefaultUserOptions( $user, false );
596  $user->saveSettings();
597  $this->assertNotEquals( $oldToken, $user->getToken() );
598  $this->assertSame( 'zh', $user->getOption( 'language' ) );
599  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
600 
601  $user = \User::newFromName( self::usernameForCreation() );
602  $user->addToDatabase();
603  $oldToken = $user->getToken();
604  $this->managerPriv->setDefaultUserOptions( $user, true );
605  $user->saveSettings();
606  $this->assertNotEquals( $oldToken, $user->getToken() );
607  $this->assertSame( 'de', $user->getOption( 'language' ) );
608  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
609 
610  $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) );
611 
612  $user = \User::newFromName( self::usernameForCreation() );
613  $user->addToDatabase();
614  $oldToken = $user->getToken();
615  $this->managerPriv->setDefaultUserOptions( $user, true );
616  $user->saveSettings();
617  $this->assertNotEquals( $oldToken, $user->getToken() );
618  $this->assertSame( 'de', $user->getOption( 'language' ) );
619  $this->assertSame( null, $user->getOption( 'variant' ) );
620  }
621 
623  $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
624  $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
625  $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
626  $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
627  $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
628  $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
629  $this->primaryauthMocks = [ $mockA ];
630 
631  $this->logger = new \TestLogger( true );
632 
633  // Test without first initializing the configured providers
634  $this->initializeManager();
635  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
636  $this->assertSame(
637  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
638  );
639  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
640  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
641  $this->assertSame( [
642  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
643  ], $this->logger->getBuffer() );
644  $this->logger->clearBuffer();
645 
646  // Test with first initializing the configured providers
647  $this->initializeManager();
648  $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
649  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
650  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
651  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
652  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
653  $this->assertSame(
654  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
655  );
656  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
657  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
658  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
659  $this->assertNull(
660  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
661  );
662  $this->assertSame( [
663  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
664  [
665  LogLevel::WARNING,
666  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
667  ],
668  ], $this->logger->getBuffer() );
669  $this->logger->clearBuffer();
670 
671  // Test duplicate IDs
672  $this->initializeManager();
673  try {
674  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
675  $this->fail( 'Expected exception not thrown' );
676  } catch ( \RuntimeException $ex ) {
677  $class1 = get_class( $mockB );
678  $class2 = get_class( $mockB2 );
679  $this->assertSame(
680  "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
681  );
682  }
683 
684  // Wrong classes
685  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
686  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
687  $class = get_class( $mock );
688  try {
689  $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
690  $this->fail( 'Expected exception not thrown' );
691  } catch ( \RuntimeException $ex ) {
692  $this->assertSame(
693  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
694  $ex->getMessage()
695  );
696  }
697 
698  }
699 
700  public function testBeginAuthentication() {
701  $this->initializeManager();
702 
703  // Immutable session
704  list( $provider, $reset ) = $this->getMockSessionProvider( false );
705  $this->hook( 'UserLoggedIn', $this->never() );
706  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
707  try {
708  $this->manager->beginAuthentication( [], 'http://localhost/' );
709  $this->fail( 'Expected exception not thrown' );
710  } catch ( \LogicException $ex ) {
711  $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
712  }
713  $this->unhook( 'UserLoggedIn' );
714  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
715  \ScopedCallback::consume( $reset );
716  $this->initializeManager( true );
717 
718  // CreatedAccountAuthenticationRequest
719  $user = \User::newFromName( 'UTSysop' );
720  $reqs = [
721  new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
722  ];
723  $this->hook( 'UserLoggedIn', $this->never() );
724  try {
725  $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
726  $this->fail( 'Expected exception not thrown' );
727  } catch ( \LogicException $ex ) {
728  $this->assertSame(
729  'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
730  'that created the account',
731  $ex->getMessage()
732  );
733  }
734  $this->unhook( 'UserLoggedIn' );
735 
736  $this->request->getSession()->clear();
737  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
738  $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
739  $this->hook( 'UserLoggedIn', $this->once() )
740  ->with( $this->callback( function ( $u ) use ( $user ) {
741  return $user->getId() === $u->getId() && $user->getName() === $u->getName();
742  } ) );
743  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
744  $this->logger->setCollect( true );
745  $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
746  $this->logger->setCollect( false );
747  $this->unhook( 'UserLoggedIn' );
748  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
749  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
750  $this->assertSame( $user->getName(), $ret->username );
751  $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
752  $this->assertEquals(
753  time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
754  'timestamp ±1', 1
755  );
756  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
757  $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
758  $this->assertSame( [
759  [ LogLevel::INFO, 'Logging in {user} after account creation' ],
760  ], $this->logger->getBuffer() );
761  }
762 
763  public function testCreateFromLogin() {
764  $user = \User::newFromName( 'UTSysop' );
765  $req1 = $this->getMock( AuthenticationRequest::class );
766  $req2 = $this->getMock( AuthenticationRequest::class );
767  $req3 = $this->getMock( AuthenticationRequest::class );
768  $userReq = new UsernameAuthenticationRequest;
769  $userReq->username = 'UTDummy';
770 
771  $req1->returnToUrl = 'http://localhost/';
772  $req2->returnToUrl = 'http://localhost/';
773  $req3->returnToUrl = 'http://localhost/';
774  $req3->username = 'UTDummy';
775  $userReq->returnToUrl = 'http://localhost/';
776 
777  // Passing one into beginAuthentication(), and an immediate FAIL
778  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
779  $this->primaryauthMocks = [ $primary ];
780  $this->initializeManager( true );
782  $res->createRequest = $req1;
783  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
784  ->will( $this->returnValue( $res ) );
785  $createReq = new CreateFromLoginAuthenticationRequest(
786  null, [ $req2->getUniqueId() => $req2 ]
787  );
788  $this->logger->setCollect( true );
789  $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
790  $this->logger->setCollect( false );
791  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
792  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
793  $this->assertSame( $req1, $ret->createRequest->createRequest );
794  $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
795 
796  // UI, then FAIL in beginAuthentication()
797  $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
798  ->setMethods( [ 'continuePrimaryAuthentication' ] )
799  ->getMockForAbstractClass();
800  $this->primaryauthMocks = [ $primary ];
801  $this->initializeManager( true );
802  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
803  ->will( $this->returnValue(
804  AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
805  ) );
807  $res->createRequest = $req2;
808  $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
809  ->will( $this->returnValue( $res ) );
810  $this->logger->setCollect( true );
811  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
812  $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
813  $ret = $this->manager->continueAuthentication( [] );
814  $this->logger->setCollect( false );
815  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
816  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
817  $this->assertSame( $req2, $ret->createRequest->createRequest );
818  $this->assertEquals( [], $ret->createRequest->maybeLink );
819 
820  // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
821  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
822  $this->primaryauthMocks = [ $primary ];
823  $this->initializeManager( true );
824  $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
825  $createReq->returnToUrl = 'http://localhost/';
826  $createReq->username = 'UTDummy';
827  $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
828  $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
829  ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
830  ->will( $this->returnValue( $res ) );
831  $primary->expects( $this->any() )->method( 'accountCreationType' )
832  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
833  $this->logger->setCollect( true );
834  $ret = $this->manager->beginAccountCreation(
835  $user, [ $userReq, $createReq ], 'http://localhost/'
836  );
837  $this->logger->setCollect( false );
838  $this->assertSame( AuthenticationResponse::UI, $ret->status );
839  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
840  $this->assertNotNull( $state );
841  $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
842  $this->assertEquals( [ $req2 ], $state['maybeLink'] );
843  }
844 
853  public function testAuthentication(
854  StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
855  array $managerResponses, $link = false
856  ) {
857  $this->initializeManager();
858  $user = \User::newFromName( 'UTSysop' );
859  $id = $user->getId();
860  $name = $user->getName();
861 
862  // Set up lots of mocks...
864  $req->rememberMe = (bool)rand( 0, 1 );
865  $req->pre = $preResponse;
866  $req->primary = $primaryResponses;
867  $req->secondary = $secondaryResponses;
868  $mocks = [];
869  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
870  $class = ucfirst( $key ) . 'AuthenticationProvider';
871  $mocks[$key] = $this->getMockForAbstractClass(
872  "MediaWiki\\Auth\\$class", [], "Mock$class"
873  );
874  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
875  ->will( $this->returnValue( $key ) );
876  $mocks[$key . '2'] = $this->getMockForAbstractClass(
877  "MediaWiki\\Auth\\$class", [], "Mock$class"
878  );
879  $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
880  ->will( $this->returnValue( $key . '2' ) );
881  $mocks[$key . '3'] = $this->getMockForAbstractClass(
882  "MediaWiki\\Auth\\$class", [], "Mock$class"
883  );
884  $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
885  ->will( $this->returnValue( $key . '3' ) );
886  }
887  foreach ( $mocks as $mock ) {
888  $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
889  ->will( $this->returnValue( [] ) );
890  }
891 
892  $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
893  ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
894  $this->assertContains( $req, $reqs );
895  return $req->pre;
896  } ) );
897 
898  $ct = count( $req->primary );
899  $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
900  $this->assertContains( $req, $reqs );
901  return array_shift( $req->primary );
902  } );
903  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
904  ->method( 'beginPrimaryAuthentication' )
905  ->will( $callback );
906  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
907  ->method( 'continuePrimaryAuthentication' )
908  ->will( $callback );
909  if ( $link ) {
910  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
911  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
912  }
913 
914  $ct = count( $req->secondary );
915  $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
916  $this->assertSame( $id, $user->getId() );
917  $this->assertSame( $name, $user->getName() );
918  $this->assertContains( $req, $reqs );
919  return array_shift( $req->secondary );
920  } );
921  $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
922  ->method( 'beginSecondaryAuthentication' )
923  ->will( $callback );
924  $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
925  ->method( 'continueSecondaryAuthentication' )
926  ->will( $callback );
927 
929  $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
930  ->will( $this->returnValue( StatusValue::newGood() ) );
931  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
932  ->will( $this->returnValue( $abstain ) );
933  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
934  $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
935  ->will( $this->returnValue( $abstain ) );
936  $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
937  $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
938  ->will( $this->returnValue( $abstain ) );
939  $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
940 
941  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
942  $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
943  $this->secondaryauthMocks = [
944  $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
945  // So linking happens
947  ];
948  $this->initializeManager( true );
949  $this->logger->setCollect( true );
950 
951  $constraint = \PHPUnit_Framework_Assert::logicalOr(
952  $this->equalTo( AuthenticationResponse::PASS ),
953  $this->equalTo( AuthenticationResponse::FAIL )
954  );
955  $providers = array_filter(
956  array_merge(
957  $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
958  ),
959  function ( $p ) {
960  return is_callable( [ $p, 'expects' ] );
961  }
962  );
963  foreach ( $providers as $p ) {
964  $p->postCalled = false;
965  $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
966  ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
967  if ( $user !== null ) {
968  $this->assertInstanceOf( 'User', $user );
969  $this->assertSame( 'UTSysop', $user->getName() );
970  }
971  $this->assertInstanceOf( AuthenticationResponse::class, $response );
972  $this->assertThat( $response->status, $constraint );
973  $p->postCalled = $response->status;
974  } );
975  }
976 
977  $session = $this->request->getSession();
978  $session->setRememberUser( !$req->rememberMe );
979 
980  foreach ( $managerResponses as $i => $response ) {
983  if ( $success ) {
984  $this->hook( 'UserLoggedIn', $this->once() )
985  ->with( $this->callback( function ( $user ) use ( $id, $name ) {
986  return $user->getId() === $id && $user->getName() === $name;
987  } ) );
988  } else {
989  $this->hook( 'UserLoggedIn', $this->never() );
990  }
991  if ( $success || (
992  $response instanceof AuthenticationResponse &&
994  $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
995  $response->message->getKey() !== 'authmanager-authn-no-primary'
996  )
997  ) {
998  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
999  } else {
1000  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1001  }
1002 
1003  $ex = null;
1004  try {
1005  if ( !$i ) {
1006  $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1007  } else {
1008  $ret = $this->manager->continueAuthentication( [ $req ] );
1009  }
1010  if ( $response instanceof \Exception ) {
1011  $this->fail( 'Expected exception not thrown', "Response $i" );
1012  }
1013  } catch ( \Exception $ex ) {
1014  if ( !$response instanceof \Exception ) {
1015  throw $ex;
1016  }
1017  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1018  $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1019  "Response $i, exception, session state" );
1020  $this->unhook( 'UserLoggedIn' );
1021  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1022  return;
1023  }
1024 
1025  $this->unhook( 'UserLoggedIn' );
1026  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1027 
1028  $this->assertSame( 'http://localhost/', $req->returnToUrl );
1029 
1030  $ret->message = $this->message( $ret->message );
1031  $this->assertEquals( $response, $ret, "Response $i, response" );
1032  if ( $success ) {
1033  $this->assertSame( $id, $session->getUser()->getId(),
1034  "Response $i, authn" );
1035  } else {
1036  $this->assertSame( 0, $session->getUser()->getId(),
1037  "Response $i, authn" );
1038  }
1039  if ( $success || $response->status === AuthenticationResponse::FAIL ) {
1040  $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1041  "Response $i, session state" );
1042  foreach ( $providers as $p ) {
1043  $this->assertSame( $response->status, $p->postCalled,
1044  "Response $i, post-auth callback called" );
1045  }
1046  } else {
1047  $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1048  "Response $i, session state" );
1049  foreach ( $ret->neededRequests as $neededReq ) {
1050  $this->assertEquals( AuthManager::ACTION_LOGIN, $neededReq->action,
1051  "Response $i, neededRequest action" );
1052  }
1053  $this->assertEquals(
1054  $ret->neededRequests,
1055  $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
1056  "Response $i, continuation check"
1057  );
1058  foreach ( $providers as $p ) {
1059  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
1060  }
1061  }
1062 
1063  $state = $session->getSecret( 'AuthManager::authnState' );
1064  $maybeLink = isset( $state['maybeLink'] ) ? $state['maybeLink'] : [];
1065  if ( $link && $response->status === AuthenticationResponse::RESTART ) {
1066  $this->assertEquals(
1067  $response->createRequest->maybeLink,
1068  $maybeLink,
1069  "Response $i, maybeLink"
1070  );
1071  } else {
1072  $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1073  }
1074  }
1075 
1076  if ( $success ) {
1077  $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1078  'rememberMe checkbox had effect' );
1079  } else {
1080  $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
1081  'rememberMe checkbox wasn\'t applied' );
1082  }
1083  }
1084 
1085  public function provideAuthentication() {
1086  $user = \User::newFromName( 'UTSysop' );
1087  $id = $user->getId();
1088  $name = $user->getName();
1089 
1090  $rememberReq = new RememberMeAuthenticationRequest;
1091  $rememberReq->action = AuthManager::ACTION_LOGIN;
1092 
1093  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1094  $req->foobar = 'baz';
1095  $restartResponse = AuthenticationResponse::newRestart(
1096  $this->message( 'authmanager-authn-no-local-user' )
1097  );
1098  $restartResponse->neededRequests = [ $rememberReq ];
1099 
1100  $restartResponse2Pass = AuthenticationResponse::newPass( null );
1101  $restartResponse2Pass->linkRequest = $req;
1102  $restartResponse2 = AuthenticationResponse::newRestart(
1103  $this->message( 'authmanager-authn-no-local-user-link' )
1104  );
1105  $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1106  null, [ $req->getUniqueId() => $req ]
1107  );
1108  $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
1109  $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
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  [
1206  ],
1207  $tmp = [
1208  AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1209  ],
1210  $tmp
1211  ],
1212  'Secondary UI, then abstain' => [
1214  [
1216  ],
1217  [
1218  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1220  ],
1221  [
1222  $tmp,
1224  ]
1225  ],
1226  'Secondary pass' => [
1228  [
1230  ],
1231  [
1233  ],
1234  [
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 
1395  global $wgGroupPermissions;
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  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1408  $this->assertEquals(
1409  \Status::newFatal( 'readonlytext', 'Because' ),
1410  $this->manager->checkAccountCreatePermissions( new \User )
1411  );
1412  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1413 
1414  $wgGroupPermissions['*']['createaccount'] = false;
1415  $status = $this->manager->checkAccountCreatePermissions( new \User );
1416  $this->assertFalse( $status->isOK() );
1417  $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1418  $wgGroupPermissions['*']['createaccount'] = true;
1419 
1420  $user = \User::newFromName( 'UTBlockee' );
1421  if ( $user->getID() == 0 ) {
1422  $user->addToDatabase();
1423  \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1424  $user->saveSettings();
1425  }
1426  $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1427  if ( $oldBlock ) {
1428  // An old block will prevent our new one from saving.
1429  $oldBlock->delete();
1430  }
1431  $blockOptions = [
1432  'address' => 'UTBlockee',
1433  'user' => $user->getID(),
1434  'reason' => __METHOD__,
1435  'expiry' => time() + 100500,
1436  'createAccount' => true,
1437  ];
1438  $block = new \Block( $blockOptions );
1439  $block->insert();
1440  $status = $this->manager->checkAccountCreatePermissions( $user );
1441  $this->assertFalse( $status->isOK() );
1442  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1443 
1444  $blockOptions = [
1445  'address' => '127.0.0.0/24',
1446  'reason' => __METHOD__,
1447  'expiry' => time() + 100500,
1448  'createAccount' => true,
1449  ];
1450  $block = new \Block( $blockOptions );
1451  $block->insert();
1452  $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
1453  $status = $this->manager->checkAccountCreatePermissions( new \User );
1454  $this->assertFalse( $status->isOK() );
1455  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1456  \ScopedCallback::consume( $scopeVariable );
1457 
1458  $this->setMwGlobals( [
1459  'wgEnableDnsBlacklist' => true,
1460  'wgDnsBlacklistUrls' => [
1461  'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1462  ],
1463  'wgProxyWhitelist' => [],
1464  ] );
1465  $status = $this->manager->checkAccountCreatePermissions( new \User );
1466  $this->assertFalse( $status->isOK() );
1467  $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1468  $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1469  $status = $this->manager->checkAccountCreatePermissions( new \User );
1470  $this->assertTrue( $status->isGood() );
1471  }
1472 
1477  private static function usernameForCreation( $uniq = '' ) {
1478  $i = 0;
1479  do {
1480  $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1481  } while ( \User::newFromName( $username )->getId() !== 0 );
1482  return $username;
1483  }
1484 
1485  public function testCanCreateAccount() {
1486  $username = self::usernameForCreation();
1487  $this->initializeManager();
1488 
1489  $this->assertEquals(
1490  \Status::newFatal( 'authmanager-create-disabled' ),
1491  $this->manager->canCreateAccount( $username )
1492  );
1493 
1494  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1495  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1496  $mock->expects( $this->any() )->method( 'accountCreationType' )
1497  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1498  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1499  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1500  ->will( $this->returnValue( StatusValue::newGood() ) );
1501  $this->primaryauthMocks = [ $mock ];
1502  $this->initializeManager( true );
1503 
1504  $this->assertEquals(
1505  \Status::newFatal( 'userexists' ),
1506  $this->manager->canCreateAccount( $username )
1507  );
1508 
1509  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1510  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1511  $mock->expects( $this->any() )->method( 'accountCreationType' )
1512  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1513  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1514  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1515  ->will( $this->returnValue( StatusValue::newGood() ) );
1516  $this->primaryauthMocks = [ $mock ];
1517  $this->initializeManager( true );
1518 
1519  $this->assertEquals(
1520  \Status::newFatal( 'noname' ),
1521  $this->manager->canCreateAccount( $username . '<>' )
1522  );
1523 
1524  $this->assertEquals(
1525  \Status::newFatal( 'userexists' ),
1526  $this->manager->canCreateAccount( 'UTSysop' )
1527  );
1528 
1529  $this->assertEquals(
1530  \Status::newGood(),
1531  $this->manager->canCreateAccount( $username )
1532  );
1533 
1534  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1535  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1536  $mock->expects( $this->any() )->method( 'accountCreationType' )
1537  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1538  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1539  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1540  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1541  $this->primaryauthMocks = [ $mock ];
1542  $this->initializeManager( true );
1543 
1544  $this->assertEquals(
1545  \Status::newFatal( 'fail' ),
1546  $this->manager->canCreateAccount( $username )
1547  );
1548  }
1549 
1550  public function testBeginAccountCreation() {
1551  $creator = \User::newFromName( 'UTSysop' );
1552  $userReq = new UsernameAuthenticationRequest;
1553  $this->logger = new \TestLogger( false, function ( $message, $level ) {
1554  return $level === LogLevel::DEBUG ? null : $message;
1555  } );
1556  $this->initializeManager();
1557 
1558  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1559  $this->hook( 'LocalUserCreated', $this->never() );
1560  try {
1561  $this->manager->beginAccountCreation(
1562  $creator, [], 'http://localhost/'
1563  );
1564  $this->fail( 'Expected exception not thrown' );
1565  } catch ( \LogicException $ex ) {
1566  $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1567  }
1568  $this->unhook( 'LocalUserCreated' );
1569  $this->assertNull(
1570  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1571  );
1572 
1573  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1574  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1575  $mock->expects( $this->any() )->method( 'accountCreationType' )
1576  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1577  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1578  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1579  ->will( $this->returnValue( StatusValue::newGood() ) );
1580  $this->primaryauthMocks = [ $mock ];
1581  $this->initializeManager( true );
1582 
1583  $this->hook( 'LocalUserCreated', $this->never() );
1584  $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
1585  $this->unhook( 'LocalUserCreated' );
1586  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1587  $this->assertSame( 'noname', $ret->message->getKey() );
1588 
1589  $this->hook( 'LocalUserCreated', $this->never() );
1590  $userReq->username = self::usernameForCreation();
1591  $userReq2 = new UsernameAuthenticationRequest;
1592  $userReq2->username = $userReq->username . 'X';
1593  $ret = $this->manager->beginAccountCreation(
1594  $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1595  );
1596  $this->unhook( 'LocalUserCreated' );
1597  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1598  $this->assertSame( 'noname', $ret->message->getKey() );
1599 
1600  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1601  $this->hook( 'LocalUserCreated', $this->never() );
1602  $userReq->username = self::usernameForCreation();
1603  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1604  $this->unhook( 'LocalUserCreated' );
1605  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1606  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1607  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1608  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1609 
1610  $this->hook( 'LocalUserCreated', $this->never() );
1611  $userReq->username = self::usernameForCreation();
1612  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1613  $this->unhook( 'LocalUserCreated' );
1614  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1615  $this->assertSame( 'userexists', $ret->message->getKey() );
1616 
1617  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1618  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1619  $mock->expects( $this->any() )->method( 'accountCreationType' )
1620  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1621  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1622  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1623  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1624  $this->primaryauthMocks = [ $mock ];
1625  $this->initializeManager( true );
1626 
1627  $this->hook( 'LocalUserCreated', $this->never() );
1628  $userReq->username = self::usernameForCreation();
1629  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1630  $this->unhook( 'LocalUserCreated' );
1631  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1632  $this->assertSame( 'fail', $ret->message->getKey() );
1633 
1634  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1635  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1636  $mock->expects( $this->any() )->method( 'accountCreationType' )
1637  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1638  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1639  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1640  ->will( $this->returnValue( StatusValue::newGood() ) );
1641  $this->primaryauthMocks = [ $mock ];
1642  $this->initializeManager( true );
1643 
1644  $this->hook( 'LocalUserCreated', $this->never() );
1645  $userReq->username = self::usernameForCreation() . '<>';
1646  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1647  $this->unhook( 'LocalUserCreated' );
1648  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1649  $this->assertSame( 'noname', $ret->message->getKey() );
1650 
1651  $this->hook( 'LocalUserCreated', $this->never() );
1652  $userReq->username = $creator->getName();
1653  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1654  $this->unhook( 'LocalUserCreated' );
1655  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1656  $this->assertSame( 'userexists', $ret->message->getKey() );
1657 
1658  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1659  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1660  $mock->expects( $this->any() )->method( 'accountCreationType' )
1661  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1662  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1663  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1664  ->will( $this->returnValue( StatusValue::newGood() ) );
1665  $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1666  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1667  $this->primaryauthMocks = [ $mock ];
1668  $this->initializeManager( true );
1669 
1670  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1671  ->setMethods( [ 'populateUser' ] )
1672  ->getMock();
1673  $req->expects( $this->any() )->method( 'populateUser' )
1674  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1675  $userReq->username = self::usernameForCreation();
1676  $ret = $this->manager->beginAccountCreation(
1677  $creator, [ $userReq, $req ], 'http://localhost/'
1678  );
1679  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1680  $this->assertSame( 'populatefail', $ret->message->getKey() );
1681 
1683  $userReq->username = self::usernameForCreation();
1684 
1685  $ret = $this->manager->beginAccountCreation(
1686  $creator, [ $userReq, $req ], 'http://localhost/'
1687  );
1688  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1689  $this->assertSame( 'fail', $ret->message->getKey() );
1690 
1691  $this->manager->beginAccountCreation(
1692  \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1693  );
1694  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1695  $this->assertSame( 'fail', $ret->message->getKey() );
1696  }
1697 
1698  public function testContinueAccountCreation() {
1699  $creator = \User::newFromName( 'UTSysop' );
1700  $username = self::usernameForCreation();
1701  $this->logger = new \TestLogger( false, function ( $message, $level ) {
1702  return $level === LogLevel::DEBUG ? null : $message;
1703  } );
1704  $this->initializeManager();
1705 
1706  $session = [
1707  'userid' => 0,
1708  'username' => $username,
1709  'creatorid' => 0,
1710  'creatorname' => $username,
1711  'reqs' => [],
1712  'primary' => null,
1713  'primaryResponse' => null,
1714  'secondary' => [],
1715  'ranPreTests' => true,
1716  ];
1717 
1718  $this->hook( 'LocalUserCreated', $this->never() );
1719  try {
1720  $this->manager->continueAccountCreation( [] );
1721  $this->fail( 'Expected exception not thrown' );
1722  } catch ( \LogicException $ex ) {
1723  $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1724  }
1725  $this->unhook( 'LocalUserCreated' );
1726 
1727  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1728  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1729  $mock->expects( $this->any() )->method( 'accountCreationType' )
1730  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1731  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1732  $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1733  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1734  );
1735  $this->primaryauthMocks = [ $mock ];
1736  $this->initializeManager( true );
1737 
1738  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1739  $this->hook( 'LocalUserCreated', $this->never() );
1740  $ret = $this->manager->continueAccountCreation( [] );
1741  $this->unhook( 'LocalUserCreated' );
1742  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1743  $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1744 
1745  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1746  [ 'username' => "$username<>" ] + $session );
1747  $this->hook( 'LocalUserCreated', $this->never() );
1748  $ret = $this->manager->continueAccountCreation( [] );
1749  $this->unhook( 'LocalUserCreated' );
1750  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1751  $this->assertSame( 'noname', $ret->message->getKey() );
1752  $this->assertNull(
1753  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1754  );
1755 
1756  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1757  $this->hook( 'LocalUserCreated', $this->never() );
1759  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1760  $ret = $this->manager->continueAccountCreation( [] );
1761  unset( $lock );
1762  $this->unhook( 'LocalUserCreated' );
1763  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1764  $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1765  // This error shouldn't remove the existing session, because the
1766  // raced-with process "owns" it.
1767  $this->assertSame(
1768  $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1769  );
1770 
1771  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1772  [ 'username' => $creator->getName() ] + $session );
1773  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1774  $this->hook( 'LocalUserCreated', $this->never() );
1775  $ret = $this->manager->continueAccountCreation( [] );
1776  $this->unhook( 'LocalUserCreated' );
1777  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1778  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1779  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1780  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1781 
1782  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1783  [ 'username' => $creator->getName() ] + $session );
1784  $this->hook( 'LocalUserCreated', $this->never() );
1785  $ret = $this->manager->continueAccountCreation( [] );
1786  $this->unhook( 'LocalUserCreated' );
1787  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1788  $this->assertSame( 'userexists', $ret->message->getKey() );
1789  $this->assertNull(
1790  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1791  );
1792 
1793  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1794  [ 'userid' => $creator->getId() ] + $session );
1795  $this->hook( 'LocalUserCreated', $this->never() );
1796  try {
1797  $ret = $this->manager->continueAccountCreation( [] );
1798  $this->fail( 'Expected exception not thrown' );
1799  } catch ( \UnexpectedValueException $ex ) {
1800  $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1801  }
1802  $this->unhook( 'LocalUserCreated' );
1803  $this->assertNull(
1804  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1805  );
1806 
1807  $id = $creator->getId();
1808  $name = $creator->getName();
1809  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1810  [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1811  $this->hook( 'LocalUserCreated', $this->never() );
1812  try {
1813  $ret = $this->manager->continueAccountCreation( [] );
1814  $this->fail( 'Expected exception not thrown' );
1815  } catch ( \UnexpectedValueException $ex ) {
1816  $this->assertEquals(
1817  "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
1818  );
1819  }
1820  $this->unhook( 'LocalUserCreated' );
1821  $this->assertNull(
1822  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1823  );
1824 
1825  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1826  ->setMethods( [ 'populateUser' ] )
1827  ->getMock();
1828  $req->expects( $this->any() )->method( 'populateUser' )
1829  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1830  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1831  [ 'reqs' => [ $req ] ] + $session );
1832  $ret = $this->manager->continueAccountCreation( [] );
1833  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1834  $this->assertSame( 'populatefail', $ret->message->getKey() );
1835  $this->assertNull(
1836  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1837  );
1838  }
1839 
1849  public function testAccountCreation(
1850  StatusValue $preTest, $primaryTest, $secondaryTest,
1851  array $primaryResponses, array $secondaryResponses, array $managerResponses
1852  ) {
1853  $creator = \User::newFromName( 'UTSysop' );
1854  $username = self::usernameForCreation();
1855 
1856  $this->initializeManager();
1857 
1858  // Set up lots of mocks...
1859  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1860  $req->preTest = $preTest;
1861  $req->primaryTest = $primaryTest;
1862  $req->secondaryTest = $secondaryTest;
1863  $req->primary = $primaryResponses;
1864  $req->secondary = $secondaryResponses;
1865  $mocks = [];
1866  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1867  $class = ucfirst( $key ) . 'AuthenticationProvider';
1868  $mocks[$key] = $this->getMockForAbstractClass(
1869  "MediaWiki\\Auth\\$class", [], "Mock$class"
1870  );
1871  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1872  ->will( $this->returnValue( $key ) );
1873  $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1874  ->will( $this->returnValue( StatusValue::newGood() ) );
1875  $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1876  ->will( $this->returnCallback(
1877  function ( $user, $creatorIn, $reqs )
1878  use ( $username, $creator, $req, $key )
1879  {
1880  $this->assertSame( $username, $user->getName() );
1881  $this->assertSame( $creator->getId(), $creatorIn->getId() );
1882  $this->assertSame( $creator->getName(), $creatorIn->getName() );
1883  $foundReq = false;
1884  foreach ( $reqs as $r ) {
1885  $this->assertSame( $username, $r->username );
1886  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1887  }
1888  $this->assertTrue( $foundReq, '$reqs contains $req' );
1889  $k = $key . 'Test';
1890  return $req->$k;
1891  }
1892  ) );
1893 
1894  for ( $i = 2; $i <= 3; $i++ ) {
1895  $mocks[$key . $i] = $this->getMockForAbstractClass(
1896  "MediaWiki\\Auth\\$class", [], "Mock$class"
1897  );
1898  $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1899  ->will( $this->returnValue( $key . $i ) );
1900  $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1901  ->will( $this->returnValue( StatusValue::newGood() ) );
1902  $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1903  ->will( $this->returnValue( StatusValue::newGood() ) );
1904  }
1905  }
1906 
1907  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1908  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1909  $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1910  ->will( $this->returnValue( false ) );
1911  $ct = count( $req->primary );
1912  $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1913  $this->assertSame( $username, $user->getName() );
1914  $this->assertSame( 'UTSysop', $creator->getName() );
1915  $foundReq = false;
1916  foreach ( $reqs as $r ) {
1917  $this->assertSame( $username, $r->username );
1918  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1919  }
1920  $this->assertTrue( $foundReq, '$reqs contains $req' );
1921  return array_shift( $req->primary );
1922  } );
1923  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1924  ->method( 'beginPrimaryAccountCreation' )
1925  ->will( $callback );
1926  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1927  ->method( 'continuePrimaryAccountCreation' )
1928  ->will( $callback );
1929 
1930  $ct = count( $req->secondary );
1931  $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1932  $this->assertSame( $username, $user->getName() );
1933  $this->assertSame( 'UTSysop', $creator->getName() );
1934  $foundReq = false;
1935  foreach ( $reqs as $r ) {
1936  $this->assertSame( $username, $r->username );
1937  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1938  }
1939  $this->assertTrue( $foundReq, '$reqs contains $req' );
1940  return array_shift( $req->secondary );
1941  } );
1942  $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1943  ->method( 'beginSecondaryAccountCreation' )
1944  ->will( $callback );
1945  $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1946  ->method( 'continueSecondaryAccountCreation' )
1947  ->will( $callback );
1948 
1950  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1951  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
1952  $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1953  ->will( $this->returnValue( false ) );
1954  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1955  ->will( $this->returnValue( $abstain ) );
1956  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1957  $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1958  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
1959  $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1960  ->will( $this->returnValue( false ) );
1961  $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1962  $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1963  $mocks['secondary2']->expects( $this->atMost( 1 ) )
1964  ->method( 'beginSecondaryAccountCreation' )
1965  ->will( $this->returnValue( $abstain ) );
1966  $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1967  $mocks['secondary3']->expects( $this->atMost( 1 ) )
1968  ->method( 'beginSecondaryAccountCreation' )
1969  ->will( $this->returnValue( $abstain ) );
1970  $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1971 
1972  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
1973  $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1974  $this->secondaryauthMocks = [
1975  $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1976  ];
1977 
1978  $this->logger = new \TestLogger( true, function ( $message, $level ) {
1979  return $level === LogLevel::DEBUG ? null : $message;
1980  } );
1981  $expectLog = [];
1982  $this->initializeManager( true );
1983 
1984  $constraint = \PHPUnit_Framework_Assert::logicalOr(
1985  $this->equalTo( AuthenticationResponse::PASS ),
1986  $this->equalTo( AuthenticationResponse::FAIL )
1987  );
1988  $providers = array_merge(
1989  $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
1990  );
1991  foreach ( $providers as $p ) {
1992  $p->postCalled = false;
1993  $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
1994  ->willReturnCallback( function ( $user, $creator, $response )
1995  use ( $constraint, $p, $username )
1996  {
1997  $this->assertInstanceOf( 'User', $user );
1998  $this->assertSame( $username, $user->getName() );
1999  $this->assertSame( 'UTSysop', $creator->getName() );
2000  $this->assertInstanceOf( AuthenticationResponse::class, $response );
2001  $this->assertThat( $response->status, $constraint );
2002  $p->postCalled = $response->status;
2003  } );
2004  }
2005 
2006  // We're testing with $wgNewUserLog = false, so assert that it worked
2007  $dbw = wfGetDB( DB_MASTER );
2008  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2009 
2010  $first = true;
2011  $created = false;
2012  foreach ( $managerResponses as $i => $response ) {
2013  $success = $response instanceof AuthenticationResponse &&
2015  if ( $i === 'created' ) {
2016  $created = true;
2017  $this->hook( 'LocalUserCreated', $this->once() )
2018  ->with(
2019  $this->callback( function ( $user ) use ( $username ) {
2020  return $user->getName() === $username;
2021  } ),
2022  $this->equalTo( false )
2023  );
2024  $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2025  } else {
2026  $this->hook( 'LocalUserCreated', $this->never() );
2027  }
2028 
2029  $ex = null;
2030  try {
2031  if ( $first ) {
2032  $userReq = new UsernameAuthenticationRequest;
2033  $userReq->username = $username;
2034  $ret = $this->manager->beginAccountCreation(
2035  $creator, [ $userReq, $req ], 'http://localhost/'
2036  );
2037  } else {
2038  $ret = $this->manager->continueAccountCreation( [ $req ] );
2039  }
2040  if ( $response instanceof \Exception ) {
2041  $this->fail( 'Expected exception not thrown', "Response $i" );
2042  }
2043  } catch ( \Exception $ex ) {
2044  if ( !$response instanceof \Exception ) {
2045  throw $ex;
2046  }
2047  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2048  $this->assertNull(
2049  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2050  "Response $i, exception, session state"
2051  );
2052  $this->unhook( 'LocalUserCreated' );
2053  return;
2054  }
2055 
2056  $this->unhook( 'LocalUserCreated' );
2057 
2058  $this->assertSame( 'http://localhost/', $req->returnToUrl );
2059 
2060  if ( $success ) {
2061  $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2062  $this->assertContains(
2063  $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2064  "Response $i, login marker"
2065  );
2066 
2067  $expectLog[] = [
2068  LogLevel::INFO,
2069  "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2070  ];
2071 
2072  // Set some fields in the expected $response that we couldn't
2073  // know in provideAccountCreation().
2074  $response->username = $username;
2075  $response->loginRequest = $ret->loginRequest;
2076  } else {
2077  $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
2078  $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
2079  "Response $i, login marker" );
2080  }
2081  $ret->message = $this->message( $ret->message );
2082  $this->assertEquals( $response, $ret, "Response $i, response" );
2083  if ( $success || $response->status === AuthenticationResponse::FAIL ) {
2084  $this->assertNull(
2085  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2086  "Response $i, session state"
2087  );
2088  foreach ( $providers as $p ) {
2089  $this->assertSame( $response->status, $p->postCalled,
2090  "Response $i, post-auth callback called" );
2091  }
2092  } else {
2093  $this->assertNotNull(
2094  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2095  "Response $i, session state"
2096  );
2097  foreach ( $ret->neededRequests as $neededReq ) {
2098  $this->assertEquals( AuthManager::ACTION_CREATE, $neededReq->action,
2099  "Response $i, neededRequest action" );
2100  }
2101  $this->assertEquals(
2102  $ret->neededRequests,
2103  $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
2104  "Response $i, continuation check"
2105  );
2106  foreach ( $providers as $p ) {
2107  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
2108  }
2109  }
2110 
2111  if ( $created ) {
2112  $this->assertNotEquals( 0, \User::idFromName( $username ) );
2113  } else {
2114  $this->assertEquals( 0, \User::idFromName( $username ) );
2115  }
2116 
2117  $first = false;
2118  }
2119 
2120  $this->assertSame( $expectLog, $this->logger->getBuffer() );
2121 
2122  $this->assertSame(
2123  $maxLogId,
2124  $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2125  );
2126  }
2127 
2128  public function provideAccountCreation() {
2129  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2130  $good = StatusValue::newGood();
2131 
2132  return [
2133  'Pre-creation test fail in pre' => [
2134  StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
2135  [],
2136  [],
2137  [
2138  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
2139  ]
2140  ],
2141  'Pre-creation test fail in primary' => [
2142  $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
2143  [],
2144  [],
2145  [
2146  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2147  ]
2148  ],
2149  'Pre-creation test fail in secondary' => [
2150  $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
2151  [],
2152  [],
2153  [
2154  AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
2155  ]
2156  ],
2157  'Failure in primary' => [
2158  $good, $good, $good,
2159  $tmp = [
2160  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2161  ],
2162  [],
2163  $tmp
2164  ],
2165  'All primary abstain' => [
2166  $good, $good, $good,
2167  [
2169  ],
2170  [],
2171  [
2172  AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
2173  ]
2174  ],
2175  'Primary UI, then redirect, then fail' => [
2176  $good, $good, $good,
2177  $tmp = [
2178  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2179  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2180  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2181  ],
2182  [],
2183  $tmp
2184  ],
2185  'Primary redirect, then abstain' => [
2186  $good, $good, $good,
2187  [
2189  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2190  ),
2192  ],
2193  [],
2194  [
2195  $tmp,
2196  new \DomainException(
2197  'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2198  )
2199  ]
2200  ],
2201  'Primary UI, then pass; secondary abstain' => [
2202  $good, $good, $good,
2203  [
2204  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2206  ],
2207  [
2209  ],
2210  [
2211  $tmp1,
2212  'created' => AuthenticationResponse::newPass( '' ),
2213  ]
2214  ],
2215  'Primary pass; secondary UI then pass' => [
2216  $good, $good, $good,
2217  [
2219  ],
2220  [
2221  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2223  ],
2224  [
2225  'created' => $tmp1,
2227  ]
2228  ],
2229  'Primary pass; secondary fail' => [
2230  $good, $good, $good,
2231  [
2233  ],
2234  [
2235  AuthenticationResponse::newFail( $this->message( '...' ) ),
2236  ],
2237  [
2238  'created' => new \DomainException(
2239  'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2240  'Secondary providers are not allowed to fail account creation, ' .
2241  'that should have been done via testForAccountCreation().'
2242  )
2243  ]
2244  ],
2245  ];
2246  }
2247 
2253  public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2254  $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
2255  $username = self::usernameForCreation();
2256 
2257  $this->initializeManager();
2258 
2259  // Set up lots of mocks...
2260  $mock = $this->getMockForAbstractClass(
2261  "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
2262  );
2263  $mock->expects( $this->any() )->method( 'getUniqueId' )
2264  ->will( $this->returnValue( 'primary' ) );
2265  $mock->expects( $this->any() )->method( 'testUserForCreation' )
2266  ->will( $this->returnValue( StatusValue::newGood() ) );
2267  $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2268  ->will( $this->returnValue( StatusValue::newGood() ) );
2269  $mock->expects( $this->any() )->method( 'accountCreationType' )
2270  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2271  $mock->expects( $this->any() )->method( 'testUserExists' )
2272  ->will( $this->returnValue( false ) );
2273  $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2274  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
2275  $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2276  ->will( $this->returnValue( $logSubtype ) );
2277 
2278  $this->primaryauthMocks = [ $mock ];
2279  $this->initializeManager( true );
2280  $this->logger->setCollect( true );
2281 
2282  $this->config->set( 'NewUserLog', true );
2283 
2284  $dbw = wfGetDB( DB_MASTER );
2285  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2286 
2287  $userReq = new UsernameAuthenticationRequest;
2288  $userReq->username = $username;
2289  $reasonReq = new CreationReasonAuthenticationRequest;
2290  $reasonReq->reason = $this->toString();
2291  $ret = $this->manager->beginAccountCreation(
2292  $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2293  );
2294 
2295  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
2296 
2298  $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2299  $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2300 
2302  $rows = iterator_to_array( $dbw->select(
2303  $data['tables'],
2304  $data['fields'],
2305  [
2306  'log_id > ' . (int)$maxLogId,
2307  'log_type' => 'newusers'
2308  ] + $data['conds'],
2309  __METHOD__,
2310  $data['options'],
2311  $data['join_conds']
2312  ) );
2313  $this->assertCount( 1, $rows );
2314  $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2315 
2316  $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
2317  $this->assertSame(
2318  $isAnon ? $user->getId() : $creator->getId(),
2319  $entry->getPerformer()->getId()
2320  );
2321  $this->assertSame(
2322  $isAnon ? $user->getName() : $creator->getName(),
2323  $entry->getPerformer()->getName()
2324  );
2325  $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2326  $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2327  $this->assertSame( $this->toString(), $entry->getComment() );
2328  }
2329 
2330  public static function provideAccountCreationLogging() {
2331  return [
2332  [ true, null ],
2333  [ true, 'foobar' ],
2334  [ false, null ],
2335  [ false, 'byemail' ],
2336  ];
2337  }
2338 
2339  public function testAutoAccountCreation() {
2340  global $wgGroupPermissions, $wgHooks;
2341 
2342  // PHPUnit seems to have a bug where it will call the ->with()
2343  // callbacks for our hooks again after the test is run (WTF?), which
2344  // breaks here because $username no longer matches $user by the end of
2345  // the testing.
2346  $workaroundPHPUnitBug = false;
2347 
2348  $username = self::usernameForCreation();
2349  $this->initializeManager();
2350 
2351  $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2352  $wgGroupPermissions['*']['createaccount'] = true;
2353  $wgGroupPermissions['*']['autocreateaccount'] = false;
2354 
2355  \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
2356  $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2357 
2358  // Set up lots of mocks...
2359  $mocks = [];
2360  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2361  $class = ucfirst( $key ) . 'AuthenticationProvider';
2362  $mocks[$key] = $this->getMockForAbstractClass(
2363  "MediaWiki\\Auth\\$class", [], "Mock$class"
2364  );
2365  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2366  ->will( $this->returnValue( $key ) );
2367  }
2368 
2369  $good = StatusValue::newGood();
2370  $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2371  return $workaroundPHPUnitBug || $user->getName() === $username;
2372  } );
2373 
2374  $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2375  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2376  ->will( $this->onConsecutiveCalls(
2377  StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
2378  StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2379  $good, // backoff test
2380  $good, // addToDatabase fails test
2381  $good, // addToDatabase throws test
2382  $good, // addToDatabase exists test
2383  $good, $good, $good // success
2384  ) );
2385 
2386  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2387  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2388  $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2389  ->will( $this->returnValue( true ) );
2390  $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2391  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2392  ->will( $this->onConsecutiveCalls(
2393  StatusValue::newFatal( 'fail-in-primary' ), $good,
2394  $good, // backoff test
2395  $good, // addToDatabase fails test
2396  $good, // addToDatabase throws test
2397  $good, // addToDatabase exists test
2398  $good, $good, $good
2399  ) );
2400  $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2401  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2402 
2403  $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2404  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2405  ->will( $this->onConsecutiveCalls(
2406  StatusValue::newFatal( 'fail-in-secondary' ),
2407  $good, // backoff test
2408  $good, // addToDatabase fails test
2409  $good, // addToDatabase throws test
2410  $good, // addToDatabase exists test
2411  $good, $good, $good
2412  ) );
2413  $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2414  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2415 
2416  $this->preauthMocks = [ $mocks['pre'] ];
2417  $this->primaryauthMocks = [ $mocks['primary'] ];
2418  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2419  $this->initializeManager( true );
2420  $session = $this->request->getSession();
2421 
2422  $logger = new \TestLogger( true, function ( $m ) {
2423  $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2424  return $m;
2425  } );
2426  $this->manager->setLogger( $logger );
2427 
2428  try {
2429  $user = \User::newFromName( 'UTSysop' );
2430  $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2431  $this->fail( 'Expected exception not thrown' );
2432  } catch ( \InvalidArgumentException $ex ) {
2433  $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2434  }
2435 
2436  // First, check an existing user
2437  $session->clear();
2438  $user = \User::newFromName( 'UTSysop' );
2439  $this->hook( 'LocalUserCreated', $this->never() );
2440  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2441  $this->unhook( 'LocalUserCreated' );
2442  $expect = \Status::newGood();
2443  $expect->warning( 'userexists' );
2444  $this->assertEquals( $expect, $ret );
2445  $this->assertNotEquals( 0, $user->getId() );
2446  $this->assertSame( 'UTSysop', $user->getName() );
2447  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2448  $this->assertSame( [
2449  [ LogLevel::DEBUG, '{username} already exists locally' ],
2450  ], $logger->getBuffer() );
2451  $logger->clearBuffer();
2452 
2453  $session->clear();
2454  $user = \User::newFromName( 'UTSysop' );
2455  $this->hook( 'LocalUserCreated', $this->never() );
2456  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2457  $this->unhook( 'LocalUserCreated' );
2458  $expect = \Status::newGood();
2459  $expect->warning( 'userexists' );
2460  $this->assertEquals( $expect, $ret );
2461  $this->assertNotEquals( 0, $user->getId() );
2462  $this->assertSame( 'UTSysop', $user->getName() );
2463  $this->assertEquals( 0, $session->getUser()->getId() );
2464  $this->assertSame( [
2465  [ LogLevel::DEBUG, '{username} already exists locally' ],
2466  ], $logger->getBuffer() );
2467  $logger->clearBuffer();
2468 
2469  // Wiki is read-only
2470  $session->clear();
2471  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2473  $this->hook( 'LocalUserCreated', $this->never() );
2474  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2475  $this->unhook( 'LocalUserCreated' );
2476  $this->assertEquals( \Status::newFatal( 'readonlytext', 'Because' ), $ret );
2477  $this->assertEquals( 0, $user->getId() );
2478  $this->assertNotEquals( $username, $user->getName() );
2479  $this->assertEquals( 0, $session->getUser()->getId() );
2480  $this->assertSame( [
2481  [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2482  ], $logger->getBuffer() );
2483  $logger->clearBuffer();
2484  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2485 
2486  // Session blacklisted
2487  $session->clear();
2488  $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2490  $this->hook( 'LocalUserCreated', $this->never() );
2491  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2492  $this->unhook( 'LocalUserCreated' );
2493  $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2494  $this->assertEquals( 0, $user->getId() );
2495  $this->assertNotEquals( $username, $user->getName() );
2496  $this->assertEquals( 0, $session->getUser()->getId() );
2497  $this->assertSame( [
2498  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2499  ], $logger->getBuffer() );
2500  $logger->clearBuffer();
2501 
2502  $session->clear();
2503  $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2505  $this->hook( 'LocalUserCreated', $this->never() );
2506  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2507  $this->unhook( 'LocalUserCreated' );
2508  $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2509  $this->assertEquals( 0, $user->getId() );
2510  $this->assertNotEquals( $username, $user->getName() );
2511  $this->assertEquals( 0, $session->getUser()->getId() );
2512  $this->assertSame( [
2513  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2514  ], $logger->getBuffer() );
2515  $logger->clearBuffer();
2516 
2517  // Uncreatable name
2518  $session->clear();
2519  $user = \User::newFromName( $username . '@' );
2520  $this->hook( 'LocalUserCreated', $this->never() );
2521  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2522  $this->unhook( 'LocalUserCreated' );
2523  $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2524  $this->assertEquals( 0, $user->getId() );
2525  $this->assertNotEquals( $username . '@', $user->getId() );
2526  $this->assertEquals( 0, $session->getUser()->getId() );
2527  $this->assertSame( [
2528  [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2529  ], $logger->getBuffer() );
2530  $logger->clearBuffer();
2531  $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2532 
2533  // IP unable to create accounts
2534  $wgGroupPermissions['*']['createaccount'] = false;
2535  $wgGroupPermissions['*']['autocreateaccount'] = false;
2536  $session->clear();
2538  $this->hook( 'LocalUserCreated', $this->never() );
2539  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2540  $this->unhook( 'LocalUserCreated' );
2541  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2542  $this->assertEquals( 0, $user->getId() );
2543  $this->assertNotEquals( $username, $user->getName() );
2544  $this->assertEquals( 0, $session->getUser()->getId() );
2545  $this->assertSame( [
2546  [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2547  ], $logger->getBuffer() );
2548  $logger->clearBuffer();
2549  $this->assertSame(
2550  'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2551  );
2552 
2553  // Test that both permutations of permissions are allowed
2554  // (this hits the two "ok" entries in $mocks['pre'])
2555  $wgGroupPermissions['*']['createaccount'] = false;
2556  $wgGroupPermissions['*']['autocreateaccount'] = true;
2557  $session->clear();
2559  $this->hook( 'LocalUserCreated', $this->never() );
2560  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2561  $this->unhook( 'LocalUserCreated' );
2562  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2563 
2564  $wgGroupPermissions['*']['createaccount'] = true;
2565  $wgGroupPermissions['*']['autocreateaccount'] = false;
2566  $session->clear();
2568  $this->hook( 'LocalUserCreated', $this->never() );
2569  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2570  $this->unhook( 'LocalUserCreated' );
2571  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2572  $logger->clearBuffer();
2573 
2574  // Test lock fail
2575  $session->clear();
2577  $this->hook( 'LocalUserCreated', $this->never() );
2579  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2580  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2581  unset( $lock );
2582  $this->unhook( 'LocalUserCreated' );
2583  $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2584  $this->assertEquals( 0, $user->getId() );
2585  $this->assertNotEquals( $username, $user->getName() );
2586  $this->assertEquals( 0, $session->getUser()->getId() );
2587  $this->assertSame( [
2588  [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2589  ], $logger->getBuffer() );
2590  $logger->clearBuffer();
2591 
2592  // Test pre-authentication provider fail
2593  $session->clear();
2595  $this->hook( 'LocalUserCreated', $this->never() );
2596  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2597  $this->unhook( 'LocalUserCreated' );
2598  $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2599  $this->assertEquals( 0, $user->getId() );
2600  $this->assertNotEquals( $username, $user->getName() );
2601  $this->assertEquals( 0, $session->getUser()->getId() );
2602  $this->assertSame( [
2603  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2604  ], $logger->getBuffer() );
2605  $logger->clearBuffer();
2606  $this->assertEquals(
2607  StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2608  );
2609 
2610  $session->clear();
2612  $this->hook( 'LocalUserCreated', $this->never() );
2613  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2614  $this->unhook( 'LocalUserCreated' );
2615  $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2616  $this->assertEquals( 0, $user->getId() );
2617  $this->assertNotEquals( $username, $user->getName() );
2618  $this->assertEquals( 0, $session->getUser()->getId() );
2619  $this->assertSame( [
2620  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2621  ], $logger->getBuffer() );
2622  $logger->clearBuffer();
2623  $this->assertEquals(
2624  StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2625  );
2626 
2627  $session->clear();
2629  $this->hook( 'LocalUserCreated', $this->never() );
2630  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2631  $this->unhook( 'LocalUserCreated' );
2632  $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2633  $this->assertEquals( 0, $user->getId() );
2634  $this->assertNotEquals( $username, $user->getName() );
2635  $this->assertEquals( 0, $session->getUser()->getId() );
2636  $this->assertSame( [
2637  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2638  ], $logger->getBuffer() );
2639  $logger->clearBuffer();
2640  $this->assertEquals(
2641  StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2642  );
2643 
2644  // Test backoff
2646  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2647  $cache->set( $backoffKey, true );
2648  $session->clear();
2650  $this->hook( 'LocalUserCreated', $this->never() );
2651  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2652  $this->unhook( 'LocalUserCreated' );
2653  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2654  $this->assertEquals( 0, $user->getId() );
2655  $this->assertNotEquals( $username, $user->getName() );
2656  $this->assertEquals( 0, $session->getUser()->getId() );
2657  $this->assertSame( [
2658  [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2659  ], $logger->getBuffer() );
2660  $logger->clearBuffer();
2661  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2662  $cache->delete( $backoffKey );
2663 
2664  // Test addToDatabase fails
2665  $session->clear();
2666  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2667  $user->expects( $this->once() )->method( 'addToDatabase' )
2668  ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2669  $user->setName( $username );
2670  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2671  $this->assertEquals( \Status::newFatal( 'because' ), $ret );
2672  $this->assertEquals( 0, $user->getId() );
2673  $this->assertNotEquals( $username, $user->getName() );
2674  $this->assertEquals( 0, $session->getUser()->getId() );
2675  $this->assertSame( [
2676  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2677  [ LogLevel::ERROR, '{username} failed with message {message}' ],
2678  ], $logger->getBuffer() );
2679  $logger->clearBuffer();
2680  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2681 
2682  // Test addToDatabase throws an exception
2684  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2685  $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2686  $session->clear();
2687  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2688  $user->expects( $this->once() )->method( 'addToDatabase' )
2689  ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2690  $user->setName( $username );
2691  try {
2692  $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2693  $this->fail( 'Expected exception not thrown' );
2694  } catch ( \Exception $ex ) {
2695  $this->assertSame( 'Excepted', $ex->getMessage() );
2696  }
2697  $this->assertEquals( 0, $user->getId() );
2698  $this->assertEquals( 0, $session->getUser()->getId() );
2699  $this->assertSame( [
2700  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2701  [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2702  ], $logger->getBuffer() );
2703  $logger->clearBuffer();
2704  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2705  $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2706  $cache->delete( $backoffKey );
2707 
2708  // Test addToDatabase fails because the user already exists.
2709  $session->clear();
2710  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2711  $user->expects( $this->once() )->method( 'addToDatabase' )
2712  ->will( $this->returnCallback( function () use ( $username ) {
2713  $status = \User::newFromName( $username )->addToDatabase();
2714  $this->assertTrue( $status->isOK(), 'sanity check' );
2715  return \Status::newFatal( 'userexists' );
2716  } ) );
2717  $user->setName( $username );
2718  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2719  $expect = \Status::newGood();
2720  $expect->warning( 'userexists' );
2721  $this->assertEquals( $expect, $ret );
2722  $this->assertNotEquals( 0, $user->getId() );
2723  $this->assertEquals( $username, $user->getName() );
2724  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2725  $this->assertSame( [
2726  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2727  [ LogLevel::INFO, '{username} already exists locally (race)' ],
2728  ], $logger->getBuffer() );
2729  $logger->clearBuffer();
2730  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2731 
2732  // Success!
2733  $session->clear();
2734  $username = self::usernameForCreation();
2736  $this->hook( 'AuthPluginAutoCreate', $this->once() )
2737  ->with( $callback );
2738  $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2739  get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2740  $this->hook( 'LocalUserCreated', $this->once() )
2741  ->with( $callback, $this->equalTo( true ) );
2742  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2743  $this->unhook( 'LocalUserCreated' );
2744  $this->unhook( 'AuthPluginAutoCreate' );
2745  $this->assertEquals( \Status::newGood(), $ret );
2746  $this->assertNotEquals( 0, $user->getId() );
2747  $this->assertEquals( $username, $user->getName() );
2748  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2749  $this->assertSame( [
2750  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2751  ], $logger->getBuffer() );
2752  $logger->clearBuffer();
2753 
2754  $dbw = wfGetDB( DB_MASTER );
2755  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2756  $session->clear();
2757  $username = self::usernameForCreation();
2759  $this->hook( 'LocalUserCreated', $this->once() )
2760  ->with( $callback, $this->equalTo( true ) );
2761  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2762  $this->unhook( 'LocalUserCreated' );
2763  $this->assertEquals( \Status::newGood(), $ret );
2764  $this->assertNotEquals( 0, $user->getId() );
2765  $this->assertEquals( $username, $user->getName() );
2766  $this->assertEquals( 0, $session->getUser()->getId() );
2767  $this->assertSame( [
2768  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2769  ], $logger->getBuffer() );
2770  $logger->clearBuffer();
2771  $this->assertSame(
2772  $maxLogId,
2773  $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2774  );
2775 
2776  $this->config->set( 'NewUserLog', true );
2777  $session->clear();
2778  $username = self::usernameForCreation();
2780  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2781  $this->assertEquals( \Status::newGood(), $ret );
2782  $logger->clearBuffer();
2783 
2785  $rows = iterator_to_array( $dbw->select(
2786  $data['tables'],
2787  $data['fields'],
2788  [
2789  'log_id > ' . (int)$maxLogId,
2790  'log_type' => 'newusers'
2791  ] + $data['conds'],
2792  __METHOD__,
2793  $data['options'],
2794  $data['join_conds']
2795  ) );
2796  $this->assertCount( 1, $rows );
2797  $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2798 
2799  $this->assertSame( 'autocreate', $entry->getSubtype() );
2800  $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2801  $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2802  $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2803  $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2804 
2805  $workaroundPHPUnitBug = true;
2806  }
2807 
2814  public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2815  $makeReq = function ( $key ) use ( $action ) {
2816  $req = $this->getMock( AuthenticationRequest::class );
2817  $req->expects( $this->any() )->method( 'getUniqueId' )
2818  ->will( $this->returnValue( $key ) );
2820  $req->key = $key;
2821  return $req;
2822  };
2823  $cmpReqs = function ( $a, $b ) {
2824  $ret = strcmp( get_class( $a ), get_class( $b ) );
2825  if ( !$ret ) {
2826  $ret = strcmp( $a->key, $b->key );
2827  }
2828  return $ret;
2829  };
2830 
2831  $good = StatusValue::newGood();
2832 
2833  $mocks = [];
2834  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2835  $class = ucfirst( $key ) . 'AuthenticationProvider';
2836  $mocks[$key] = $this->getMockForAbstractClass(
2837  "MediaWiki\\Auth\\$class", [], "Mock$class"
2838  );
2839  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2840  ->will( $this->returnValue( $key ) );
2841  $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2842  ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2843  return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2844  } ) );
2845  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2846  ->will( $this->returnValue( $good ) );
2847  }
2848 
2849  $primaries = [];
2850  foreach ( [
2854  ] as $type ) {
2855  $class = 'PrimaryAuthenticationProvider';
2856  $mocks["primary-$type"] = $this->getMockForAbstractClass(
2857  "MediaWiki\\Auth\\$class", [], "Mock$class"
2858  );
2859  $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2860  ->will( $this->returnValue( "primary-$type" ) );
2861  $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2862  ->will( $this->returnValue( $type ) );
2863  $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2864  ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2865  return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2866  } ) );
2867  $mocks["primary-$type"]->expects( $this->any() )
2868  ->method( 'providerAllowsAuthenticationDataChange' )
2869  ->will( $this->returnValue( $good ) );
2870  $this->primaryauthMocks[] = $mocks["primary-$type"];
2871  }
2872 
2873  $mocks['primary2'] = $this->getMockForAbstractClass(
2874  PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
2875  );
2876  $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2877  ->will( $this->returnValue( 'primary2' ) );
2878  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2879  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2880  $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2881  ->will( $this->returnValue( [] ) );
2882  $mocks['primary2']->expects( $this->any() )
2883  ->method( 'providerAllowsAuthenticationDataChange' )
2884  ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2885  return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2886  } ) );
2887  $this->primaryauthMocks[] = $mocks['primary2'];
2888 
2889  $this->preauthMocks = [ $mocks['pre'] ];
2890  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2891  $this->initializeManager( true );
2892 
2893  if ( $state ) {
2894  if ( isset( $state['continueRequests'] ) ) {
2895  $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2896  }
2898  $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2899  } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2900  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2901  } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2902  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2903  }
2904  }
2905 
2906  $expectReqs = array_map( $makeReq, $expect );
2907  if ( $action === AuthManager::ACTION_LOGIN ) {
2909  $req->action = $action;
2911  $expectReqs[] = $req;
2912  } elseif ( $action === AuthManager::ACTION_CREATE ) {
2914  $req->action = $action;
2915  $expectReqs[] = $req;
2917  $req->action = $action;
2919  $expectReqs[] = $req;
2920  }
2921  usort( $expectReqs, $cmpReqs );
2922 
2923  $actual = $this->manager->getAuthenticationRequests( $action );
2924  foreach ( $actual as $req ) {
2925  // Don't test this here.
2926  $req->required = AuthenticationRequest::REQUIRED;
2927  }
2928  usort( $actual, $cmpReqs );
2929 
2930  $this->assertEquals( $expectReqs, $actual );
2931 
2932  // Test CreationReasonAuthenticationRequest gets returned
2933  if ( $action === AuthManager::ACTION_CREATE ) {
2935  $req->action = $action;
2936  $req->required = AuthenticationRequest::REQUIRED;
2937  $expectReqs[] = $req;
2938  usort( $expectReqs, $cmpReqs );
2939 
2940  $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2941  foreach ( $actual as $req ) {
2942  // Don't test this here.
2943  $req->required = AuthenticationRequest::REQUIRED;
2944  }
2945  usort( $actual, $cmpReqs );
2946 
2947  $this->assertEquals( $expectReqs, $actual );
2948  }
2949  }
2950 
2951  public static function provideGetAuthenticationRequests() {
2952  return [
2953  [
2955  [ 'pre-login', 'primary-none-login', 'primary-create-login',
2956  'primary-link-login', 'secondary-login', 'generic' ],
2957  ],
2958  [
2960  [ 'pre-create', 'primary-none-create', 'primary-create-create',
2961  'primary-link-create', 'secondary-create', 'generic' ],
2962  ],
2963  [
2965  [ 'primary-link-link', 'generic' ],
2966  ],
2967  [
2969  [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2970  'secondary-change' ],
2971  ],
2972  [
2974  [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2975  'secondary-remove' ],
2976  ],
2977  [
2979  [ 'primary-link-remove' ],
2980  ],
2981  [
2983  [],
2984  ],
2985  [
2987  $reqs = [ 'continue-login', 'foo', 'bar' ],
2988  [
2989  'continueRequests' => $reqs,
2990  ],
2991  ],
2992  [
2994  [],
2995  ],
2996  [
2998  $reqs = [ 'continue-create', 'foo', 'bar' ],
2999  [
3000  'continueRequests' => $reqs,
3001  ],
3002  ],
3003  [
3005  [],
3006  ],
3007  [
3009  $reqs = [ 'continue-link', 'foo', 'bar' ],
3010  [
3011  'continueRequests' => $reqs,
3012  ],
3013  ],
3014  ];
3015  }
3016 
3018  $makeReq = function ( $key, $required ) {
3019  $req = $this->getMock( AuthenticationRequest::class );
3020  $req->expects( $this->any() )->method( 'getUniqueId' )
3021  ->will( $this->returnValue( $key ) );
3022  $req->action = AuthManager::ACTION_LOGIN;
3023  $req->key = $key;
3024  $req->required = $required;
3025  return $req;
3026  };
3027  $cmpReqs = function ( $a, $b ) {
3028  $ret = strcmp( get_class( $a ), get_class( $b ) );
3029  if ( !$ret ) {
3030  $ret = strcmp( $a->key, $b->key );
3031  }
3032  return $ret;
3033  };
3034 
3035  $good = StatusValue::newGood();
3036 
3037  $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3038  $primary1->expects( $this->any() )->method( 'getUniqueId' )
3039  ->will( $this->returnValue( 'primary1' ) );
3040  $primary1->expects( $this->any() )->method( 'accountCreationType' )
3041  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3042  $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3043  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3044  return [
3045  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3046  $makeReq( "required", AuthenticationRequest::REQUIRED ),
3047  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3048  $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3049  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3050  $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3051  ];
3052  } ) );
3053 
3054  $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3055  $primary2->expects( $this->any() )->method( 'getUniqueId' )
3056  ->will( $this->returnValue( 'primary2' ) );
3057  $primary2->expects( $this->any() )->method( 'accountCreationType' )
3058  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3059  $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3060  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3061  return [
3062  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3063  $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3064  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3065  ];
3066  } ) );
3067 
3068  $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3069  $secondary->expects( $this->any() )->method( 'getUniqueId' )
3070  ->will( $this->returnValue( 'secondary' ) );
3071  $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3072  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3073  return [
3074  $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3075  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3076  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3077  ];
3078  } ) );
3079 
3080  $rememberReq = new RememberMeAuthenticationRequest;
3081  $rememberReq->action = AuthManager::ACTION_LOGIN;
3082 
3083  $this->primaryauthMocks = [ $primary1, $primary2 ];
3084  $this->secondaryauthMocks = [ $secondary ];
3085  $this->initializeManager( true );
3086 
3087  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3088  $expected = [
3089  $rememberReq,
3090  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3091  $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3092  $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3093  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3094  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3095  $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3096  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3097  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3098  ];
3099  usort( $actual, $cmpReqs );
3100  usort( $expected, $cmpReqs );
3101  $this->assertEquals( $expected, $actual );
3102 
3103  $this->primaryauthMocks = [ $primary1 ];
3104  $this->secondaryauthMocks = [ $secondary ];
3105  $this->initializeManager( true );
3106 
3107  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3108  $expected = [
3109  $rememberReq,
3110  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3111  $makeReq( "required", AuthenticationRequest::REQUIRED ),
3112  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3113  $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3114  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3115  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3116  ];
3117  usort( $actual, $cmpReqs );
3118  usort( $expected, $cmpReqs );
3119  $this->assertEquals( $expected, $actual );
3120  }
3121 
3122  public function testAllowsPropertyChange() {
3123  $mocks = [];
3124  foreach ( [ 'primary', 'secondary' ] as $key ) {
3125  $class = ucfirst( $key ) . 'AuthenticationProvider';
3126  $mocks[$key] = $this->getMockForAbstractClass(
3127  "MediaWiki\\Auth\\$class", [], "Mock$class"
3128  );
3129  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3130  ->will( $this->returnValue( $key ) );
3131  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3132  ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3133  return $prop !== $key;
3134  } ) );
3135  }
3136 
3137  $this->primaryauthMocks = [ $mocks['primary'] ];
3138  $this->secondaryauthMocks = [ $mocks['secondary'] ];
3139  $this->initializeManager( true );
3140 
3141  $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3142  $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3143  $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3144  }
3145 
3146  public function testAutoCreateOnLogin() {
3147  $username = self::usernameForCreation();
3148 
3149  $req = $this->getMock( AuthenticationRequest::class );
3150 
3151  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3152  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3153  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3154  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3155  $mock->expects( $this->any() )->method( 'accountCreationType' )
3156  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3157  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3158  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3159  ->will( $this->returnValue( StatusValue::newGood() ) );
3160 
3161  $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3162  $mock2->expects( $this->any() )->method( 'getUniqueId' )
3163  ->will( $this->returnValue( 'secondary' ) );
3164  $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3165  $this->returnValue(
3166  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3167  )
3168  );
3169  $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3170  ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3171  $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3172  ->will( $this->returnValue( StatusValue::newGood() ) );
3173 
3174  $this->primaryauthMocks = [ $mock ];
3175  $this->secondaryauthMocks = [ $mock2 ];
3176  $this->initializeManager( true );
3177  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3178  $session = $this->request->getSession();
3179  $session->clear();
3180 
3181  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3182  'sanity check' );
3183 
3184  $callback = $this->callback( function ( $user ) use ( $username ) {
3185  return $user->getName() === $username;
3186  } );
3187 
3188  $this->hook( 'UserLoggedIn', $this->never() );
3189  $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3190  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3191  $this->unhook( 'LocalUserCreated' );
3192  $this->unhook( 'UserLoggedIn' );
3193  $this->assertSame( AuthenticationResponse::UI, $ret->status );
3194 
3195  $id = (int)\User::newFromName( $username )->getId();
3196  $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3197  $this->assertSame( 0, $session->getUser()->getId() );
3198 
3199  $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3200  $this->hook( 'LocalUserCreated', $this->never() );
3201  $ret = $this->manager->continueAuthentication( [] );
3202  $this->unhook( 'LocalUserCreated' );
3203  $this->unhook( 'UserLoggedIn' );
3204  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3205  $this->assertSame( $username, $ret->username );
3206  $this->assertSame( $id, $session->getUser()->getId() );
3207  }
3208 
3209  public function testAutoCreateFailOnLogin() {
3210  $username = self::usernameForCreation();
3211 
3212  $mock = $this->getMockForAbstractClass(
3213  PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
3214  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3215  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3216  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3217  $mock->expects( $this->any() )->method( 'accountCreationType' )
3218  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3219  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3220  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3221  ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3222 
3223  $this->primaryauthMocks = [ $mock ];
3224  $this->initializeManager( true );
3225  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3226  $session = $this->request->getSession();
3227  $session->clear();
3228 
3229  $this->assertSame( 0, $session->getUser()->getId(),
3230  'sanity check' );
3231  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3232  'sanity check' );
3233 
3234  $this->hook( 'UserLoggedIn', $this->never() );
3235  $this->hook( 'LocalUserCreated', $this->never() );
3236  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3237  $this->unhook( 'LocalUserCreated' );
3238  $this->unhook( 'UserLoggedIn' );
3239  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3240  $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3241 
3242  $this->assertSame( 0, \User::newFromName( $username )->getId() );
3243  $this->assertSame( 0, $session->getUser()->getId() );
3244  }
3245 
3246  public function testAuthenticationSessionData() {
3247  $this->initializeManager( true );
3248 
3249  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3250  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3251  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3252  $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3253  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3254  $this->manager->removeAuthenticationSessionData( 'foo' );
3255  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3256  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3257  $this->manager->removeAuthenticationSessionData( 'bar' );
3258  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3259 
3260  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3261  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3262  $this->manager->removeAuthenticationSessionData( null );
3263  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3264  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3265 
3266  }
3267 
3268  public function testCanLinkAccounts() {
3269  $types = [
3273  ];
3274 
3275  foreach ( $types as $type => $can ) {
3276  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3277  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3278  $mock->expects( $this->any() )->method( 'accountCreationType' )
3279  ->will( $this->returnValue( $type ) );
3280  $this->primaryauthMocks = [ $mock ];
3281  $this->initializeManager( true );
3282  $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3283  }
3284  }
3285 
3286  public function testBeginAccountLink() {
3287  $user = \User::newFromName( 'UTSysop' );
3288  $this->initializeManager();
3289 
3290  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3291  try {
3292  $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3293  $this->fail( 'Expected exception not thrown' );
3294  } catch ( \LogicException $ex ) {
3295  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3296  }
3297  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3298 
3299  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3300  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3301  $mock->expects( $this->any() )->method( 'accountCreationType' )
3302  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3303  $this->primaryauthMocks = [ $mock ];
3304  $this->initializeManager( true );
3305 
3306  $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3307  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3308  $this->assertSame( 'noname', $ret->message->getKey() );
3309 
3310  $ret = $this->manager->beginAccountLink(
3311  \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3312  );
3313  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3314  $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3315  }
3316 
3317  public function testContinueAccountLink() {
3318  $user = \User::newFromName( 'UTSysop' );
3319  $this->initializeManager();
3320 
3321  $session = [
3322  'userid' => $user->getId(),
3323  'username' => $user->getName(),
3324  'primary' => 'X',
3325  ];
3326 
3327  try {
3328  $this->manager->continueAccountLink( [] );
3329  $this->fail( 'Expected exception not thrown' );
3330  } catch ( \LogicException $ex ) {
3331  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3332  }
3333 
3334  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3335  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3336  $mock->expects( $this->any() )->method( 'accountCreationType' )
3337  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3338  $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3339  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3340  );
3341  $this->primaryauthMocks = [ $mock ];
3342  $this->initializeManager( true );
3343 
3344  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3345  $ret = $this->manager->continueAccountLink( [] );
3346  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3347  $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3348 
3349  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3350  [ 'username' => $user->getName() . '<>' ] + $session );
3351  $ret = $this->manager->continueAccountLink( [] );
3352  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3353  $this->assertSame( 'noname', $ret->message->getKey() );
3354  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3355 
3356  $id = $user->getId();
3357  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3358  [ 'userid' => $id + 1 ] + $session );
3359  try {
3360  $ret = $this->manager->continueAccountLink( [] );
3361  $this->fail( 'Expected exception not thrown' );
3362  } catch ( \UnexpectedValueException $ex ) {
3363  $this->assertEquals(
3364  "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
3365  $ex->getMessage()
3366  );
3367  }
3368  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3369  }
3370 
3377  public function testAccountLink(
3378  StatusValue $preTest, array $primaryResponses, array $managerResponses
3379  ) {
3380  $user = \User::newFromName( 'UTSysop' );
3381 
3382  $this->initializeManager();
3383 
3384  // Set up lots of mocks...
3385  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3386  $req->primary = $primaryResponses;
3387  $mocks = [];
3388 
3389  foreach ( [ 'pre', 'primary' ] as $key ) {
3390  $class = ucfirst( $key ) . 'AuthenticationProvider';
3391  $mocks[$key] = $this->getMockForAbstractClass(
3392  "MediaWiki\\Auth\\$class", [], "Mock$class"
3393  );
3394  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3395  ->will( $this->returnValue( $key ) );
3396 
3397  for ( $i = 2; $i <= 3; $i++ ) {
3398  $mocks[$key . $i] = $this->getMockForAbstractClass(
3399  "MediaWiki\\Auth\\$class", [], "Mock$class"
3400  );
3401  $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3402  ->will( $this->returnValue( $key . $i ) );
3403  }
3404  }
3405 
3406  $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3407  ->will( $this->returnCallback(
3408  function ( $u )
3409  use ( $user, $preTest )
3410  {
3411  $this->assertSame( $user->getId(), $u->getId() );
3412  $this->assertSame( $user->getName(), $u->getName() );
3413  return $preTest;
3414  }
3415  ) );
3416 
3417  $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3418  ->will( $this->returnValue( StatusValue::newGood() ) );
3419 
3420  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3421  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3422  $ct = count( $req->primary );
3423  $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3424  $this->assertSame( $user->getId(), $u->getId() );
3425  $this->assertSame( $user->getName(), $u->getName() );
3426  $foundReq = false;
3427  foreach ( $reqs as $r ) {
3428  $this->assertSame( $user->getName(), $r->username );
3429  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3430  }
3431  $this->assertTrue( $foundReq, '$reqs contains $req' );
3432  return array_shift( $req->primary );
3433  } );
3434  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3435  ->method( 'beginPrimaryAccountLink' )
3436  ->will( $callback );
3437  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3438  ->method( 'continuePrimaryAccountLink' )
3439  ->will( $callback );
3440 
3442  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3443  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3444  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3445  ->will( $this->returnValue( $abstain ) );
3446  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3447  $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3448  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3449  $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3450  $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3451 
3452  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3453  $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3454  $this->logger = new \TestLogger( true, function ( $message, $level ) {
3455  return $level === LogLevel::DEBUG ? null : $message;
3456  } );
3457  $this->initializeManager( true );
3458 
3459  $constraint = \PHPUnit_Framework_Assert::logicalOr(
3460  $this->equalTo( AuthenticationResponse::PASS ),
3461  $this->equalTo( AuthenticationResponse::FAIL )
3462  );
3463  $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3464  foreach ( $providers as $p ) {
3465  $p->postCalled = false;
3466  $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3467  ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3468  $this->assertInstanceOf( 'User', $user );
3469  $this->assertSame( 'UTSysop', $user->getName() );
3470  $this->assertInstanceOf( AuthenticationResponse::class, $response );
3471  $this->assertThat( $response->status, $constraint );
3472  $p->postCalled = $response->status;
3473  } );
3474  }
3475 
3476  $first = true;
3477  $created = false;
3478  $expectLog = [];
3479  foreach ( $managerResponses as $i => $response ) {
3480  if ( $response instanceof AuthenticationResponse &&
3482  ) {
3483  $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3484  }
3485 
3486  $ex = null;
3487  try {
3488  if ( $first ) {
3489  $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3490  } else {
3491  $ret = $this->manager->continueAccountLink( [ $req ] );
3492  }
3493  if ( $response instanceof \Exception ) {
3494  $this->fail( 'Expected exception not thrown', "Response $i" );
3495  }
3496  } catch ( \Exception $ex ) {
3497  if ( !$response instanceof \Exception ) {
3498  throw $ex;
3499  }
3500  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3501  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3502  "Response $i, exception, session state" );
3503  return;
3504  }
3505 
3506  $this->assertSame( 'http://localhost/', $req->returnToUrl );
3507 
3508  $ret->message = $this->message( $ret->message );
3509  $this->assertEquals( $response, $ret, "Response $i, response" );
3510  if ( $response->status === AuthenticationResponse::PASS ||
3512  ) {
3513  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3514  "Response $i, session state" );
3515  foreach ( $providers as $p ) {
3516  $this->assertSame( $response->status, $p->postCalled,
3517  "Response $i, post-auth callback called" );
3518  }
3519  } else {
3520  $this->assertNotNull(
3521  $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3522  "Response $i, session state"
3523  );
3524  foreach ( $ret->neededRequests as $neededReq ) {
3525  $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
3526  "Response $i, neededRequest action" );
3527  }
3528  $this->assertEquals(
3529  $ret->neededRequests,
3530  $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3531  "Response $i, continuation check"
3532  );
3533  foreach ( $providers as $p ) {
3534  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3535  }
3536  }
3537 
3538  $first = false;
3539  }
3540 
3541  $this->assertSame( $expectLog, $this->logger->getBuffer() );
3542  }
3543 
3544  public function provideAccountLink() {
3545  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3546  $good = StatusValue::newGood();
3547 
3548  return [
3549  'Pre-link test fail in pre' => [
3550  StatusValue::newFatal( 'fail-from-pre' ),
3551  [],
3552  [
3553  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3554  ]
3555  ],
3556  'Failure in primary' => [
3557  $good,
3558  $tmp = [
3559  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3560  ],
3561  $tmp
3562  ],
3563  'All primary abstain' => [
3564  $good,
3565  [
3567  ],
3568  [
3569  AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3570  ]
3571  ],
3572  'Primary UI, then redirect, then fail' => [
3573  $good,
3574  $tmp = [
3575  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3576  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3577  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3578  ],
3579  $tmp
3580  ],
3581  'Primary redirect, then abstain' => [
3582  $good,
3583  [
3585  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3586  ),
3588  ],
3589  [
3590  $tmp,
3591  new \DomainException(
3592  'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3593  )
3594  ]
3595  ],
3596  'Primary UI, then pass' => [
3597  $good,
3598  [
3599  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3601  ],
3602  [
3603  $tmp1,
3605  ]
3606  ],
3607  'Primary pass' => [
3608  $good,
3609  [
3611  ],
3612  [
3614  ]
3615  ],
3616  ];
3617  }
3618 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:568
initializeManager($regen=false)
Initialize $this->manager.
testAccountCreation(StatusValue $preTest, $primaryTest, $secondaryTest, array $primaryResponses, array $secondaryResponses, array $managerResponses)
provideAccountCreation
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
onSecuritySensitiveOperationStatus(&$status, $operation, $session, $time)
This transfers state between the login and account creation flows.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provdier, but other primary authen...
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
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
the array() calling protocol came about after MediaWiki 1.4rc1.
initializeConfig()
Initialize the AuthManagerConfig variable in $this->config.
static wrap($sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:79
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
$success
const RESTART
Indicates that third-party authentication succeeded but no user exists.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
testAccountLink(StatusValue $preTest, array $primaryResponses, array $managerResponses)
provideAccountLink
static newFatal($message)
Factory function for fatal errors.
Definition: StatusValue.php:63
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:1798
hook($hook, $expect)
Sets a mock on a hook.
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
Definition: AuthManager.php:64
static newFromUser(User $user, $verified=false)
Create an instance from an existing User object.
Definition: UserInfo.php:116
static setSessionManagerSingleton(SessionManager $manager=null)
Override the singleton for unit testing.
Definition: TestUtils.php:17
testUserExists($primary1Exists, $primary2Exists, $expect)
provideUserExists
static getLocalClusterInstance()
Get the main cluster-local cache object.
This is a value object to hold authentication response data.
$wgHooks['ArticleShow'][]
Definition: hooks.txt:110
Authentication request for the reason given for account creation.
Psr Log LoggerInterface $logger
this hook is for auditing only $response
Definition: hooks.txt:762
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
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
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1612
get($name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
static newRedirect(array $reqs, $redirectTarget, $redirectApiData=null)
the value to return A Title object or null for latest to be modified or replaced by the hook handler or if authentication is not possible after cache objects are set for highlighting & $link
Definition: hooks.txt:2581
unhook($hook)
Unsets a hook.
static BagOStuff[] $instances
Map of (id => BagOStuff)
Definition: ObjectCache.php:82
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:1798
static setPasswordForUser(User $user, $password)
Set the password on a testing user.
Definition: TestUser.php:127
static getMain()
Static methods.
IContextSource $context
Definition: MediaWiki.php:32
This represents additional user data requested on the account creation form.
const FAIL
Indicates that the authentication failed.
const TYPE_LINK
Provider can link to existing accounts elsewhere.
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:1077
static newUI(array $reqs, Message $msg)
static singleton()
Get the global AuthManager.
$res
Definition: database.txt:21
const ACTION_CHANGE
Change a user's credentials.
Definition: AuthManager.php:60
const SEC_FAIL
Security-sensitive should not be performed.
Definition: AuthManager.php:71
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
Definition: AuthManager.php:69
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
Definition: AuthManager.php:74
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 an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing 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 unsetoffset-wrap String Wrap the message in html(usually something like"&lt
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
$cache
Definition: mcc.php:33
$params
const REQUIRED
Indicates that the request is required for authentication to proceed.
Returned from account creation to allow for logging into the created account.
This is an authentication request added by AuthManager to show a "remember me" checkbox.
const PASS
Indicates that the authentication succeeded.
static newGood($value=null)
Factory function for good results.
Definition: StatusValue.php:76
hideDeprecated($function)
Don't throw a warning if $function is deprecated and called later.
This serves as the entry point to the authentication system.
Definition: AuthManager.php:43
AuthenticationRequest to ensure something with a username is present.
const TYPE_NONE
Provider cannot create or link to accounts.
getLanguage()
Get the Language object.
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
testSecuritySensitiveOperationStatus($mutableSession)
provideSecuritySensitiveOperationStatus
const ACTION_LINK
Link an existing user to a third-party account.
Definition: AuthManager.php:55
testAccountCreationLogging($isAnon, $logSubtype)
provideAccountCreationLogging
static getSelectQueryData()
Returns array of information that is needed for querying log entries.
Definition: LogEntry.php:170
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
testUserCanAuthenticate($primary1Can, $primary2Can, $expect)
provideUserCanAuthenticate
AuthManager Database MediaWiki\Auth\AuthManager.
String $action
Cache what action this request is.
Definition: MediaWiki.php:42
message($key, $params=[])
Ensure a value is a clean Message object.
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
this hook is for auditing only $req
Definition: hooks.txt:965
getMockSessionProvider($canChangeUser=null, array $methods=[])
Setup SessionManager with a mock session provider.
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:762
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
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:48
const ACTION_REMOVE
Remove a user's credentials.
Definition: AuthManager.php:62
testAuthentication(StatusValue $preResponse, array $primaryResponses, array $secondaryResponses, array $managerResponses, $link=false)
provideAuthentication
static idFromName($name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:770
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
Definition: AuthManager.php:53
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1004
static newFromRow($row)
Constructs new LogEntry from database result row.
Definition: LogEntry.php:201
static consume(ScopedCallback &$sc=null)
Trigger a scoped callback and destroy it.
testGetAuthenticationRequests($action, $expect, $state=[])
provideGetAuthenticationRequests
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:50
wfMemcKey()
Make a cache key for the local wiki.
const DB_MASTER
Definition: Defines.php:47
TestingAccessWrapper $managerPriv
</td >< td > &</td >< td > t want your writing to be edited mercilessly and redistributed at will
const SEC_OK
Security-sensitive operations are ok.
Definition: AuthManager.php:67
static newFromObject($object)
Return the same object, without access restrictions.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:45
this hook is for auditing only etc 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:1946
static factory($code)
Get a cached or new language object for a given language code.
Definition: Language.php:179
setMwGlobals($pairs, $value=null)
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
Definition: AuthManager.php:58
testAllowsAuthenticationDataChange($primaryReturn, $secondaryReturn, $expect)
provideAllowsAuthenticationDataChange
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2338
stashMwGlobals($globalKeys)
Stashes the global, will be restored in tearDown()
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
const UI
Indicates that the authentication needs further user input of some sort.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310