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