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