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