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