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