MediaWiki  master
AuthManagerTest.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Auth;
4 
7 use Psr\Log\LogLevel;
9 
17  protected $request;
19  protected $config;
21  protected $logger;
22 
23  protected $preauthMocks = [];
24  protected $primaryauthMocks = [];
25  protected $secondaryauthMocks = [];
26 
28  protected $manager;
30  protected $managerPriv;
31 
32  protected function setUp() {
34 
35  parent::setUp();
36  if ( $wgDisableAuthManager ) {
37  $this->markTestSkipped( '$wgDisableAuthManager is set' );
38  }
39 
40  $this->setMwGlobals( [ 'wgAuth' => null ] );
41  $this->stashMwGlobals( [ 'wgHooks' ] );
42  }
43 
50  protected function hook( $hook, $expect ) {
52  $mock = $this->getMock( __CLASS__, [ "on$hook" ] );
53  $wgHooks[$hook] = [ $mock ];
54  return $mock->expects( $expect )->method( "on$hook" );
55  }
56 
61  protected function unhook( $hook ) {
63  $wgHooks[$hook] = [];
64  }
65 
72  protected function message( $key, $params = [] ) {
73  if ( $key === null ) {
74  return null;
75  }
76  if ( $key instanceof \MessageSpecifier ) {
77  $params = $key->getParams();
78  $key = $key->getKey();
79  }
80  return new \Message( $key, $params, \Language::factory( 'en' ) );
81  }
82 
88  protected function initializeConfig() {
89  $config = [
90  'preauth' => [
91  ],
92  'primaryauth' => [
93  ],
94  'secondaryauth' => [
95  ],
96  ];
97 
98  foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
99  $key = $type . 'Mocks';
100  foreach ( $this->$key as $mock ) {
101  $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
102  return $mock;
103  } ];
104  }
105  }
106 
107  $this->config->set( 'AuthManagerConfig', $config );
108  $this->config->set( 'LanguageCode', 'en' );
109  $this->config->set( 'NewUserLog', false );
110  }
111 
116  protected function initializeManager( $regen = false ) {
117  if ( $regen || !$this->config ) {
118  $this->config = new \HashConfig();
119  }
120  if ( $regen || !$this->request ) {
121  $this->request = new \FauxRequest();
122  }
123  if ( !$this->logger ) {
124  $this->logger = new \TestLogger();
125  }
126 
127  if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
128  $this->initializeConfig();
129  }
130  $this->manager = new AuthManager( $this->request, $this->config );
131  $this->manager->setLogger( $this->logger );
132  $this->managerPriv = \TestingAccessWrapper::newFromObject( $this->manager );
133  }
134 
141  protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
142  if ( !$this->config ) {
143  $this->config = new \HashConfig();
144  $this->initializeConfig();
145  }
146  $this->config->set( 'ObjectCacheSessionExpiry', 100 );
147 
148  $methods[] = '__toString';
149  $methods[] = 'describe';
150  if ( $canChangeUser !== null ) {
151  $methods[] = 'canChangeUser';
152  }
153  $provider = $this->getMockBuilder( 'DummySessionProvider' )
154  ->setMethods( $methods )
155  ->getMock();
156  $provider->expects( $this->any() )->method( '__toString' )
157  ->will( $this->returnValue( 'MockSessionProvider' ) );
158  $provider->expects( $this->any() )->method( 'describe' )
159  ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
160  if ( $canChangeUser !== null ) {
161  $provider->expects( $this->any() )->method( 'canChangeUser' )
162  ->will( $this->returnValue( $canChangeUser ) );
163  }
164  $this->config->set( 'SessionProviders', [
165  [ 'factory' => function () use ( $provider ) {
166  return $provider;
167  } ],
168  ] );
169 
170  $manager = new \MediaWiki\Session\SessionManager( [
171  'config' => $this->config,
172  'logger' => new \Psr\Log\NullLogger(),
173  'store' => new \HashBagOStuff(),
174  ] );
175  \TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
176 
178 
179  if ( $this->request ) {
180  $manager->getSessionForRequest( $this->request );
181  }
182 
183  return [ $provider, $reset ];
184  }
185 
186  public function testSingleton() {
187  // Temporarily clear out the global singleton, if any, to test creating
188  // one.
189  $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
190  $rProp->setAccessible( true );
191  $old = $rProp->getValue();
192  $cb = new \ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
193  $rProp->setValue( null );
194 
195  $singleton = AuthManager::singleton();
196  $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
197  $this->assertSame( $singleton, AuthManager::singleton() );
198  $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
199  $this->assertSame(
200  \RequestContext::getMain()->getConfig(),
201  \TestingAccessWrapper::newFromObject( $singleton )->config
202  );
203 
204  $this->setMwGlobals( [ 'wgDisableAuthManager' => true ] );
205  try {
207  $this->fail( 'Expected exception not thrown' );
208  } catch ( \BadMethodCallException $ex ) {
209  $this->assertSame( '$wgDisableAuthManager is set', $ex->getMessage() );
210  }
211  }
212 
213  public function testCanAuthenticateNow() {
214  $this->initializeManager();
215 
216  list( $provider, $reset ) = $this->getMockSessionProvider( false );
217  $this->assertFalse( $this->manager->canAuthenticateNow() );
218  \ScopedCallback::consume( $reset );
219 
220  list( $provider, $reset ) = $this->getMockSessionProvider( true );
221  $this->assertTrue( $this->manager->canAuthenticateNow() );
222  \ScopedCallback::consume( $reset );
223  }
224 
225  public function testNormalizeUsername() {
226  $mocks = [
227  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
228  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
229  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
230  $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
231  ];
232  foreach ( $mocks as $key => $mock ) {
233  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
234  }
235  $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
236  ->with( $this->identicalTo( 'XYZ' ) )
237  ->willReturn( 'Foo' );
238  $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
239  ->with( $this->identicalTo( 'XYZ' ) )
240  ->willReturn( 'Foo' );
241  $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
242  ->with( $this->identicalTo( 'XYZ' ) )
243  ->willReturn( null );
244  $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
245  ->with( $this->identicalTo( 'XYZ' ) )
246  ->willReturn( 'Bar!' );
247 
248  $this->primaryauthMocks = $mocks;
249 
250  $this->initializeManager();
251 
252  $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
253  }
254 
259  public function testSecuritySensitiveOperationStatus( $mutableSession ) {
260  $this->logger = new \Psr\Log\NullLogger();
261  $user = \User::newFromName( 'UTSysop' );
262  $provideUser = null;
263  $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
264 
265  list( $provider, $reset ) = $this->getMockSessionProvider(
266  $mutableSession, [ 'provideSessionInfo' ]
267  );
268  $provider->expects( $this->any() )->method( 'provideSessionInfo' )
269  ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
271  'provider' => $provider,
272  'id' => \DummySessionProvider::ID,
273  'persisted' => true,
274  'userInfo' => UserInfo::newFromUser( $provideUser, true )
275  ] );
276  } ) );
277  $this->initializeManager();
278 
279  $this->config->set( 'ReauthenticateTime', [] );
280  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
281  $provideUser = new \User;
282  $session = $provider->getManager()->getSessionForRequest( $this->request );
283  $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
284 
285  // Anonymous user => reauth
286  $session->set( 'AuthManager:lastAuthId', 0 );
287  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
288  $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
289 
290  $provideUser = $user;
291  $session = $provider->getManager()->getSessionForRequest( $this->request );
292  $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
293 
294  // Error for no default (only gets thrown for non-anonymous user)
295  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
296  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
297  try {
298  $this->manager->securitySensitiveOperationStatus( 'foo' );
299  $this->fail( 'Expected exception not thrown' );
300  } catch ( \UnexpectedValueException $ex ) {
301  $this->assertSame(
302  $mutableSession
303  ? '$wgReauthenticateTime lacks a default'
304  : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
305  $ex->getMessage()
306  );
307  }
308 
309  if ( $mutableSession ) {
310  $this->config->set( 'ReauthenticateTime', [
311  'test' => 100,
312  'test2' => -1,
313  'default' => 10,
314  ] );
315 
316  // Mismatched user ID
317  $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
318  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
319  $this->assertSame(
320  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
321  );
322  $this->assertSame(
323  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
324  );
325  $this->assertSame(
326  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
327  );
328 
329  // Missing time
330  $session->set( 'AuthManager:lastAuthId', $user->getId() );
331  $session->set( 'AuthManager:lastAuthTimestamp', null );
332  $this->assertSame(
333  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
334  );
335  $this->assertSame(
336  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
337  );
338  $this->assertSame(
339  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
340  );
341 
342  // Recent enough to pass
343  $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
344  $this->assertSame(
345  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
346  );
347 
348  // Not recent enough to pass
349  $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
350  $this->assertSame(
351  AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
352  );
353  // But recent enough for the 'test' operation
354  $this->assertSame(
355  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
356  );
357  } else {
358  $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
359  'test' => false,
360  'default' => true,
361  ] );
362 
363  $this->assertEquals(
364  AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
365  );
366 
367  $this->assertEquals(
368  AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
369  );
370  }
371 
372  // Test hook, all three possible values
373  foreach ( [
375  AuthManager::SEC_REAUTH => $reauth,
377  ] as $hook => $expect ) {
378  $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
379  ->with(
380  $this->anything(),
381  $this->anything(),
382  $this->callback( function ( $s ) use ( $session ) {
383  return $s->getId() === $session->getId();
384  } ),
385  $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
386  )
387  ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
388  $v = $hook;
389  return true;
390  } ) );
391  $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
392  $this->assertEquals(
393  $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
394  );
395  $this->assertEquals(
396  $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
397  );
398  $this->unhook( 'SecuritySensitiveOperationStatus' );
399  }
400 
401  \ScopedCallback::consume( $reset );
402  }
403 
404  public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
405  }
406 
407  public static function provideSecuritySensitiveOperationStatus() {
408  return [
409  [ true ],
410  [ false ],
411  ];
412  }
413 
420  public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
421  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
422  $mock1->expects( $this->any() )->method( 'getUniqueId' )
423  ->will( $this->returnValue( 'primary1' ) );
424  $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
425  ->with( $this->equalTo( 'UTSysop' ) )
426  ->will( $this->returnValue( $primary1Can ) );
427  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
428  $mock2->expects( $this->any() )->method( 'getUniqueId' )
429  ->will( $this->returnValue( 'primary2' ) );
430  $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
431  ->with( $this->equalTo( 'UTSysop' ) )
432  ->will( $this->returnValue( $primary2Can ) );
433  $this->primaryauthMocks = [ $mock1, $mock2 ];
434 
435  $this->initializeManager( true );
436  $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
437  }
438 
439  public static function provideUserCanAuthenticate() {
440  return [
441  [ false, false, false ],
442  [ true, false, true ],
443  [ false, true, true ],
444  [ true, true, true ],
445  ];
446  }
447 
448  public function testRevokeAccessForUser() {
449  $this->initializeManager();
450 
451  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
452  $mock->expects( $this->any() )->method( 'getUniqueId' )
453  ->will( $this->returnValue( 'primary' ) );
454  $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
455  ->with( $this->equalTo( 'UTSysop' ) );
456  $this->primaryauthMocks = [ $mock ];
457 
458  $this->initializeManager( true );
459  $this->logger->setCollect( true );
460 
461  $this->manager->revokeAccessForUser( 'UTSysop' );
462 
463  $this->assertSame( [
464  [ LogLevel::INFO, 'Revoking access for {user}' ],
465  ], $this->logger->getBuffer() );
466  }
467 
468  public function testProviderCreation() {
469  $mocks = [
470  'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
471  'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
472  'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
473  ];
474  foreach ( $mocks as $key => $mock ) {
475  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
476  $mock->expects( $this->once() )->method( 'setLogger' );
477  $mock->expects( $this->once() )->method( 'setManager' );
478  $mock->expects( $this->once() )->method( 'setConfig' );
479  }
480  $this->preauthMocks = [ $mocks['pre'] ];
481  $this->primaryauthMocks = [ $mocks['primary'] ];
482  $this->secondaryauthMocks = [ $mocks['secondary'] ];
483 
484  // Normal operation
485  $this->initializeManager();
486  $this->assertSame(
487  $mocks['primary'],
488  $this->managerPriv->getAuthenticationProvider( 'primary' )
489  );
490  $this->assertSame(
491  $mocks['secondary'],
492  $this->managerPriv->getAuthenticationProvider( 'secondary' )
493  );
494  $this->assertSame(
495  $mocks['pre'],
496  $this->managerPriv->getAuthenticationProvider( 'pre' )
497  );
498  $this->assertSame(
499  [ 'pre' => $mocks['pre'] ],
500  $this->managerPriv->getPreAuthenticationProviders()
501  );
502  $this->assertSame(
503  [ 'primary' => $mocks['primary'] ],
504  $this->managerPriv->getPrimaryAuthenticationProviders()
505  );
506  $this->assertSame(
507  [ 'secondary' => $mocks['secondary'] ],
508  $this->managerPriv->getSecondaryAuthenticationProviders()
509  );
510 
511  // Duplicate IDs
512  $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
513  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
514  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
515  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
516  $this->preauthMocks = [ $mock1 ];
517  $this->primaryauthMocks = [ $mock2 ];
518  $this->secondaryauthMocks = [];
519  $this->initializeManager( true );
520  try {
521  $this->managerPriv->getAuthenticationProvider( 'Y' );
522  $this->fail( 'Expected exception not thrown' );
523  } catch ( \RuntimeException $ex ) {
524  $class1 = get_class( $mock1 );
525  $class2 = get_class( $mock2 );
526  $this->assertSame(
527  "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
528  );
529  }
530 
531  // Wrong classes
532  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
533  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
534  $class = get_class( $mock );
535  $this->preauthMocks = [ $mock ];
536  $this->primaryauthMocks = [ $mock ];
537  $this->secondaryauthMocks = [ $mock ];
538  $this->initializeManager( true );
539  try {
540  $this->managerPriv->getPreAuthenticationProviders();
541  $this->fail( 'Expected exception not thrown' );
542  } catch ( \RuntimeException $ex ) {
543  $this->assertSame(
544  "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
545  $ex->getMessage()
546  );
547  }
548  try {
549  $this->managerPriv->getPrimaryAuthenticationProviders();
550  $this->fail( 'Expected exception not thrown' );
551  } catch ( \RuntimeException $ex ) {
552  $this->assertSame(
553  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
554  $ex->getMessage()
555  );
556  }
557  try {
558  $this->managerPriv->getSecondaryAuthenticationProviders();
559  $this->fail( 'Expected exception not thrown' );
560  } catch ( \RuntimeException $ex ) {
561  $this->assertSame(
562  "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
563  $ex->getMessage()
564  );
565  }
566 
567  // Sorting
568  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
569  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
570  $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
571  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
572  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
573  $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
574  $this->preauthMocks = [];
575  $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
576  $this->secondaryauthMocks = [];
577  $this->initializeConfig();
578  $config = $this->config->get( 'AuthManagerConfig' );
579 
580  $this->initializeManager( false );
581  $this->assertSame(
582  [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
583  $this->managerPriv->getPrimaryAuthenticationProviders(),
584  'sanity check'
585  );
586 
587  $config['primaryauth']['A']['sort'] = 100;
588  $config['primaryauth']['C']['sort'] = -1;
589  $this->config->set( 'AuthManagerConfig', $config );
590  $this->initializeManager( false );
591  $this->assertSame(
592  [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
593  $this->managerPriv->getPrimaryAuthenticationProviders()
594  );
595  }
596 
597  public function testSetDefaultUserOptions() {
598  $this->initializeManager();
599 
601  $reset = new \ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
602  $context->setLanguage( 'de' );
603  $this->setMwGlobals( 'wgContLang', \Language::factory( 'zh' ) );
604 
605  $user = \User::newFromName( self::usernameForCreation() );
606  $user->addToDatabase();
607  $oldToken = $user->getToken();
608  $this->managerPriv->setDefaultUserOptions( $user, false );
609  $user->saveSettings();
610  $this->assertNotEquals( $oldToken, $user->getToken() );
611  $this->assertSame( 'zh', $user->getOption( 'language' ) );
612  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
613 
614  $user = \User::newFromName( self::usernameForCreation() );
615  $user->addToDatabase();
616  $oldToken = $user->getToken();
617  $this->managerPriv->setDefaultUserOptions( $user, true );
618  $user->saveSettings();
619  $this->assertNotEquals( $oldToken, $user->getToken() );
620  $this->assertSame( 'de', $user->getOption( 'language' ) );
621  $this->assertSame( 'zh', $user->getOption( 'variant' ) );
622 
623  $this->setMwGlobals( 'wgContLang', \Language::factory( 'en' ) );
624 
625  $user = \User::newFromName( self::usernameForCreation() );
626  $user->addToDatabase();
627  $oldToken = $user->getToken();
628  $this->managerPriv->setDefaultUserOptions( $user, true );
629  $user->saveSettings();
630  $this->assertNotEquals( $oldToken, $user->getToken() );
631  $this->assertSame( 'de', $user->getOption( 'language' ) );
632  $this->assertSame( null, $user->getOption( 'variant' ) );
633  }
634 
636  $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
637  $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
638  $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
639  $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
640  $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
641  $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
642  $this->primaryauthMocks = [ $mockA ];
643 
644  $this->logger = new \TestLogger( true );
645 
646  // Test without first initializing the configured providers
647  $this->initializeManager();
648  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
649  $this->assertSame(
650  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
651  );
652  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
653  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
654  $this->assertSame( [
655  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
656  ], $this->logger->getBuffer() );
657  $this->logger->clearBuffer();
658 
659  // Test with first initializing the configured providers
660  $this->initializeManager();
661  $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
662  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
663  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
664  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
665  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
666  $this->assertSame(
667  [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
668  );
669  $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
670  $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
671  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
672  $this->assertNull(
673  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
674  );
675  $this->assertSame( [
676  [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
677  [
678  LogLevel::WARNING,
679  'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
680  ],
681  ], $this->logger->getBuffer() );
682  $this->logger->clearBuffer();
683 
684  // Test duplicate IDs
685  $this->initializeManager();
686  try {
687  $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
688  $this->fail( 'Expected exception not thrown' );
689  } catch ( \RuntimeException $ex ) {
690  $class1 = get_class( $mockB );
691  $class2 = get_class( $mockB2 );
692  $this->assertSame(
693  "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
694  );
695  }
696 
697  // Wrong classes
698  $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
699  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
700  $class = get_class( $mock );
701  try {
702  $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
703  $this->fail( 'Expected exception not thrown' );
704  } catch ( \RuntimeException $ex ) {
705  $this->assertSame(
706  "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
707  $ex->getMessage()
708  );
709  }
710 
711  }
712 
713  public function testBeginAuthentication() {
714  $this->initializeManager();
715 
716  // Immutable session
717  list( $provider, $reset ) = $this->getMockSessionProvider( false );
718  $this->hook( 'UserLoggedIn', $this->never() );
719  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
720  try {
721  $this->manager->beginAuthentication( [], 'http://localhost/' );
722  $this->fail( 'Expected exception not thrown' );
723  } catch ( \LogicException $ex ) {
724  $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
725  }
726  $this->unhook( 'UserLoggedIn' );
727  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
728  \ScopedCallback::consume( $reset );
729  $this->initializeManager( true );
730 
731  // CreatedAccountAuthenticationRequest
732  $user = \User::newFromName( 'UTSysop' );
733  $reqs = [
734  new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
735  ];
736  $this->hook( 'UserLoggedIn', $this->never() );
737  try {
738  $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
739  $this->fail( 'Expected exception not thrown' );
740  } catch ( \LogicException $ex ) {
741  $this->assertSame(
742  'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
743  'that created the account',
744  $ex->getMessage()
745  );
746  }
747  $this->unhook( 'UserLoggedIn' );
748 
749  $this->request->getSession()->clear();
750  $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
751  $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
752  $this->hook( 'UserLoggedIn', $this->once() )
753  ->with( $this->callback( function ( $u ) use ( $user ) {
754  return $user->getId() === $u->getId() && $user->getName() === $u->getName();
755  } ) );
756  $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
757  $this->logger->setCollect( true );
758  $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
759  $this->logger->setCollect( false );
760  $this->unhook( 'UserLoggedIn' );
761  $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
762  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
763  $this->assertSame( $user->getName(), $ret->username );
764  $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
765  $this->assertEquals(
766  time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
767  'timestamp ±1', 1
768  );
769  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
770  $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
771  $this->assertSame( [
772  [ LogLevel::INFO, 'Logging in {user} after account creation' ],
773  ], $this->logger->getBuffer() );
774  }
775 
776  public function testCreateFromLogin() {
777  $user = \User::newFromName( 'UTSysop' );
778  $req1 = $this->getMock( AuthenticationRequest::class );
779  $req2 = $this->getMock( AuthenticationRequest::class );
780  $req3 = $this->getMock( AuthenticationRequest::class );
781  $userReq = new UsernameAuthenticationRequest;
782  $userReq->username = 'UTDummy';
783 
784  $req1->returnToUrl = 'http://localhost/';
785  $req2->returnToUrl = 'http://localhost/';
786  $req3->returnToUrl = 'http://localhost/';
787  $req3->username = 'UTDummy';
788  $userReq->returnToUrl = 'http://localhost/';
789 
790  // Passing one into beginAuthentication(), and an immediate FAIL
791  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
792  $this->primaryauthMocks = [ $primary ];
793  $this->initializeManager( true );
795  $res->createRequest = $req1;
796  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
797  ->will( $this->returnValue( $res ) );
798  $createReq = new CreateFromLoginAuthenticationRequest(
799  null, [ $req2->getUniqueId() => $req2 ]
800  );
801  $this->logger->setCollect( true );
802  $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
803  $this->logger->setCollect( false );
804  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
805  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
806  $this->assertSame( $req1, $ret->createRequest->createRequest );
807  $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
808 
809  // UI, then FAIL in beginAuthentication()
810  $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
811  ->setMethods( [ 'continuePrimaryAuthentication' ] )
812  ->getMockForAbstractClass();
813  $this->primaryauthMocks = [ $primary ];
814  $this->initializeManager( true );
815  $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
816  ->will( $this->returnValue(
817  AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
818  ) );
820  $res->createRequest = $req2;
821  $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
822  ->will( $this->returnValue( $res ) );
823  $this->logger->setCollect( true );
824  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
825  $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
826  $ret = $this->manager->continueAuthentication( [] );
827  $this->logger->setCollect( false );
828  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
829  $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
830  $this->assertSame( $req2, $ret->createRequest->createRequest );
831  $this->assertEquals( [], $ret->createRequest->maybeLink );
832 
833  // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
834  $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
835  $this->primaryauthMocks = [ $primary ];
836  $this->initializeManager( true );
837  $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
838  $createReq->returnToUrl = 'http://localhost/';
839  $createReq->username = 'UTDummy';
840  $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
841  $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
842  ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
843  ->will( $this->returnValue( $res ) );
844  $primary->expects( $this->any() )->method( 'accountCreationType' )
845  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
846  $this->logger->setCollect( true );
847  $ret = $this->manager->beginAccountCreation(
848  $user, [ $userReq, $createReq ], 'http://localhost/'
849  );
850  $this->logger->setCollect( false );
851  $this->assertSame( AuthenticationResponse::UI, $ret->status );
852  $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
853  $this->assertNotNull( $state );
854  $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
855  $this->assertEquals( [ $req2 ], $state['maybeLink'] );
856  }
857 
866  public function testAuthentication(
867  StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
868  array $managerResponses, $link = false
869  ) {
870  $this->initializeManager();
871  $user = \User::newFromName( 'UTSysop' );
872  $id = $user->getId();
873  $name = $user->getName();
874 
875  // Set up lots of mocks...
877  $req->rememberMe = (bool)rand( 0, 1 );
878  $req->pre = $preResponse;
879  $req->primary = $primaryResponses;
880  $req->secondary = $secondaryResponses;
881  $mocks = [];
882  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
883  $class = ucfirst( $key ) . 'AuthenticationProvider';
884  $mocks[$key] = $this->getMockForAbstractClass(
885  "MediaWiki\\Auth\\$class", [], "Mock$class"
886  );
887  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
888  ->will( $this->returnValue( $key ) );
889  $mocks[$key . '2'] = $this->getMockForAbstractClass(
890  "MediaWiki\\Auth\\$class", [], "Mock$class"
891  );
892  $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
893  ->will( $this->returnValue( $key . '2' ) );
894  $mocks[$key . '3'] = $this->getMockForAbstractClass(
895  "MediaWiki\\Auth\\$class", [], "Mock$class"
896  );
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', $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->assertEquals( $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 = isset( $state['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  $user = \User::newFromName( 'UTSysop' );
1100  $id = $user->getId();
1101  $name = $user->getName();
1102 
1103  $rememberReq = new RememberMeAuthenticationRequest;
1104  $rememberReq->action = AuthManager::ACTION_LOGIN;
1105 
1106  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1107  $req->foobar = 'baz';
1108  $restartResponse = AuthenticationResponse::newRestart(
1109  $this->message( 'authmanager-authn-no-local-user' )
1110  );
1111  $restartResponse->neededRequests = [ $rememberReq ];
1112 
1113  $restartResponse2Pass = AuthenticationResponse::newPass( null );
1114  $restartResponse2Pass->linkRequest = $req;
1115  $restartResponse2 = AuthenticationResponse::newRestart(
1116  $this->message( 'authmanager-authn-no-local-user-link' )
1117  );
1118  $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1119  null, [ $req->getUniqueId() => $req ]
1120  );
1121  $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
1122  $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
1123 
1124  return [
1125  'Failure in pre-auth' => [
1126  StatusValue::newFatal( 'fail-from-pre' ),
1127  [],
1128  [],
1129  [
1130  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
1132  $this->message( 'authmanager-authn-not-in-progress' )
1133  ),
1134  ]
1135  ],
1136  'Failure in primary' => [
1138  $tmp = [
1139  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1140  ],
1141  [],
1142  $tmp
1143  ],
1144  'All primary abstain' => [
1146  [
1148  ],
1149  [],
1150  [
1151  AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1152  ]
1153  ],
1154  'Primary UI, then redirect, then fail' => [
1156  $tmp = [
1157  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1158  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1159  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1160  ],
1161  [],
1162  $tmp
1163  ],
1164  'Primary redirect, then abstain' => [
1166  [
1168  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1169  ),
1171  ],
1172  [],
1173  [
1174  $tmp,
1175  new \DomainException(
1176  'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1177  )
1178  ]
1179  ],
1180  'Primary UI, then pass with no local user' => [
1182  [
1183  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1185  ],
1186  [],
1187  [
1188  $tmp,
1189  $restartResponse,
1190  ]
1191  ],
1192  'Primary UI, then pass with no local user (link type)' => [
1194  [
1195  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1196  $restartResponse2Pass,
1197  ],
1198  [],
1199  [
1200  $tmp,
1201  $restartResponse2,
1202  ],
1203  true
1204  ],
1205  'Primary pass with invalid username' => [
1207  [
1209  ],
1210  [],
1211  [
1212  new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1213  ]
1214  ],
1215  'Secondary fail' => [
1217  [
1219  ],
1220  $tmp = [
1221  AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1222  ],
1223  $tmp
1224  ],
1225  'Secondary UI, then abstain' => [
1227  [
1229  ],
1230  [
1231  $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1233  ],
1234  [
1235  $tmp,
1237  ]
1238  ],
1239  'Secondary pass' => [
1241  [
1243  ],
1244  [
1246  ],
1247  [
1249  ]
1250  ],
1251  ];
1252  }
1253 
1260  public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1261  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1262  $mock1->expects( $this->any() )->method( 'getUniqueId' )
1263  ->will( $this->returnValue( 'primary1' ) );
1264  $mock1->expects( $this->any() )->method( 'testUserExists' )
1265  ->with( $this->equalTo( 'UTSysop' ) )
1266  ->will( $this->returnValue( $primary1Exists ) );
1267  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1268  $mock2->expects( $this->any() )->method( 'getUniqueId' )
1269  ->will( $this->returnValue( 'primary2' ) );
1270  $mock2->expects( $this->any() )->method( 'testUserExists' )
1271  ->with( $this->equalTo( 'UTSysop' ) )
1272  ->will( $this->returnValue( $primary2Exists ) );
1273  $this->primaryauthMocks = [ $mock1, $mock2 ];
1274 
1275  $this->initializeManager( true );
1276  $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
1277  }
1278 
1279  public static function provideUserExists() {
1280  return [
1281  [ false, false, false ],
1282  [ true, false, true ],
1283  [ false, true, true ],
1284  [ true, true, true ],
1285  ];
1286  }
1287 
1294  public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1295  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1296 
1297  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1298  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1299  $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1300  ->with( $this->equalTo( $req ) )
1301  ->will( $this->returnValue( $primaryReturn ) );
1302  $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
1303  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1304  $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1305  ->with( $this->equalTo( $req ) )
1306  ->will( $this->returnValue( $secondaryReturn ) );
1307 
1308  $this->primaryauthMocks = [ $mock1 ];
1309  $this->secondaryauthMocks = [ $mock2 ];
1310  $this->initializeManager( true );
1311  $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
1312  }
1313 
1314  public static function provideAllowsAuthenticationDataChange() {
1315  $ignored = \Status::newGood( 'ignored' );
1316  $ignored->warning( 'authmanager-change-not-supported' );
1317 
1318  $okFromPrimary = StatusValue::newGood();
1319  $okFromPrimary->warning( 'warning-from-primary' );
1320  $okFromSecondary = StatusValue::newGood();
1321  $okFromSecondary->warning( 'warning-from-secondary' );
1322 
1323  return [
1324  [
1327  \Status::newGood(),
1328  ],
1329  [
1331  StatusValue::newGood( 'ignore' ),
1332  \Status::newGood(),
1333  ],
1334  [
1335  StatusValue::newGood( 'ignored' ),
1337  \Status::newGood(),
1338  ],
1339  [
1340  StatusValue::newGood( 'ignored' ),
1341  StatusValue::newGood( 'ignored' ),
1342  $ignored,
1343  ],
1344  [
1345  StatusValue::newFatal( 'fail from primary' ),
1347  \Status::newFatal( 'fail from primary' ),
1348  ],
1349  [
1350  $okFromPrimary,
1352  \Status::wrap( $okFromPrimary ),
1353  ],
1354  [
1356  StatusValue::newFatal( 'fail from secondary' ),
1357  \Status::newFatal( 'fail from secondary' ),
1358  ],
1359  [
1361  $okFromSecondary,
1362  \Status::wrap( $okFromSecondary ),
1363  ],
1364  ];
1365  }
1366 
1367  public function testChangeAuthenticationData() {
1368  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1369  $req->username = 'UTSysop';
1370 
1371  $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1372  $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1373  $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1374  ->with( $this->equalTo( $req ) );
1375  $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1376  $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1377  $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1378  ->with( $this->equalTo( $req ) );
1379 
1380  $this->primaryauthMocks = [ $mock1, $mock2 ];
1381  $this->initializeManager( true );
1382  $this->logger->setCollect( true );
1383  $this->manager->changeAuthenticationData( $req );
1384  $this->assertSame( [
1385  [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
1386  ], $this->logger->getBuffer() );
1387  }
1388 
1389  public function testCanCreateAccounts() {
1390  $types = [
1394  ];
1395 
1396  foreach ( $types as $type => $can ) {
1397  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1398  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1399  $mock->expects( $this->any() )->method( 'accountCreationType' )
1400  ->will( $this->returnValue( $type ) );
1401  $this->primaryauthMocks = [ $mock ];
1402  $this->initializeManager( true );
1403  $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
1404  }
1405  }
1406 
1409 
1410  $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
1411 
1412  $this->initializeManager( true );
1413 
1414  $wgGroupPermissions['*']['createaccount'] = true;
1415  $this->assertEquals(
1416  \Status::newGood(),
1417  $this->manager->checkAccountCreatePermissions( new \User )
1418  );
1419 
1420  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1421  $this->assertEquals(
1422  \Status::newFatal( 'readonlytext', 'Because' ),
1423  $this->manager->checkAccountCreatePermissions( new \User )
1424  );
1425  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1426 
1427  $wgGroupPermissions['*']['createaccount'] = false;
1428  $status = $this->manager->checkAccountCreatePermissions( new \User );
1429  $this->assertFalse( $status->isOK() );
1430  $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1431  $wgGroupPermissions['*']['createaccount'] = true;
1432 
1433  $user = \User::newFromName( 'UTBlockee' );
1434  if ( $user->getID() == 0 ) {
1435  $user->addToDatabase();
1436  \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1437  $user->saveSettings();
1438  }
1439  $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1440  if ( $oldBlock ) {
1441  // An old block will prevent our new one from saving.
1442  $oldBlock->delete();
1443  }
1444  $blockOptions = [
1445  'address' => 'UTBlockee',
1446  'user' => $user->getID(),
1447  'reason' => __METHOD__,
1448  'expiry' => time() + 100500,
1449  'createAccount' => true,
1450  ];
1451  $block = new \Block( $blockOptions );
1452  $block->insert();
1453  $status = $this->manager->checkAccountCreatePermissions( $user );
1454  $this->assertFalse( $status->isOK() );
1455  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1456 
1457  $blockOptions = [
1458  'address' => '127.0.0.0/24',
1459  'reason' => __METHOD__,
1460  'expiry' => time() + 100500,
1461  'createAccount' => true,
1462  ];
1463  $block = new \Block( $blockOptions );
1464  $block->insert();
1465  $scopeVariable = new \ScopedCallback( [ $block, 'delete' ] );
1466  $status = $this->manager->checkAccountCreatePermissions( new \User );
1467  $this->assertFalse( $status->isOK() );
1468  $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1469  \ScopedCallback::consume( $scopeVariable );
1470 
1471  $this->setMwGlobals( [
1472  'wgEnableDnsBlacklist' => true,
1473  'wgDnsBlacklistUrls' => [
1474  'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1475  ],
1476  'wgProxyWhitelist' => [],
1477  ] );
1478  $status = $this->manager->checkAccountCreatePermissions( new \User );
1479  $this->assertFalse( $status->isOK() );
1480  $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1481  $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
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  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1614  $this->hook( 'LocalUserCreated', $this->never() );
1615  $userReq->username = self::usernameForCreation();
1616  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1617  $this->unhook( 'LocalUserCreated' );
1618  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1619  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1620  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1621  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1622 
1623  $this->hook( 'LocalUserCreated', $this->never() );
1624  $userReq->username = self::usernameForCreation();
1625  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1626  $this->unhook( 'LocalUserCreated' );
1627  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1628  $this->assertSame( 'userexists', $ret->message->getKey() );
1629 
1630  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1631  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1632  $mock->expects( $this->any() )->method( 'accountCreationType' )
1633  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1634  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1635  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1636  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1637  $this->primaryauthMocks = [ $mock ];
1638  $this->initializeManager( true );
1639 
1640  $this->hook( 'LocalUserCreated', $this->never() );
1641  $userReq->username = self::usernameForCreation();
1642  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1643  $this->unhook( 'LocalUserCreated' );
1644  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1645  $this->assertSame( 'fail', $ret->message->getKey() );
1646 
1647  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1648  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1649  $mock->expects( $this->any() )->method( 'accountCreationType' )
1650  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1651  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1652  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1653  ->will( $this->returnValue( StatusValue::newGood() ) );
1654  $this->primaryauthMocks = [ $mock ];
1655  $this->initializeManager( true );
1656 
1657  $this->hook( 'LocalUserCreated', $this->never() );
1658  $userReq->username = self::usernameForCreation() . '<>';
1659  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1660  $this->unhook( 'LocalUserCreated' );
1661  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1662  $this->assertSame( 'noname', $ret->message->getKey() );
1663 
1664  $this->hook( 'LocalUserCreated', $this->never() );
1665  $userReq->username = $creator->getName();
1666  $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1667  $this->unhook( 'LocalUserCreated' );
1668  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1669  $this->assertSame( 'userexists', $ret->message->getKey() );
1670 
1671  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1672  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1673  $mock->expects( $this->any() )->method( 'accountCreationType' )
1674  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1675  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1676  $mock->expects( $this->any() )->method( 'testUserForCreation' )
1677  ->will( $this->returnValue( StatusValue::newGood() ) );
1678  $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1679  ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1680  $this->primaryauthMocks = [ $mock ];
1681  $this->initializeManager( true );
1682 
1683  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1684  ->setMethods( [ 'populateUser' ] )
1685  ->getMock();
1686  $req->expects( $this->any() )->method( 'populateUser' )
1687  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1688  $userReq->username = self::usernameForCreation();
1689  $ret = $this->manager->beginAccountCreation(
1690  $creator, [ $userReq, $req ], 'http://localhost/'
1691  );
1692  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1693  $this->assertSame( 'populatefail', $ret->message->getKey() );
1694 
1696  $userReq->username = self::usernameForCreation();
1697 
1698  $ret = $this->manager->beginAccountCreation(
1699  $creator, [ $userReq, $req ], 'http://localhost/'
1700  );
1701  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1702  $this->assertSame( 'fail', $ret->message->getKey() );
1703 
1704  $this->manager->beginAccountCreation(
1705  \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1706  );
1707  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1708  $this->assertSame( 'fail', $ret->message->getKey() );
1709  }
1710 
1711  public function testContinueAccountCreation() {
1712  $creator = \User::newFromName( 'UTSysop' );
1713  $username = self::usernameForCreation();
1714  $this->logger = new \TestLogger( false, function ( $message, $level ) {
1715  return $level === LogLevel::DEBUG ? null : $message;
1716  } );
1717  $this->initializeManager();
1718 
1719  $session = [
1720  'userid' => 0,
1721  'username' => $username,
1722  'creatorid' => 0,
1723  'creatorname' => $username,
1724  'reqs' => [],
1725  'primary' => null,
1726  'primaryResponse' => null,
1727  'secondary' => [],
1728  'ranPreTests' => true,
1729  ];
1730 
1731  $this->hook( 'LocalUserCreated', $this->never() );
1732  try {
1733  $this->manager->continueAccountCreation( [] );
1734  $this->fail( 'Expected exception not thrown' );
1735  } catch ( \LogicException $ex ) {
1736  $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1737  }
1738  $this->unhook( 'LocalUserCreated' );
1739 
1740  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1741  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1742  $mock->expects( $this->any() )->method( 'accountCreationType' )
1743  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1744  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1745  $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1746  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1747  );
1748  $this->primaryauthMocks = [ $mock ];
1749  $this->initializeManager( true );
1750 
1751  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1752  $this->hook( 'LocalUserCreated', $this->never() );
1753  $ret = $this->manager->continueAccountCreation( [] );
1754  $this->unhook( 'LocalUserCreated' );
1755  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1756  $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1757 
1758  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1759  [ 'username' => "$username<>" ] + $session );
1760  $this->hook( 'LocalUserCreated', $this->never() );
1761  $ret = $this->manager->continueAccountCreation( [] );
1762  $this->unhook( 'LocalUserCreated' );
1763  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1764  $this->assertSame( 'noname', $ret->message->getKey() );
1765  $this->assertNull(
1766  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1767  );
1768 
1769  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1770  $this->hook( 'LocalUserCreated', $this->never() );
1772  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1773  $ret = $this->manager->continueAccountCreation( [] );
1774  unset( $lock );
1775  $this->unhook( 'LocalUserCreated' );
1776  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1777  $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1778  // This error shouldn't remove the existing session, because the
1779  // raced-with process "owns" it.
1780  $this->assertSame(
1781  $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1782  );
1783 
1784  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1785  [ 'username' => $creator->getName() ] + $session );
1786  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
1787  $this->hook( 'LocalUserCreated', $this->never() );
1788  $ret = $this->manager->continueAccountCreation( [] );
1789  $this->unhook( 'LocalUserCreated' );
1790  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1791  $this->assertSame( 'readonlytext', $ret->message->getKey() );
1792  $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1793  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
1794 
1795  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1796  [ 'username' => $creator->getName() ] + $session );
1797  $this->hook( 'LocalUserCreated', $this->never() );
1798  $ret = $this->manager->continueAccountCreation( [] );
1799  $this->unhook( 'LocalUserCreated' );
1800  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1801  $this->assertSame( 'userexists', $ret->message->getKey() );
1802  $this->assertNull(
1803  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1804  );
1805 
1806  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1807  [ 'userid' => $creator->getId() ] + $session );
1808  $this->hook( 'LocalUserCreated', $this->never() );
1809  try {
1810  $ret = $this->manager->continueAccountCreation( [] );
1811  $this->fail( 'Expected exception not thrown' );
1812  } catch ( \UnexpectedValueException $ex ) {
1813  $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1814  }
1815  $this->unhook( 'LocalUserCreated' );
1816  $this->assertNull(
1817  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1818  );
1819 
1820  $id = $creator->getId();
1821  $name = $creator->getName();
1822  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1823  [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1824  $this->hook( 'LocalUserCreated', $this->never() );
1825  try {
1826  $ret = $this->manager->continueAccountCreation( [] );
1827  $this->fail( 'Expected exception not thrown' );
1828  } catch ( \UnexpectedValueException $ex ) {
1829  $this->assertEquals(
1830  "User \"{$name}\" exists, but ID $id != " . ( $id + 1 ) . '!', $ex->getMessage()
1831  );
1832  }
1833  $this->unhook( 'LocalUserCreated' );
1834  $this->assertNull(
1835  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1836  );
1837 
1838  $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1839  ->setMethods( [ 'populateUser' ] )
1840  ->getMock();
1841  $req->expects( $this->any() )->method( 'populateUser' )
1842  ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1843  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1844  [ 'reqs' => [ $req ] ] + $session );
1845  $ret = $this->manager->continueAccountCreation( [] );
1846  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1847  $this->assertSame( 'populatefail', $ret->message->getKey() );
1848  $this->assertNull(
1849  $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1850  );
1851  }
1852 
1862  public function testAccountCreation(
1863  StatusValue $preTest, $primaryTest, $secondaryTest,
1864  array $primaryResponses, array $secondaryResponses, array $managerResponses
1865  ) {
1866  $creator = \User::newFromName( 'UTSysop' );
1867  $username = self::usernameForCreation();
1868 
1869  $this->initializeManager();
1870 
1871  // Set up lots of mocks...
1872  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1873  $req->preTest = $preTest;
1874  $req->primaryTest = $primaryTest;
1875  $req->secondaryTest = $secondaryTest;
1876  $req->primary = $primaryResponses;
1877  $req->secondary = $secondaryResponses;
1878  $mocks = [];
1879  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1880  $class = ucfirst( $key ) . 'AuthenticationProvider';
1881  $mocks[$key] = $this->getMockForAbstractClass(
1882  "MediaWiki\\Auth\\$class", [], "Mock$class"
1883  );
1884  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1885  ->will( $this->returnValue( $key ) );
1886  $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1887  ->will( $this->returnValue( StatusValue::newGood() ) );
1888  $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1889  ->will( $this->returnCallback(
1890  function ( $user, $creatorIn, $reqs )
1891  use ( $username, $creator, $req, $key )
1892  {
1893  $this->assertSame( $username, $user->getName() );
1894  $this->assertSame( $creator->getId(), $creatorIn->getId() );
1895  $this->assertSame( $creator->getName(), $creatorIn->getName() );
1896  $foundReq = false;
1897  foreach ( $reqs as $r ) {
1898  $this->assertSame( $username, $r->username );
1899  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1900  }
1901  $this->assertTrue( $foundReq, '$reqs contains $req' );
1902  $k = $key . 'Test';
1903  return $req->$k;
1904  }
1905  ) );
1906 
1907  for ( $i = 2; $i <= 3; $i++ ) {
1908  $mocks[$key . $i] = $this->getMockForAbstractClass(
1909  "MediaWiki\\Auth\\$class", [], "Mock$class"
1910  );
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', $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->assertEquals( $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(
2274  "MediaWiki\\Auth\\PrimaryAuthenticationProvider", []
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() {
2354 
2355  // PHPUnit seems to have a bug where it will call the ->with()
2356  // callbacks for our hooks again after the test is run (WTF?), which
2357  // breaks here because $username no longer matches $user by the end of
2358  // the testing.
2359  $workaroundPHPUnitBug = false;
2360 
2361  $username = self::usernameForCreation();
2362  $this->initializeManager();
2363 
2364  $this->stashMwGlobals( [ 'wgGroupPermissions' ] );
2365  $wgGroupPermissions['*']['createaccount'] = true;
2366  $wgGroupPermissions['*']['autocreateaccount'] = false;
2367 
2368  \ObjectCache::$instances[__METHOD__] = new \HashBagOStuff();
2369  $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2370 
2371  // Set up lots of mocks...
2372  $mocks = [];
2373  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2374  $class = ucfirst( $key ) . 'AuthenticationProvider';
2375  $mocks[$key] = $this->getMockForAbstractClass(
2376  "MediaWiki\\Auth\\$class", [], "Mock$class"
2377  );
2378  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2379  ->will( $this->returnValue( $key ) );
2380  }
2381 
2382  $good = StatusValue::newGood();
2383  $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2384  return $workaroundPHPUnitBug || $user->getName() === $username;
2385  } );
2386 
2387  $mocks['pre']->expects( $this->exactly( 12 ) )->method( 'testUserForCreation' )
2388  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2389  ->will( $this->onConsecutiveCalls(
2390  StatusValue::newFatal( 'ok' ), StatusValue::newFatal( 'ok' ), // For testing permissions
2391  StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2392  $good, // backoff test
2393  $good, // addToDatabase fails test
2394  $good, // addToDatabase throws test
2395  $good, // addToDatabase exists test
2396  $good, $good, $good // success
2397  ) );
2398 
2399  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2400  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2401  $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2402  ->will( $this->returnValue( true ) );
2403  $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2404  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2405  ->will( $this->onConsecutiveCalls(
2406  StatusValue::newFatal( 'fail-in-primary' ), $good,
2407  $good, // backoff test
2408  $good, // addToDatabase fails test
2409  $good, // addToDatabase throws test
2410  $good, // addToDatabase exists test
2411  $good, $good, $good
2412  ) );
2413  $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2414  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2415 
2416  $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2417  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) )
2418  ->will( $this->onConsecutiveCalls(
2419  StatusValue::newFatal( 'fail-in-secondary' ),
2420  $good, // backoff test
2421  $good, // addToDatabase fails test
2422  $good, // addToDatabase throws test
2423  $good, // addToDatabase exists test
2424  $good, $good, $good
2425  ) );
2426  $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2427  ->with( $callback, $this->identicalTo( AuthManager::AUTOCREATE_SOURCE_SESSION ) );
2428 
2429  $this->preauthMocks = [ $mocks['pre'] ];
2430  $this->primaryauthMocks = [ $mocks['primary'] ];
2431  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2432  $this->initializeManager( true );
2433  $session = $this->request->getSession();
2434 
2435  $logger = new \TestLogger( true, function ( $m ) {
2436  $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2437  return $m;
2438  } );
2439  $this->manager->setLogger( $logger );
2440 
2441  try {
2442  $user = \User::newFromName( 'UTSysop' );
2443  $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2444  $this->fail( 'Expected exception not thrown' );
2445  } catch ( \InvalidArgumentException $ex ) {
2446  $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2447  }
2448 
2449  // First, check an existing user
2450  $session->clear();
2451  $user = \User::newFromName( 'UTSysop' );
2452  $this->hook( 'LocalUserCreated', $this->never() );
2453  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2454  $this->unhook( 'LocalUserCreated' );
2455  $expect = \Status::newGood();
2456  $expect->warning( 'userexists' );
2457  $this->assertEquals( $expect, $ret );
2458  $this->assertNotEquals( 0, $user->getId() );
2459  $this->assertSame( 'UTSysop', $user->getName() );
2460  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2461  $this->assertSame( [
2462  [ LogLevel::DEBUG, '{username} already exists locally' ],
2463  ], $logger->getBuffer() );
2464  $logger->clearBuffer();
2465 
2466  $session->clear();
2467  $user = \User::newFromName( 'UTSysop' );
2468  $this->hook( 'LocalUserCreated', $this->never() );
2469  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2470  $this->unhook( 'LocalUserCreated' );
2471  $expect = \Status::newGood();
2472  $expect->warning( 'userexists' );
2473  $this->assertEquals( $expect, $ret );
2474  $this->assertNotEquals( 0, $user->getId() );
2475  $this->assertSame( 'UTSysop', $user->getName() );
2476  $this->assertEquals( 0, $session->getUser()->getId() );
2477  $this->assertSame( [
2478  [ LogLevel::DEBUG, '{username} already exists locally' ],
2479  ], $logger->getBuffer() );
2480  $logger->clearBuffer();
2481 
2482  // Wiki is read-only
2483  $session->clear();
2484  $this->setMwGlobals( [ 'wgReadOnly' => 'Because' ] );
2486  $this->hook( 'LocalUserCreated', $this->never() );
2487  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2488  $this->unhook( 'LocalUserCreated' );
2489  $this->assertEquals( \Status::newFatal( 'readonlytext', 'Because' ), $ret );
2490  $this->assertEquals( 0, $user->getId() );
2491  $this->assertNotEquals( $username, $user->getName() );
2492  $this->assertEquals( 0, $session->getUser()->getId() );
2493  $this->assertSame( [
2494  [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2495  ], $logger->getBuffer() );
2496  $logger->clearBuffer();
2497  $this->setMwGlobals( [ 'wgReadOnly' => false ] );
2498 
2499  // Session blacklisted
2500  $session->clear();
2501  $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2503  $this->hook( 'LocalUserCreated', $this->never() );
2504  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2505  $this->unhook( 'LocalUserCreated' );
2506  $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2507  $this->assertEquals( 0, $user->getId() );
2508  $this->assertNotEquals( $username, $user->getName() );
2509  $this->assertEquals( 0, $session->getUser()->getId() );
2510  $this->assertSame( [
2511  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2512  ], $logger->getBuffer() );
2513  $logger->clearBuffer();
2514 
2515  $session->clear();
2516  $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2518  $this->hook( 'LocalUserCreated', $this->never() );
2519  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2520  $this->unhook( 'LocalUserCreated' );
2521  $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2522  $this->assertEquals( 0, $user->getId() );
2523  $this->assertNotEquals( $username, $user->getName() );
2524  $this->assertEquals( 0, $session->getUser()->getId() );
2525  $this->assertSame( [
2526  [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2527  ], $logger->getBuffer() );
2528  $logger->clearBuffer();
2529 
2530  // Uncreatable name
2531  $session->clear();
2532  $user = \User::newFromName( $username . '@' );
2533  $this->hook( 'LocalUserCreated', $this->never() );
2534  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2535  $this->unhook( 'LocalUserCreated' );
2536  $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2537  $this->assertEquals( 0, $user->getId() );
2538  $this->assertNotEquals( $username . '@', $user->getId() );
2539  $this->assertEquals( 0, $session->getUser()->getId() );
2540  $this->assertSame( [
2541  [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2542  ], $logger->getBuffer() );
2543  $logger->clearBuffer();
2544  $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2545 
2546  // IP unable to create accounts
2547  $wgGroupPermissions['*']['createaccount'] = false;
2548  $wgGroupPermissions['*']['autocreateaccount'] = false;
2549  $session->clear();
2551  $this->hook( 'LocalUserCreated', $this->never() );
2552  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2553  $this->unhook( 'LocalUserCreated' );
2554  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2555  $this->assertEquals( 0, $user->getId() );
2556  $this->assertNotEquals( $username, $user->getName() );
2557  $this->assertEquals( 0, $session->getUser()->getId() );
2558  $this->assertSame( [
2559  [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2560  ], $logger->getBuffer() );
2561  $logger->clearBuffer();
2562  $this->assertSame(
2563  'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2564  );
2565 
2566  // Test that both permutations of permissions are allowed
2567  // (this hits the two "ok" entries in $mocks['pre'])
2568  $wgGroupPermissions['*']['createaccount'] = false;
2569  $wgGroupPermissions['*']['autocreateaccount'] = true;
2570  $session->clear();
2572  $this->hook( 'LocalUserCreated', $this->never() );
2573  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2574  $this->unhook( 'LocalUserCreated' );
2575  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2576 
2577  $wgGroupPermissions['*']['createaccount'] = true;
2578  $wgGroupPermissions['*']['autocreateaccount'] = false;
2579  $session->clear();
2581  $this->hook( 'LocalUserCreated', $this->never() );
2582  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2583  $this->unhook( 'LocalUserCreated' );
2584  $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2585  $logger->clearBuffer();
2586 
2587  // Test lock fail
2588  $session->clear();
2590  $this->hook( 'LocalUserCreated', $this->never() );
2592  $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2593  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2594  unset( $lock );
2595  $this->unhook( 'LocalUserCreated' );
2596  $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2597  $this->assertEquals( 0, $user->getId() );
2598  $this->assertNotEquals( $username, $user->getName() );
2599  $this->assertEquals( 0, $session->getUser()->getId() );
2600  $this->assertSame( [
2601  [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2602  ], $logger->getBuffer() );
2603  $logger->clearBuffer();
2604 
2605  // Test pre-authentication provider fail
2606  $session->clear();
2608  $this->hook( 'LocalUserCreated', $this->never() );
2609  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2610  $this->unhook( 'LocalUserCreated' );
2611  $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2612  $this->assertEquals( 0, $user->getId() );
2613  $this->assertNotEquals( $username, $user->getName() );
2614  $this->assertEquals( 0, $session->getUser()->getId() );
2615  $this->assertSame( [
2616  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2617  ], $logger->getBuffer() );
2618  $logger->clearBuffer();
2619  $this->assertEquals(
2620  StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2621  );
2622 
2623  $session->clear();
2625  $this->hook( 'LocalUserCreated', $this->never() );
2626  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2627  $this->unhook( 'LocalUserCreated' );
2628  $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2629  $this->assertEquals( 0, $user->getId() );
2630  $this->assertNotEquals( $username, $user->getName() );
2631  $this->assertEquals( 0, $session->getUser()->getId() );
2632  $this->assertSame( [
2633  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2634  ], $logger->getBuffer() );
2635  $logger->clearBuffer();
2636  $this->assertEquals(
2637  StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2638  );
2639 
2640  $session->clear();
2642  $this->hook( 'LocalUserCreated', $this->never() );
2643  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2644  $this->unhook( 'LocalUserCreated' );
2645  $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2646  $this->assertEquals( 0, $user->getId() );
2647  $this->assertNotEquals( $username, $user->getName() );
2648  $this->assertEquals( 0, $session->getUser()->getId() );
2649  $this->assertSame( [
2650  [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2651  ], $logger->getBuffer() );
2652  $logger->clearBuffer();
2653  $this->assertEquals(
2654  StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2655  );
2656 
2657  // Test backoff
2659  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2660  $cache->set( $backoffKey, true );
2661  $session->clear();
2663  $this->hook( 'LocalUserCreated', $this->never() );
2664  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2665  $this->unhook( 'LocalUserCreated' );
2666  $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2667  $this->assertEquals( 0, $user->getId() );
2668  $this->assertNotEquals( $username, $user->getName() );
2669  $this->assertEquals( 0, $session->getUser()->getId() );
2670  $this->assertSame( [
2671  [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2672  ], $logger->getBuffer() );
2673  $logger->clearBuffer();
2674  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2675  $cache->delete( $backoffKey );
2676 
2677  // Test addToDatabase fails
2678  $session->clear();
2679  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2680  $user->expects( $this->once() )->method( 'addToDatabase' )
2681  ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2682  $user->setName( $username );
2683  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2684  $this->assertEquals( \Status::newFatal( 'because' ), $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::INFO, 'creating new user ({username}) - from: {from}' ],
2690  [ LogLevel::ERROR, '{username} failed with message {message}' ],
2691  ], $logger->getBuffer() );
2692  $logger->clearBuffer();
2693  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2694 
2695  // Test addToDatabase throws an exception
2697  $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2698  $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2699  $session->clear();
2700  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2701  $user->expects( $this->once() )->method( 'addToDatabase' )
2702  ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2703  $user->setName( $username );
2704  try {
2705  $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2706  $this->fail( 'Expected exception not thrown' );
2707  } catch ( \Exception $ex ) {
2708  $this->assertSame( 'Excepted', $ex->getMessage() );
2709  }
2710  $this->assertEquals( 0, $user->getId() );
2711  $this->assertEquals( 0, $session->getUser()->getId() );
2712  $this->assertSame( [
2713  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2714  [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2715  ], $logger->getBuffer() );
2716  $logger->clearBuffer();
2717  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2718  $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2719  $cache->delete( $backoffKey );
2720 
2721  // Test addToDatabase fails because the user already exists.
2722  $session->clear();
2723  $user = $this->getMock( 'User', [ 'addToDatabase' ] );
2724  $user->expects( $this->once() )->method( 'addToDatabase' )
2725  ->will( $this->returnCallback( function () use ( $username ) {
2726  $status = \User::newFromName( $username )->addToDatabase();
2727  $this->assertTrue( $status->isOK(), 'sanity check' );
2728  return \Status::newFatal( 'userexists' );
2729  } ) );
2730  $user->setName( $username );
2731  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2732  $expect = \Status::newGood();
2733  $expect->warning( 'userexists' );
2734  $this->assertEquals( $expect, $ret );
2735  $this->assertNotEquals( 0, $user->getId() );
2736  $this->assertEquals( $username, $user->getName() );
2737  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2738  $this->assertSame( [
2739  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2740  [ LogLevel::INFO, '{username} already exists locally (race)' ],
2741  ], $logger->getBuffer() );
2742  $logger->clearBuffer();
2743  $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2744 
2745  // Success!
2746  $session->clear();
2747  $username = self::usernameForCreation();
2749  $this->hook( 'AuthPluginAutoCreate', $this->once() )
2750  ->with( $callback );
2751  $this->hideDeprecated( 'AuthPluginAutoCreate hook (used in ' .
2752  get_class( $wgHooks['AuthPluginAutoCreate'][0] ) . '::onAuthPluginAutoCreate)' );
2753  $this->hook( 'LocalUserCreated', $this->once() )
2754  ->with( $callback, $this->equalTo( true ) );
2755  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2756  $this->unhook( 'LocalUserCreated' );
2757  $this->unhook( 'AuthPluginAutoCreate' );
2758  $this->assertEquals( \Status::newGood(), $ret );
2759  $this->assertNotEquals( 0, $user->getId() );
2760  $this->assertEquals( $username, $user->getName() );
2761  $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2762  $this->assertSame( [
2763  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2764  ], $logger->getBuffer() );
2765  $logger->clearBuffer();
2766 
2767  $dbw = wfGetDB( DB_MASTER );
2768  $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
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, false );
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( 0, $session->getUser()->getId() );
2780  $this->assertSame( [
2781  [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2782  ], $logger->getBuffer() );
2783  $logger->clearBuffer();
2784  $this->assertSame(
2785  $maxLogId,
2786  $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2787  );
2788 
2789  $this->config->set( 'NewUserLog', true );
2790  $session->clear();
2791  $username = self::usernameForCreation();
2793  $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2794  $this->assertEquals( \Status::newGood(), $ret );
2795  $logger->clearBuffer();
2796 
2798  $rows = iterator_to_array( $dbw->select(
2799  $data['tables'],
2800  $data['fields'],
2801  [
2802  'log_id > ' . (int)$maxLogId,
2803  'log_type' => 'newusers'
2804  ] + $data['conds'],
2805  __METHOD__,
2806  $data['options'],
2807  $data['join_conds']
2808  ) );
2809  $this->assertCount( 1, $rows );
2810  $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2811 
2812  $this->assertSame( 'autocreate', $entry->getSubtype() );
2813  $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2814  $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2815  $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2816  $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2817 
2818  $workaroundPHPUnitBug = true;
2819  }
2820 
2827  public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2828  $makeReq = function ( $key ) use ( $action ) {
2829  $req = $this->getMock( AuthenticationRequest::class );
2830  $req->expects( $this->any() )->method( 'getUniqueId' )
2831  ->will( $this->returnValue( $key ) );
2833  $req->key = $key;
2834  return $req;
2835  };
2836  $cmpReqs = function ( $a, $b ) {
2837  $ret = strcmp( get_class( $a ), get_class( $b ) );
2838  if ( !$ret ) {
2839  $ret = strcmp( $a->key, $b->key );
2840  }
2841  return $ret;
2842  };
2843 
2844  $good = StatusValue::newGood();
2845 
2846  $mocks = [];
2847  foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2848  $class = ucfirst( $key ) . 'AuthenticationProvider';
2849  $mocks[$key] = $this->getMockForAbstractClass(
2850  "MediaWiki\\Auth\\$class", [], "Mock$class"
2851  );
2852  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2853  ->will( $this->returnValue( $key ) );
2854  $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2855  ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2856  return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2857  } ) );
2858  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2859  ->will( $this->returnValue( $good ) );
2860  }
2861 
2862  $primaries = [];
2863  foreach ( [
2867  ] as $type ) {
2868  $class = 'PrimaryAuthenticationProvider';
2869  $mocks["primary-$type"] = $this->getMockForAbstractClass(
2870  "MediaWiki\\Auth\\$class", [], "Mock$class"
2871  );
2872  $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2873  ->will( $this->returnValue( "primary-$type" ) );
2874  $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2875  ->will( $this->returnValue( $type ) );
2876  $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2877  ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2878  return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2879  } ) );
2880  $mocks["primary-$type"]->expects( $this->any() )
2881  ->method( 'providerAllowsAuthenticationDataChange' )
2882  ->will( $this->returnValue( $good ) );
2883  $this->primaryauthMocks[] = $mocks["primary-$type"];
2884  }
2885 
2886  $mocks['primary2'] = $this->getMockForAbstractClass(
2887  PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider"
2888  );
2889  $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2890  ->will( $this->returnValue( 'primary2' ) );
2891  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2892  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2893  $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2894  ->will( $this->returnValue( [] ) );
2895  $mocks['primary2']->expects( $this->any() )
2896  ->method( 'providerAllowsAuthenticationDataChange' )
2897  ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2898  return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2899  } ) );
2900  $this->primaryauthMocks[] = $mocks['primary2'];
2901 
2902  $this->preauthMocks = [ $mocks['pre'] ];
2903  $this->secondaryauthMocks = [ $mocks['secondary'] ];
2904  $this->initializeManager( true );
2905 
2906  if ( $state ) {
2907  if ( isset( $state['continueRequests'] ) ) {
2908  $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2909  }
2911  $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2912  } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2913  $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2914  } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2915  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2916  }
2917  }
2918 
2919  $expectReqs = array_map( $makeReq, $expect );
2920  if ( $action === AuthManager::ACTION_LOGIN ) {
2922  $req->action = $action;
2924  $expectReqs[] = $req;
2925  } elseif ( $action === AuthManager::ACTION_CREATE ) {
2927  $req->action = $action;
2928  $expectReqs[] = $req;
2930  $req->action = $action;
2932  $expectReqs[] = $req;
2933  }
2934  usort( $expectReqs, $cmpReqs );
2935 
2936  $actual = $this->manager->getAuthenticationRequests( $action );
2937  foreach ( $actual as $req ) {
2938  // Don't test this here.
2939  $req->required = AuthenticationRequest::REQUIRED;
2940  }
2941  usort( $actual, $cmpReqs );
2942 
2943  $this->assertEquals( $expectReqs, $actual );
2944 
2945  // Test CreationReasonAuthenticationRequest gets returned
2946  if ( $action === AuthManager::ACTION_CREATE ) {
2948  $req->action = $action;
2949  $req->required = AuthenticationRequest::REQUIRED;
2950  $expectReqs[] = $req;
2951  usort( $expectReqs, $cmpReqs );
2952 
2953  $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2954  foreach ( $actual as $req ) {
2955  // Don't test this here.
2956  $req->required = AuthenticationRequest::REQUIRED;
2957  }
2958  usort( $actual, $cmpReqs );
2959 
2960  $this->assertEquals( $expectReqs, $actual );
2961  }
2962  }
2963 
2964  public static function provideGetAuthenticationRequests() {
2965  return [
2966  [
2968  [ 'pre-login', 'primary-none-login', 'primary-create-login',
2969  'primary-link-login', 'secondary-login', 'generic' ],
2970  ],
2971  [
2973  [ 'pre-create', 'primary-none-create', 'primary-create-create',
2974  'primary-link-create', 'secondary-create', 'generic' ],
2975  ],
2976  [
2978  [ 'primary-link-link', 'generic' ],
2979  ],
2980  [
2982  [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
2983  'secondary-change' ],
2984  ],
2985  [
2987  [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
2988  'secondary-remove' ],
2989  ],
2990  [
2992  [ 'primary-link-remove' ],
2993  ],
2994  [
2996  [],
2997  ],
2998  [
3000  $reqs = [ 'continue-login', 'foo', 'bar' ],
3001  [
3002  'continueRequests' => $reqs,
3003  ],
3004  ],
3005  [
3007  [],
3008  ],
3009  [
3011  $reqs = [ 'continue-create', 'foo', 'bar' ],
3012  [
3013  'continueRequests' => $reqs,
3014  ],
3015  ],
3016  [
3018  [],
3019  ],
3020  [
3022  $reqs = [ 'continue-link', 'foo', 'bar' ],
3023  [
3024  'continueRequests' => $reqs,
3025  ],
3026  ],
3027  ];
3028  }
3029 
3031  $makeReq = function ( $key, $required ) {
3032  $req = $this->getMock( AuthenticationRequest::class );
3033  $req->expects( $this->any() )->method( 'getUniqueId' )
3034  ->will( $this->returnValue( $key ) );
3035  $req->action = AuthManager::ACTION_LOGIN;
3036  $req->key = $key;
3037  $req->required = $required;
3038  return $req;
3039  };
3040  $cmpReqs = function ( $a, $b ) {
3041  $ret = strcmp( get_class( $a ), get_class( $b ) );
3042  if ( !$ret ) {
3043  $ret = strcmp( $a->key, $b->key );
3044  }
3045  return $ret;
3046  };
3047 
3048  $good = StatusValue::newGood();
3049 
3050  $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3051  $primary1->expects( $this->any() )->method( 'getUniqueId' )
3052  ->will( $this->returnValue( 'primary1' ) );
3053  $primary1->expects( $this->any() )->method( 'accountCreationType' )
3054  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3055  $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3056  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3057  return [
3058  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3059  $makeReq( "required", AuthenticationRequest::REQUIRED ),
3060  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3061  $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3062  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3063  $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3064  ];
3065  } ) );
3066 
3067  $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3068  $primary2->expects( $this->any() )->method( 'getUniqueId' )
3069  ->will( $this->returnValue( 'primary2' ) );
3070  $primary2->expects( $this->any() )->method( 'accountCreationType' )
3071  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3072  $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3073  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3074  return [
3075  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3076  $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3077  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3078  ];
3079  } ) );
3080 
3081  $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3082  $secondary->expects( $this->any() )->method( 'getUniqueId' )
3083  ->will( $this->returnValue( 'secondary' ) );
3084  $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3085  ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3086  return [
3087  $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3088  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3089  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3090  ];
3091  } ) );
3092 
3093  $rememberReq = new RememberMeAuthenticationRequest;
3094  $rememberReq->action = AuthManager::ACTION_LOGIN;
3095 
3096  $this->primaryauthMocks = [ $primary1, $primary2 ];
3097  $this->secondaryauthMocks = [ $secondary ];
3098  $this->initializeManager( true );
3099 
3100  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3101  $expected = [
3102  $rememberReq,
3103  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3104  $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3105  $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3106  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3107  $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3108  $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3109  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3110  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3111  ];
3112  usort( $actual, $cmpReqs );
3113  usort( $expected, $cmpReqs );
3114  $this->assertEquals( $expected, $actual );
3115 
3116  $this->primaryauthMocks = [ $primary1 ];
3117  $this->secondaryauthMocks = [ $secondary ];
3118  $this->initializeManager( true );
3119 
3120  $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3121  $expected = [
3122  $rememberReq,
3123  $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3124  $makeReq( "required", AuthenticationRequest::REQUIRED ),
3125  $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3126  $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3127  $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3128  $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3129  ];
3130  usort( $actual, $cmpReqs );
3131  usort( $expected, $cmpReqs );
3132  $this->assertEquals( $expected, $actual );
3133  }
3134 
3135  public function testAllowsPropertyChange() {
3136  $mocks = [];
3137  foreach ( [ 'primary', 'secondary' ] as $key ) {
3138  $class = ucfirst( $key ) . 'AuthenticationProvider';
3139  $mocks[$key] = $this->getMockForAbstractClass(
3140  "MediaWiki\\Auth\\$class", [], "Mock$class"
3141  );
3142  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3143  ->will( $this->returnValue( $key ) );
3144  $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3145  ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3146  return $prop !== $key;
3147  } ) );
3148  }
3149 
3150  $this->primaryauthMocks = [ $mocks['primary'] ];
3151  $this->secondaryauthMocks = [ $mocks['secondary'] ];
3152  $this->initializeManager( true );
3153 
3154  $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3155  $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3156  $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3157  }
3158 
3159  public function testAutoCreateOnLogin() {
3160  $username = self::usernameForCreation();
3161 
3162  $req = $this->getMock( AuthenticationRequest::class );
3163 
3164  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3165  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3166  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3167  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3168  $mock->expects( $this->any() )->method( 'accountCreationType' )
3169  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3170  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3171  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3172  ->will( $this->returnValue( StatusValue::newGood() ) );
3173 
3174  $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3175  $mock2->expects( $this->any() )->method( 'getUniqueId' )
3176  ->will( $this->returnValue( 'secondary' ) );
3177  $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3178  $this->returnValue(
3179  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3180  )
3181  );
3182  $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3183  ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3184  $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3185  ->will( $this->returnValue( StatusValue::newGood() ) );
3186 
3187  $this->primaryauthMocks = [ $mock ];
3188  $this->secondaryauthMocks = [ $mock2 ];
3189  $this->initializeManager( true );
3190  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3191  $session = $this->request->getSession();
3192  $session->clear();
3193 
3194  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3195  'sanity check' );
3196 
3197  $callback = $this->callback( function ( $user ) use ( $username ) {
3198  return $user->getName() === $username;
3199  } );
3200 
3201  $this->hook( 'UserLoggedIn', $this->never() );
3202  $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3203  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3204  $this->unhook( 'LocalUserCreated' );
3205  $this->unhook( 'UserLoggedIn' );
3206  $this->assertSame( AuthenticationResponse::UI, $ret->status );
3207 
3208  $id = (int)\User::newFromName( $username )->getId();
3209  $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3210  $this->assertSame( 0, $session->getUser()->getId() );
3211 
3212  $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3213  $this->hook( 'LocalUserCreated', $this->never() );
3214  $ret = $this->manager->continueAuthentication( [] );
3215  $this->unhook( 'LocalUserCreated' );
3216  $this->unhook( 'UserLoggedIn' );
3217  $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3218  $this->assertSame( $username, $ret->username );
3219  $this->assertSame( $id, $session->getUser()->getId() );
3220  }
3221 
3222  public function testAutoCreateFailOnLogin() {
3223  $username = self::usernameForCreation();
3224 
3225  $mock = $this->getMockForAbstractClass(
3226  PrimaryAuthenticationProvider::class, [], "MockPrimaryAuthenticationProvider" );
3227  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3228  $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3229  ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3230  $mock->expects( $this->any() )->method( 'accountCreationType' )
3231  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3232  $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3233  $mock->expects( $this->any() )->method( 'testUserForCreation' )
3234  ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3235 
3236  $this->primaryauthMocks = [ $mock ];
3237  $this->initializeManager( true );
3238  $this->manager->setLogger( new \Psr\Log\NullLogger() );
3239  $session = $this->request->getSession();
3240  $session->clear();
3241 
3242  $this->assertSame( 0, $session->getUser()->getId(),
3243  'sanity check' );
3244  $this->assertSame( 0, \User::newFromName( $username )->getId(),
3245  'sanity check' );
3246 
3247  $this->hook( 'UserLoggedIn', $this->never() );
3248  $this->hook( 'LocalUserCreated', $this->never() );
3249  $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3250  $this->unhook( 'LocalUserCreated' );
3251  $this->unhook( 'UserLoggedIn' );
3252  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3253  $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3254 
3255  $this->assertSame( 0, \User::newFromName( $username )->getId() );
3256  $this->assertSame( 0, $session->getUser()->getId() );
3257  }
3258 
3259  public function testAuthenticationSessionData() {
3260  $this->initializeManager( true );
3261 
3262  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3263  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3264  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3265  $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3266  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3267  $this->manager->removeAuthenticationSessionData( 'foo' );
3268  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3269  $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3270  $this->manager->removeAuthenticationSessionData( 'bar' );
3271  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3272 
3273  $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3274  $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3275  $this->manager->removeAuthenticationSessionData( null );
3276  $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3277  $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3278 
3279  }
3280 
3281  public function testCanLinkAccounts() {
3282  $types = [
3286  ];
3287 
3288  foreach ( $types as $type => $can ) {
3289  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3290  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3291  $mock->expects( $this->any() )->method( 'accountCreationType' )
3292  ->will( $this->returnValue( $type ) );
3293  $this->primaryauthMocks = [ $mock ];
3294  $this->initializeManager( true );
3295  $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3296  }
3297  }
3298 
3299  public function testBeginAccountLink() {
3300  $user = \User::newFromName( 'UTSysop' );
3301  $this->initializeManager();
3302 
3303  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3304  try {
3305  $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3306  $this->fail( 'Expected exception not thrown' );
3307  } catch ( \LogicException $ex ) {
3308  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3309  }
3310  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3311 
3312  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3313  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3314  $mock->expects( $this->any() )->method( 'accountCreationType' )
3315  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3316  $this->primaryauthMocks = [ $mock ];
3317  $this->initializeManager( true );
3318 
3319  $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3320  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3321  $this->assertSame( 'noname', $ret->message->getKey() );
3322 
3323  $ret = $this->manager->beginAccountLink(
3324  \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3325  );
3326  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3327  $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3328  }
3329 
3330  public function testContinueAccountLink() {
3331  $user = \User::newFromName( 'UTSysop' );
3332  $this->initializeManager();
3333 
3334  $session = [
3335  'userid' => $user->getId(),
3336  'username' => $user->getName(),
3337  'primary' => 'X',
3338  ];
3339 
3340  try {
3341  $this->manager->continueAccountLink( [] );
3342  $this->fail( 'Expected exception not thrown' );
3343  } catch ( \LogicException $ex ) {
3344  $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3345  }
3346 
3347  $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3348  $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3349  $mock->expects( $this->any() )->method( 'accountCreationType' )
3350  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3351  $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3352  $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3353  );
3354  $this->primaryauthMocks = [ $mock ];
3355  $this->initializeManager( true );
3356 
3357  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3358  $ret = $this->manager->continueAccountLink( [] );
3359  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3360  $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3361 
3362  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3363  [ 'username' => $user->getName() . '<>' ] + $session );
3364  $ret = $this->manager->continueAccountLink( [] );
3365  $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3366  $this->assertSame( 'noname', $ret->message->getKey() );
3367  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3368 
3369  $id = $user->getId();
3370  $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3371  [ 'userid' => $id + 1 ] + $session );
3372  try {
3373  $ret = $this->manager->continueAccountLink( [] );
3374  $this->fail( 'Expected exception not thrown' );
3375  } catch ( \UnexpectedValueException $ex ) {
3376  $this->assertEquals(
3377  "User \"{$user->getName()}\" is valid, but ID $id != " . ( $id + 1 ) . '!',
3378  $ex->getMessage()
3379  );
3380  }
3381  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3382  }
3383 
3390  public function testAccountLink(
3391  StatusValue $preTest, array $primaryResponses, array $managerResponses
3392  ) {
3393  $user = \User::newFromName( 'UTSysop' );
3394 
3395  $this->initializeManager();
3396 
3397  // Set up lots of mocks...
3398  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3399  $req->primary = $primaryResponses;
3400  $mocks = [];
3401 
3402  foreach ( [ 'pre', 'primary' ] as $key ) {
3403  $class = ucfirst( $key ) . 'AuthenticationProvider';
3404  $mocks[$key] = $this->getMockForAbstractClass(
3405  "MediaWiki\\Auth\\$class", [], "Mock$class"
3406  );
3407  $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3408  ->will( $this->returnValue( $key ) );
3409 
3410  for ( $i = 2; $i <= 3; $i++ ) {
3411  $mocks[$key . $i] = $this->getMockForAbstractClass(
3412  "MediaWiki\\Auth\\$class", [], "Mock$class"
3413  );
3414  $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3415  ->will( $this->returnValue( $key . $i ) );
3416  }
3417  }
3418 
3419  $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3420  ->will( $this->returnCallback(
3421  function ( $u )
3422  use ( $user, $preTest )
3423  {
3424  $this->assertSame( $user->getId(), $u->getId() );
3425  $this->assertSame( $user->getName(), $u->getName() );
3426  return $preTest;
3427  }
3428  ) );
3429 
3430  $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3431  ->will( $this->returnValue( StatusValue::newGood() ) );
3432 
3433  $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3434  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3435  $ct = count( $req->primary );
3436  $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3437  $this->assertSame( $user->getId(), $u->getId() );
3438  $this->assertSame( $user->getName(), $u->getName() );
3439  $foundReq = false;
3440  foreach ( $reqs as $r ) {
3441  $this->assertSame( $user->getName(), $r->username );
3442  $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3443  }
3444  $this->assertTrue( $foundReq, '$reqs contains $req' );
3445  return array_shift( $req->primary );
3446  } );
3447  $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3448  ->method( 'beginPrimaryAccountLink' )
3449  ->will( $callback );
3450  $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3451  ->method( 'continuePrimaryAccountLink' )
3452  ->will( $callback );
3453 
3455  $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3456  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3457  $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3458  ->will( $this->returnValue( $abstain ) );
3459  $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3460  $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3461  ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3462  $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3463  $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3464 
3465  $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3466  $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3467  $this->logger = new \TestLogger( true, function ( $message, $level ) {
3468  return $level === LogLevel::DEBUG ? null : $message;
3469  } );
3470  $this->initializeManager( true );
3471 
3472  $constraint = \PHPUnit_Framework_Assert::logicalOr(
3473  $this->equalTo( AuthenticationResponse::PASS ),
3474  $this->equalTo( AuthenticationResponse::FAIL )
3475  );
3476  $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3477  foreach ( $providers as $p ) {
3478  $p->postCalled = false;
3479  $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3480  ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3481  $this->assertInstanceOf( 'User', $user );
3482  $this->assertSame( 'UTSysop', $user->getName() );
3483  $this->assertInstanceOf( AuthenticationResponse::class, $response );
3484  $this->assertThat( $response->status, $constraint );
3485  $p->postCalled = $response->status;
3486  } );
3487  }
3488 
3489  $first = true;
3490  $created = false;
3491  $expectLog = [];
3492  foreach ( $managerResponses as $i => $response ) {
3493  if ( $response instanceof AuthenticationResponse &&
3495  ) {
3496  $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3497  }
3498 
3499  $ex = null;
3500  try {
3501  if ( $first ) {
3502  $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3503  } else {
3504  $ret = $this->manager->continueAccountLink( [ $req ] );
3505  }
3506  if ( $response instanceof \Exception ) {
3507  $this->fail( 'Expected exception not thrown', "Response $i" );
3508  }
3509  } catch ( \Exception $ex ) {
3510  if ( !$response instanceof \Exception ) {
3511  throw $ex;
3512  }
3513  $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3514  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3515  "Response $i, exception, session state" );
3516  return;
3517  }
3518 
3519  $this->assertSame( 'http://localhost/', $req->returnToUrl );
3520 
3521  $ret->message = $this->message( $ret->message );
3522  $this->assertEquals( $response, $ret, "Response $i, response" );
3523  if ( $response->status === AuthenticationResponse::PASS ||
3525  ) {
3526  $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3527  "Response $i, session state" );
3528  foreach ( $providers as $p ) {
3529  $this->assertSame( $response->status, $p->postCalled,
3530  "Response $i, post-auth callback called" );
3531  }
3532  } else {
3533  $this->assertNotNull(
3534  $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3535  "Response $i, session state"
3536  );
3537  foreach ( $ret->neededRequests as $neededReq ) {
3538  $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
3539  "Response $i, neededRequest action" );
3540  }
3541  $this->assertEquals(
3542  $ret->neededRequests,
3543  $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3544  "Response $i, continuation check"
3545  );
3546  foreach ( $providers as $p ) {
3547  $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3548  }
3549  }
3550 
3551  $first = false;
3552  }
3553 
3554  $this->assertSame( $expectLog, $this->logger->getBuffer() );
3555  }
3556 
3557  public function provideAccountLink() {
3558  $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3559  $good = StatusValue::newGood();
3560 
3561  return [
3562  'Pre-link test fail in pre' => [
3563  StatusValue::newFatal( 'fail-from-pre' ),
3564  [],
3565  [
3566  AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3567  ]
3568  ],
3569  'Failure in primary' => [
3570  $good,
3571  $tmp = [
3572  AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3573  ],
3574  $tmp
3575  ],
3576  'All primary abstain' => [
3577  $good,
3578  [
3580  ],
3581  [
3582  AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3583  ]
3584  ],
3585  'Primary UI, then redirect, then fail' => [
3586  $good,
3587  $tmp = [
3588  AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3589  AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3590  AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3591  ],
3592  $tmp
3593  ],
3594  'Primary redirect, then abstain' => [
3595  $good,
3596  [
3598  [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3599  ),
3601  ],
3602  [
3603  $tmp,
3604  new \DomainException(
3605  'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3606  )
3607  ]
3608  ],
3609  'Primary UI, then pass' => [
3610  $good,
3611  [
3612  $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3614  ],
3615  [
3616  $tmp1,
3618  ]
3619  ],
3620  'Primary pass' => [
3621  $good,
3622  [
3624  ],
3625  [
3627  ]
3628  ],
3629  ];
3630  }
3631 }
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:522
initializeManager($regen=false)
Initialize $this->manager.
testAccountCreation(StatusValue $preTest, $primaryTest, $secondaryTest, array $primaryResponses, array $secondaryResponses, array $managerResponses)
provideAccountCreation
const MIN_PRIORITY
Minimum allowed priority.
Definition: SessionInfo.php:36
onSecuritySensitiveOperationStatus(&$status, $operation, $session, $time)
This transfers state between the login and account creation flows.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provdier, but other primary authen...
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
the array() calling protocol came about after MediaWiki 1.4rc1.
initializeConfig()
Initialize the AuthManagerConfig variable in $this->config.
static wrap($sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:79
Object holding data about a session's user.
Definition: UserInfo.php:51
$success
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
const RESTART
Indicates that third-party authentication succeeded but no user exists.
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
testAccountLink(StatusValue $preTest, array $primaryResponses, array $managerResponses)
provideAccountLink
static newFatal($message)
Factory function for fatal errors.
Definition: StatusValue.php:63
hook($hook, $expect)
Sets a mock on a hook.
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
Definition: AuthManager.php:64
static newFromUser(User $user, $verified=false)
Create an instance from an existing User object.
Definition: UserInfo.php:116
static setSessionManagerSingleton(SessionManager $manager=null)
Override the singleton for unit testing.
Definition: TestUtils.php:17
testUserExists($primary1Exists, $primary2Exists, $expect)
provideUserExists
static getLocalClusterInstance()
Get the main cluster-local cache object.
This is a value object to hold authentication response data.
$wgHooks['ArticleShow'][]
Definition: hooks.txt:110
Authentication request for the reason given for account creation.
Psr Log LoggerInterface $logger
this hook is for auditing only $response
Definition: hooks.txt:776
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same so they can t rely on Unix and must forbid reads to even standard directories like tmp lest users read each others files We cannot assume that the user has the ability to install or run any programs not written as web accessible PHP scripts Since anything that works on cheap shared hosting will work if you have shell or root access MediaWiki s design is based around catering to the lowest common denominator Although we support higher end setups as the way many things work by default is tailored toward shared hosting These defaults are unconventional from the point of view of and they certainly aren t ideal for someone who s installing MediaWiki as MediaWiki does not conform to normal Unix filesystem layout Hopefully we ll offer direct support for standard layouts in the but for now *any change to the location of files is unsupported *Moving things and leaving symlinks will *probably *not break anything
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1629
get($name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
static newRedirect(array $reqs, $redirectTarget, $redirectApiData=null)
$wgGroupPermissions
Permission keys given to users in each group.
the value to return A Title object or null for latest to be modified or replaced by the hook handler or if authentication is not possible after cache objects are set for highlighting & $link
Definition: hooks.txt:2621
unhook($hook)
Unsets a hook.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
static BagOStuff[] $instances
Map of (id => BagOStuff)
Definition: ObjectCache.php:83
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:1816
static setPasswordForUser(User $user, $password)
Set the password on a testing user.
Definition: TestUser.php:127
static getMain()
Static methods.
IContextSource $context
Definition: MediaWiki.php:32
This represents additional user data requested on the account creation form.
const FAIL
Indicates that the authentication failed.
const TYPE_LINK
Provider can link to existing accounts elsewhere.
static newFromTarget($specificTarget, $vagueTarget=null, $fromMaster=false)
Given a target and the target's type, get an existing Block object if possible.
Definition: Block.php:1057
static newUI(array $reqs, Message $msg)
static singleton()
Get the global AuthManager.
$res
Definition: database.txt:21
const ACTION_CHANGE
Change a user's credentials.
Definition: AuthManager.php:60
const SEC_FAIL
Security-sensitive should not be performed.
Definition: AuthManager.php:71
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
Definition: AuthManager.php:69
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
Definition: AuthManager.php:74
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.
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 just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned after processing after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock()-offset Set to overwrite offset parameter in $wgRequest set to ''to unsetoffset-wrap String Wrap the message in html(usually something like"&lt
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value...
Definition: StatusValue.php:42
static newGood($value=null)
Factory function for good results.
Definition: StatusValue.php:76
hideDeprecated($function)
Don't throw a warning if $function is deprecated and called later.
This serves as the entry point to the authentication system.
Definition: AuthManager.php:43
AuthenticationRequest to ensure something with a username is present.
const TYPE_NONE
Provider cannot create or link to accounts.
getLanguage()
Get the Language object.
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:1816
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
testSecuritySensitiveOperationStatus($mutableSession)
provideSecuritySensitiveOperationStatus
const ACTION_LINK
Link an existing user to a third-party account.
Definition: AuthManager.php:55
testAccountCreationLogging($isAnon, $logSubtype)
provideAccountCreationLogging
static getSelectQueryData()
Returns array of information that is needed for querying log entries.
Definition: LogEntry.php:170
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition: hooks.txt:242
testUserCanAuthenticate($primary1Can, $primary2Can, $expect)
provideUserCanAuthenticate
AuthManager Database MediaWiki\Auth\AuthManager.
String $action
Cache what action this request is.
Definition: MediaWiki.php:42
message($key, $params=[])
Ensure a value is a clean Message object.
Simple store for keeping values in an associative array for the current process.
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:981
getMockSessionProvider($canChangeUser=null, array $methods=[])
Setup SessionManager with a mock session provider.
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:776
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
const ACTION_LOGIN_CONTINUE
Continue a login process that was interrupted by the need for user input or communication with an ext...
Definition: AuthManager.php:48
const ACTION_REMOVE
Remove a user's credentials.
Definition: AuthManager.php:62
testAuthentication(StatusValue $preResponse, array $primaryResponses, array $secondaryResponses, array $managerResponses, $link=false)
provideAuthentication
$wgDisableAuthManager
Disable AuthManager.
static idFromName($name, $flags=self::READ_NORMAL)
Get database id given a user name.
Definition: User.php:764
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
Definition: AuthManager.php:53
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1020
static newFromRow($row)
Constructs new LogEntry from database result row.
Definition: LogEntry.php:201
static consume(ScopedCallback &$sc=null)
Trigger a scoped callback and destroy it.
testGetAuthenticationRequests($action, $expect, $state=[])
provideGetAuthenticationRequests
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:50
wfMemcKey()
Make a cache key for the local wiki.
const DB_MASTER
Definition: Defines.php:47
TestingAccessWrapper $managerPriv
</td >< td > &</td >< td > t want your writing to be edited mercilessly and redistributed at will
const SEC_OK
Security-sensitive operations are ok.
Definition: AuthManager.php:67
static newFromObject($object)
Return the same object, without access restrictions.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:45
this hook is for auditing only etc instead of letting the login form give the generic error message that the account does not exist For when the account has been renamed or deleted or an array to pass a message key and parameters create2 Corresponds to logging log_action database field and which is displayed in the UI similar to $comment this hook should only be used to add variables that depend on the current page request
Definition: hooks.txt:1994
static factory($code)
Get a cached or new language object for a given language code.
Definition: Language.php:179
setMwGlobals($pairs, $value=null)
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
Definition: AuthManager.php:58
testAllowsAuthenticationDataChange($primaryReturn, $secondaryReturn, $expect)
provideAllowsAuthenticationDataChange
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2376
stashMwGlobals($globalKeys)
Stashes the global, will be restored in tearDown()
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
Value object returned by SessionProvider.
Definition: SessionInfo.php:34
const UI
Indicates that the authentication needs further user input of some sort.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:310