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