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