Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / user / UserTest.php
blob63fd6c914cd46c9fb3111ac7eafe70759d7b255b
1 <?php
3 use MediaWiki\Block\CompositeBlock;
4 use MediaWiki\Block\DatabaseBlock;
5 use MediaWiki\Block\SystemBlock;
6 use MediaWiki\Context\RequestContext;
7 use MediaWiki\MainConfigNames;
8 use MediaWiki\Permissions\RateLimiter;
9 use MediaWiki\Permissions\RateLimitSubject;
10 use MediaWiki\Request\FauxRequest;
11 use MediaWiki\Request\WebRequest;
12 use MediaWiki\Tests\Unit\DummyServicesTrait;
13 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
14 use MediaWiki\Title\Title;
15 use MediaWiki\User\User;
16 use MediaWiki\User\UserIdentityValue;
17 use MediaWiki\Utils\MWTimestamp;
18 use Wikimedia\Assert\PreconditionException;
19 use Wikimedia\Rdbms\IDBAccessObject;
20 use Wikimedia\TestingAccessWrapper;
22 /**
23 * @coversDefaultClass \MediaWiki\User\User
24 * @group Database
26 class UserTest extends MediaWikiIntegrationTestCase {
27 use DummyServicesTrait;
28 use TempUserTestTrait;
30 /** Constant for self::testIsBlockedFrom */
31 private const USER_TALK_PAGE = '<user talk page>';
33 protected User $user;
35 protected function setUp(): void {
36 parent::setUp();
38 $this->overrideConfigValues( [
39 MainConfigNames::GroupPermissions => [],
40 MainConfigNames::RevokePermissions => [],
41 MainConfigNames::UseRCPatrol => true,
42 MainConfigNames::WatchlistExpiry => true,
43 MainConfigNames::AutoConfirmAge => 0,
44 MainConfigNames::AutoConfirmCount => 0,
45 ] );
47 $this->setUpPermissionGlobals();
49 $this->user = $this->getTestUser( 'unittesters' )->getUser();
52 private function setUpPermissionGlobals() {
53 $this->setGroupPermissions( [
54 // Data for regular $wgGroupPermissions test
55 'unittesters' => [
56 'test' => true,
57 'runtest' => true,
58 'writetest' => false,
59 'nukeworld' => false,
60 'autoconfirmed' => false,
62 'testwriters' => [
63 'test' => true,
64 'writetest' => true,
65 'modifytest' => true,
66 'autoconfirmed' => true,
68 // For the options and watchlist tests
69 '*' => [
70 'editmyoptions' => true,
71 'editmywatchlist' => true,
72 'viewmywatchlist' => true,
74 // For patrol tests
75 'patroller' => [
76 'patrol' => true,
78 // For account creation when blocked test
79 'accountcreator' => [
80 'createaccount' => true,
81 'ipblock-exempt' => true
83 // For bot and ratelimit tests
84 'bot' => [
85 'bot' => true,
86 'noratelimit' => true,
88 ] );
90 $this->overrideConfigValue(
91 MainConfigNames::RevokePermissions,
92 // Data for regular $wgRevokePermissions test
93 [ 'formertesters' => [ 'runtest' => true ] ]
97 private function setSessionUser( User $user, WebRequest $request ) {
98 RequestContext::getMain()->setUser( $user );
99 RequestContext::getMain()->setRequest( $request );
100 TestingAccessWrapper::newFromObject( $user )->mRequest = $request;
101 $request->getSession()->setUser( $user );
105 * @covers \MediaWiki\User\User::isAllowedAny
106 * @covers \MediaWiki\User\User::isAllowedAll
107 * @covers \MediaWiki\User\User::isAllowed
108 * @covers \MediaWiki\User\User::isNewbie
110 public function testIsAllowed() {
111 $this->assertFalse(
112 $this->user->isAllowed( 'writetest' ),
113 'Basic isAllowed works with a group not granted a right'
115 $this->assertTrue(
116 $this->user->isAllowedAny( 'test', 'writetest' ),
117 'A user with only one of the rights can pass isAllowedAll'
119 $this->assertTrue(
120 $this->user->isAllowedAll( 'test', 'runtest' ),
121 'A user with multiple rights can pass isAllowedAll'
123 $this->assertFalse(
124 $this->user->isAllowedAll( 'test', 'runtest', 'writetest' ),
125 'A user needs all rights specified to pass isAllowedAll'
127 $this->assertTrue(
128 $this->user->isNewbie(),
129 'Unit testers are not autoconfirmed yet'
132 $user = $this->getTestUser( 'testwriters' )->getUser();
133 $this->assertTrue(
134 $user->isAllowed( 'test' ),
135 'Basic isAllowed works with a group granted a right'
137 $this->assertTrue(
138 $user->isAllowed( 'writetest' ),
139 'Testwriters pass isAllowed with `writetest`'
141 $this->assertFalse(
142 $user->isNewbie(),
143 'Test writers are autoconfirmed'
148 * @covers \MediaWiki\User\User::useRCPatrol
149 * @covers \MediaWiki\User\User::useNPPatrol
150 * @covers \MediaWiki\User\User::useFilePatrol
152 public function testPatrolling() {
153 $user = $this->getTestUser( 'patroller' )->getUser();
155 $this->assertTrue( $user->useRCPatrol() );
156 $this->assertTrue( $user->useNPPatrol() );
157 $this->assertTrue( $user->useFilePatrol() );
159 $this->assertFalse( $this->user->useRCPatrol() );
160 $this->assertFalse( $this->user->useNPPatrol() );
161 $this->assertFalse( $this->user->useFilePatrol() );
165 * @covers \MediaWiki\User\User::isBot
167 public function testBot() {
168 $user = $this->getTestUser( 'bot' )->getUser();
170 $userGroupManager = $this->getServiceContainer()->getUserGroupManager();
171 $this->assertSame( [ 'bot' ], $userGroupManager->getUserGroups( $user ) );
172 $this->assertArrayHasKey( 'bot', $userGroupManager->getUserGroupMemberships( $user ) );
173 $this->assertTrue( $user->isBot() );
175 $this->assertArrayNotHasKey( 'bot', $userGroupManager->getUserGroupMemberships( $this->user ) );
176 $this->assertFalse( $this->user->isBot() );
180 * Test User::editCount
181 * @group medium
182 * @covers \MediaWiki\User\User::getEditCount
184 public function testGetEditCount() {
185 $user = $this->getMutableTestUser()->getUser();
187 // let the user have a few (3) edits
188 $title = Title::makeTitle( NS_HELP, 'UserTest_EditCount' );
189 for ( $i = 0; $i < 3; $i++ ) {
190 $this->editPage(
191 $title,
192 (string)$i,
193 'test',
194 NS_MAIN,
195 $user
199 $this->assertSame(
201 $user->getEditCount(),
202 'After three edits, the user edit count should be 3'
205 // increase the edit count
206 $this->getServiceContainer()->getUserEditTracker()->incrementUserEditCount( $user );
207 $user->clearInstanceCache();
209 $this->assertSame(
211 $user->getEditCount(),
212 'After increasing the edit count manually, the user edit count should be 4'
217 * Test User::editCount
218 * @group medium
219 * @covers \MediaWiki\User\User::getEditCount
221 public function testGetEditCountForAnons() {
222 $user = User::newFromName( 'Anonymous' );
224 $this->assertNull(
225 $user->getEditCount(),
226 'Edit count starts null for anonymous users.'
229 $this->assertNull(
230 $this->getServiceContainer()->getUserEditTracker()->incrementUserEditCount( $user ),
231 'Edit count cannot be increased for anonymous users'
234 $this->assertNull(
235 $user->getEditCount(),
236 'Edit count remains null for anonymous users despite calls to increase it.'
241 * @covers \MediaWiki\User\User::getRightDescription
243 public function testGetRightDescription() {
244 $key = 'deletechangetags';
245 $parsedDescription = User::getRightDescription( $key );
246 $this->assertMatchesRegularExpression( '/[|]/', $parsedDescription );
250 * @covers \MediaWiki\User\User::getRightDescriptionHtml
252 public function testGetParsedRightDescription() {
253 $key = 'deletechangetags';
254 $parsedDescription = User::getRightDescriptionHtml( $key );
255 $this->assertMatchesRegularExpression( '/<.*>/', $parsedDescription );
259 * Test password validity checks. There are 3 checks in core:
260 * - ensure the password meets the minimal length
261 * - ensure the password is not the same as the username
262 * - ensure the username/password combo isn't forbidden
263 * @covers \MediaWiki\User\User::checkPasswordValidity()
264 * @covers \MediaWiki\User\User::isValidPassword()
266 public function testCheckPasswordValidity() {
267 $this->overrideConfigValue(
268 MainConfigNames::PasswordPolicy,
270 'policies' => [
271 'sysop' => [
272 'MinimalPasswordLength' => 8,
273 'MinimumPasswordLengthToLogin' => 1,
274 'PasswordCannotBeSubstringInUsername' => 1,
276 'default' => [
277 'MinimalPasswordLength' => 6,
278 'PasswordCannotBeSubstringInUsername' => true,
279 'PasswordCannotMatchDefaults' => true,
280 'MaximalPasswordLength' => 40,
283 'checks' => [
284 'MinimalPasswordLength' => 'MediaWiki\Password\PasswordPolicyChecks::checkMinimalPasswordLength',
285 'MinimumPasswordLengthToLogin' => 'MediaWiki\Password\PasswordPolicyChecks::checkMinimumPasswordLengthToLogin',
286 'PasswordCannotBeSubstringInUsername' =>
287 'MediaWiki\Password\PasswordPolicyChecks::checkPasswordCannotBeSubstringInUsername',
288 'PasswordCannotMatchDefaults' => 'MediaWiki\Password\PasswordPolicyChecks::checkPasswordCannotMatchDefaults',
289 'MaximalPasswordLength' => 'MediaWiki\Password\PasswordPolicyChecks::checkMaximalPasswordLength',
294 $this->assertTrue( $this->user->isValidPassword( 'Password1234' ) );
296 // Minimum length
297 $this->assertFalse( $this->user->isValidPassword( 'a' ) );
298 $status = $this->user->checkPasswordValidity( 'a' );
299 $this->assertStatusWarning( 'passwordtooshort', $status );
301 // Maximum length
302 $longPass = str_repeat( 'a', 41 );
303 $this->assertFalse( $this->user->isValidPassword( $longPass ) );
304 $status = $this->user->checkPasswordValidity( $longPass );
305 $this->assertStatusError( 'passwordtoolong', $status );
307 // Matches username
308 $status = $this->user->checkPasswordValidity( $this->user->getName() );
309 $this->assertStatusWarning( 'password-substring-username-match', $status );
311 $this->setTemporaryHook( 'isValidPassword', static function ( $password, &$result, $user ) {
312 $result = 'isValidPassword returned false';
313 return false;
314 } );
315 $status = $this->user->checkPasswordValidity( 'Password1234' );
316 $this->assertStatusWarning( 'isValidPassword returned false', $status );
318 $this->removeTemporaryHook( 'isValidPassword' );
320 $this->setTemporaryHook( 'isValidPassword', static function ( $password, &$result, $user ) {
321 $result = true;
322 return true;
323 } );
324 $status = $this->user->checkPasswordValidity( 'Password1234' );
325 $this->assertStatusGood( $status );
327 $this->removeTemporaryHook( 'isValidPassword' );
329 $this->setTemporaryHook( 'isValidPassword', static function ( $password, &$result, $user ) {
330 $result = 'isValidPassword returned true';
331 return true;
332 } );
333 $status = $this->user->checkPasswordValidity( 'Password1234' );
334 $this->assertStatusWarning( 'isValidPassword returned true', $status );
336 $this->removeTemporaryHook( 'isValidPassword' );
338 // On the forbidden list
339 $user = User::newFromName( 'Useruser' );
340 $status = $user->checkPasswordValidity( 'Passpass' );
341 $this->assertStatusWarning( 'password-login-forbidden', $status );
345 * @covers \MediaWiki\User\User::equals
347 public function testEquals() {
348 $first = $this->getMutableTestUser()->getUser();
349 $second = User::newFromName( $first->getName() );
351 $this->assertTrue( $first->equals( $first ) );
352 $this->assertTrue( $first->equals( $second ) );
353 $this->assertTrue( $second->equals( $first ) );
355 $third = $this->getMutableTestUser()->getUser();
356 $fourth = $this->getMutableTestUser()->getUser();
358 $this->assertFalse( $third->equals( $fourth ) );
359 $this->assertFalse( $fourth->equals( $third ) );
361 // Test users loaded from db with id
362 $user = $this->getMutableTestUser()->getUser();
363 $fifth = User::newFromId( $user->getId() );
364 $sixth = User::newFromName( $user->getName() );
365 $this->assertTrue( $fifth->equals( $sixth ) );
369 * @covers \MediaWiki\User\User::getId
370 * @covers \MediaWiki\User\User::setId
372 public function testUserId() {
373 $this->assertGreaterThan( 0, $this->user->getId() );
375 $user = User::newFromName( 'UserWithNoId' );
376 $this->assertSame( 0, $user->getId() );
378 $user->setId( 7 );
379 $this->assertSame(
381 $user->getId(),
382 'Manually setting a user id via ::setId is reflected in ::getId'
385 $user = new User;
386 $user->setName( '1.2.3.4' );
387 $this->assertSame(
389 $user->getId(),
390 'IPs have an id of 0'
395 * @covers \MediaWiki\User\User::isRegistered
396 * @covers \MediaWiki\User\User::isAnon
397 * @covers \MediaWiki\User\User::logOut
399 public function testIsRegistered() {
400 $user = $this->getMutableTestUser()->getUser();
401 $this->assertTrue( $user->isRegistered() );
402 $this->assertFalse( $user->isAnon() );
404 $this->setTemporaryHook( 'UserLogout', static function ( &$user ) {
405 return false;
406 } );
407 $user->logout();
408 $this->assertTrue( $user->isRegistered() );
410 $this->removeTemporaryHook( 'UserLogout' );
411 $user->logout();
412 $this->assertFalse( $user->isRegistered() );
414 // Non-existent users are perceived as anonymous
415 $user = User::newFromName( 'UTNonexistent' );
416 $this->assertFalse( $user->isRegistered() );
417 $this->assertTrue( $user->isAnon() );
419 $user = new User;
420 $this->assertFalse( $user->isRegistered() );
421 $this->assertTrue( $user->isAnon() );
425 * @covers \MediaWiki\User\User::setRealName
426 * @covers \MediaWiki\User\User::getRealName
428 public function testRealName() {
429 $user = $this->getMutableTestUser()->getUser();
430 $realName = 'John Doe';
432 $user->setRealName( $realName );
433 $this->assertSame(
434 $realName,
435 $user->getRealName(),
436 'Real name retrieved from cache'
439 $id = $user->getId();
440 $user->saveSettings();
442 $otherUser = User::newFromId( $id );
443 $this->assertSame(
444 $realName,
445 $otherUser->getRealName(),
446 'Real name retrieved from database'
451 * @covers \MediaWiki\User\User::checkAndSetTouched
452 * @covers \MediaWiki\User\User::getDBTouched()
454 public function testCheckAndSetTouched() {
455 $user = $this->getMutableTestUser()->getUser();
456 $user = TestingAccessWrapper::newFromObject( $user );
457 $this->assertTrue( $user->isRegistered() );
459 $touched = $user->getDBTouched();
460 $this->assertTrue(
461 $user->checkAndSetTouched(), "checkAndSetTouched() succedeed" );
462 $this->assertGreaterThan(
463 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched()" );
465 $touched = $user->getDBTouched();
466 $this->assertTrue(
467 $user->checkAndSetTouched(), "checkAndSetTouched() succedeed #2" );
468 $this->assertGreaterThan(
469 $touched, $user->getDBTouched(), "user_touched increased with casOnTouched() #2" );
473 * @covers \MediaWiki\User\User::validateCache
474 * @covers \MediaWiki\User\User::getTouched
476 public function testValidateCache() {
477 $user = $this->getTestUser()->getUser();
479 $initialTouchMW = $user->getTouched();
480 $initialTouchUnix = ( new MWTimestamp( $initialTouchMW ) )->getTimestamp();
482 $earlierUnix = $initialTouchUnix - 1000;
483 $earlierMW = ( new MWTimestamp( $earlierUnix ) )->getTimestamp( TS_MW );
484 $this->assertFalse(
485 $user->validateCache( $earlierMW ),
486 'Caches from before the value of getTouched() are not valid'
489 $laterUnix = $initialTouchUnix + 1000;
490 $laterMW = ( new MWTimestamp( $laterUnix ) )->getTimestamp( TS_MW );
491 $this->assertTrue(
492 $user->validateCache( $laterMW ),
493 'Caches from after the value of getTouched() are valid'
498 * @covers \MediaWiki\User\User::findUsersByGroup
500 public function testFindUsersByGroup() {
501 $users = User::findUsersByGroup( [] );
502 $this->assertSame( 0, iterator_count( $users ) );
504 $users = User::findUsersByGroup( 'foo', 1, 1 );
505 $this->assertSame( 0, iterator_count( $users ) );
507 $user = $this->getMutableTestUser( [ 'foo' ] )->getUser();
508 $users = User::findUsersByGroup( 'foo' );
509 $this->assertSame( 1, iterator_count( $users ) );
510 $users->rewind();
511 $this->assertTrue( $user->equals( $users->current() ) );
513 // arguments have OR relationship
514 $user2 = $this->getMutableTestUser( [ 'bar' ] )->getUser();
515 $users = User::findUsersByGroup( [ 'foo', 'bar' ] );
516 $this->assertSame( 2, iterator_count( $users ) );
517 $users->rewind();
518 $this->assertTrue( $user->equals( $users->current() ) );
519 $users->next();
520 $this->assertTrue( $user2->equals( $users->current() ) );
522 // users are not duplicated
523 $user = $this->getMutableTestUser( [ 'baz', 'boom' ] )->getUser();
524 $users = User::findUsersByGroup( [ 'baz', 'boom' ] );
525 $this->assertSame( 1, iterator_count( $users ) );
526 $users->rewind();
527 $this->assertTrue( $user->equals( $users->current() ) );
531 * @covers \MediaWiki\User\User::getBlock
533 public function testSoftBlockRanges() {
534 $this->overrideConfigValue( MainConfigNames::SoftBlockRanges, [ '10.0.0.0/8' ] );
536 // IP isn't in $wgSoftBlockRanges
537 $user = new User();
538 $request = new FauxRequest();
539 $request->setIP( '192.168.0.1' );
540 $this->setSessionUser( $user, $request );
541 $this->assertNull( $user->getBlock() );
543 // IP is in $wgSoftBlockRanges
544 $user = new User();
545 $request = new FauxRequest();
546 $request->setIP( '10.20.30.40' );
547 $this->setSessionUser( $user, $request );
548 $block = $user->getBlock();
549 $this->assertInstanceOf( SystemBlock::class, $block );
550 $this->assertSame( 'wgSoftBlockRanges', $block->getSystemBlockType() );
552 // IP is in $wgSoftBlockRanges and user is temporary
553 $this->enableAutoCreateTempUser();
554 $user = ( new TestUser( '~1' ) )->getUser();
555 $request = new FauxRequest();
556 $request->setIP( '10.20.30.40' );
557 $this->setSessionUser( $user, $request );
558 $block = $user->getBlock();
559 $this->assertTrue( $user->isTemp() );
560 $this->assertInstanceOf( SystemBlock::class, $block );
561 $this->assertSame( 'wgSoftBlockRanges', $block->getSystemBlockType() );
563 // Make sure the block is really soft
564 $request = new FauxRequest();
565 $request->setIP( '10.20.30.40' );
566 $this->setSessionUser( $this->user, $request );
567 $this->assertFalse( $this->user->isAnon() );
568 $this->assertNull( $this->user->getBlock() );
571 public static function provideIsPingLimitable() {
572 yield 'Not ip excluded' => [ [], null, true ];
573 yield 'Ip excluded' => [ [ '1.2.3.4' ], null, false ];
574 yield 'Ip subnet excluded' => [ [ '1.2.3.0/8' ], null, false ];
575 yield 'noratelimit right' => [ [], 'noratelimit', false ];
579 * @dataProvider provideIsPingLimitable
580 * @covers \MediaWiki\User\User::isPingLimitable
581 * @param array $rateLimitExcludeIps
582 * @param string|null $rightOverride
583 * @param bool $expected
585 public function testIsPingLimitable(
586 array $rateLimitExcludeIps,
587 ?string $rightOverride,
588 bool $expected
590 $request = new FauxRequest();
591 $request->setIP( '1.2.3.4' );
592 $user = User::newFromSession( $request );
593 // We are trying to test for current user behaviour
594 // since we are interested in request IP
595 RequestContext::getMain()->setUser( $user );
597 $this->overrideConfigValue( MainConfigNames::RateLimitsExcludedIPs, $rateLimitExcludeIps );
598 if ( $rightOverride ) {
599 $this->overrideUserPermissions( $user, $rightOverride );
601 $this->assertSame( $expected, $user->isPingLimitable() );
604 public static function provideExperienceLevel() {
605 return [
606 [ 2, 2, 'newcomer' ],
607 [ 12, 3, 'newcomer' ],
608 [ 8, 5, 'newcomer' ],
609 [ 15, 10, 'learner' ],
610 [ 450, 20, 'learner' ],
611 [ 460, 33, 'learner' ],
612 [ 525, 28, 'learner' ],
613 [ 538, 33, 'experienced' ],
614 [ 9, null, 'newcomer' ],
615 [ 10, null, 'learner' ],
616 [ 501, null, 'experienced' ],
621 * @covers \MediaWiki\User\User::getExperienceLevel
622 * @dataProvider provideExperienceLevel
624 public function testExperienceLevel( $editCount, $memberSince, $expLevel ) {
625 $this->overrideConfigValues( [
626 MainConfigNames::LearnerEdits => 10,
627 MainConfigNames::LearnerMemberSince => 4,
628 MainConfigNames::ExperiencedUserEdits => 500,
629 MainConfigNames::ExperiencedUserMemberSince => 30,
630 ] );
632 $db = $this->getDb();
633 $row = User::newQueryBuilder( $db )
634 ->where( [ 'user_id' => $this->user->getId() ] )
635 ->caller( __METHOD__ )
636 ->fetchRow();
637 $row->user_editcount = $editCount;
638 if ( $memberSince !== null ) {
639 $row->user_registration = $db->timestamp( time() - $memberSince * 86400 );
640 } else {
641 $row->user_registration = null;
643 $user = User::newFromRow( $row );
645 $this->assertSame( $expLevel, $user->getExperienceLevel() );
649 * @covers \MediaWiki\User\User::getExperienceLevel
651 public function testExperienceLevelAnon() {
652 $user = User::newFromName( '10.11.12.13', false );
654 $this->assertFalse( $user->getExperienceLevel() );
657 public static function provideIsLocallyBlockedProxy() {
658 return [
659 [ '1.2.3.4', '1.2.3.4' ],
660 [ '1.2.3.4', '1.2.3.0/16' ],
665 * @covers \MediaWiki\User\User::newFromId
667 public function testNewFromId() {
668 $userId = $this->user->getId();
669 $this->assertGreaterThan(
671 $userId,
672 'user has a working id'
675 $otherUser = User::newFromId( $userId );
676 $this->assertTrue(
677 $this->user->equals( $otherUser ),
678 'User created by id should match user with that id'
683 * @covers \MediaWiki\User\User::newFromActorId
685 public function testActorId() {
686 $this->filterDeprecated( '/Passing a parameter to getActorId\(\) is deprecated/', '1.36' );
688 // Newly-created user has an actor ID
689 $user = User::createNew( 'UserTestActorId1' );
690 $id = $user->getId();
691 $this->assertGreaterThan( 0, $user->getActorId(), 'User::createNew sets an actor ID' );
693 $user = User::newFromName( 'UserTestActorId2' );
694 $user->addToDatabase();
695 $this->assertGreaterThan( 0, $user->getActorId(), 'User::addToDatabase sets an actor ID' );
697 $user = User::newFromName( 'UserTestActorId1' );
698 $this->assertGreaterThan( 0, $user->getActorId(),
699 'Actor ID can be retrieved for user loaded by name' );
701 $user = User::newFromId( $id );
702 $this->assertGreaterThan( 0, $user->getActorId(),
703 'Actor ID can be retrieved for user loaded by ID' );
705 $user2 = User::newFromActorId( $user->getActorId() );
706 $this->assertSame( $user->getId(), $user2->getId(),
707 'User::newFromActorId works for an existing user' );
709 $row = User::newQueryBuilder( $this->getDb() )
710 ->where( [ 'user_id' => $id ] )
711 ->caller( __METHOD__ )
712 ->fetchRow();
713 $user = User::newFromRow( $row );
714 $this->assertGreaterThan( 0, $user->getActorId(),
715 'Actor ID can be retrieved for user loaded with User::selectFields()' );
717 $user = User::newFromId( $id );
718 $user->setName( 'UserTestActorId4-renamed' );
719 $user->saveSettings();
720 $this->assertSame(
721 $user->getName(),
722 $this->getDb()->newSelectQueryBuilder()
723 ->select( 'actor_name' )
724 ->from( 'actor' )
725 ->where( [ 'actor_id' => $user->getActorId() ] )
726 ->caller( __METHOD__ )->fetchField(),
727 'User::saveSettings updates actor table for name change'
730 $ip = '192.168.12.34';
731 $this->getDb()->newDeleteQueryBuilder()
732 ->deleteFrom( 'actor' )
733 ->where( [ 'actor_name' => $ip ] )
734 ->caller( __METHOD__ )
735 ->execute();
737 // Next tests require disabling temp user feature.
738 $this->disableAutoCreateTempUser();
739 $user = User::newFromName( $ip, false );
740 $this->assertSame( 0, $user->getActorId(), 'Anonymous user has no actor ID by default' );
741 $this->filterDeprecated( '/Passing parameter of type IDatabase/' );
742 $this->assertGreaterThan( 0, $user->getActorId( $this->getDb() ),
743 'Actor ID can be created for an anonymous user' );
745 $user = User::newFromName( $ip, false );
746 $this->assertGreaterThan( 0, $user->getActorId(),
747 'Actor ID can be loaded for an anonymous user' );
748 $user2 = User::newFromActorId( $user->getActorId() );
749 $this->assertSame( $user->getName(), $user2->getName(),
750 'User::newFromActorId works for an anonymous user' );
754 * @covers \MediaWiki\User\User::getActorId
756 public function testForeignGetActorId() {
757 $this->filterDeprecated( '/Passing a parameter to getActorId\(\) is deprecated/', '1.36' );
759 $user = User::newFromName( 'UserTestActorId1' );
760 $this->expectException( PreconditionException::class );
761 $user->getActorId( 'Foreign Wiki' );
765 * @covers \MediaWiki\User\User::getWikiId
767 public function testGetWiki() {
768 $user = User::newFromName( 'UserTestActorId1' );
769 $this->assertSame( User::LOCAL, $user->getWikiId() );
773 * @covers \MediaWiki\User\User::assertWiki
775 public function testAssertWiki() {
776 $user = User::newFromName( 'UserTestActorId1' );
778 $user->assertWiki( User::LOCAL );
779 $this->assertTrue( true, 'User is for local wiki' );
781 $this->expectException( PreconditionException::class );
782 $user->assertWiki( 'Foreign Wiki' );
786 * @covers \MediaWiki\User\User::newFromAnyId
788 public function testNewFromAnyId() {
789 $this->disableAutoCreateTempUser();
790 // Registered user
791 $user = $this->user;
792 for ( $i = 1; $i <= 7; $i++ ) {
793 $test = User::newFromAnyId(
794 ( $i & 1 ) ? $user->getId() : null,
795 ( $i & 2 ) ? $user->getName() : null,
796 ( $i & 4 ) ? $user->getActorId() : null
798 $this->assertSame( $user->getId(), $test->getId() );
799 $this->assertSame( $user->getName(), $test->getName() );
800 $this->assertSame( $user->getActorId(), $test->getActorId() );
803 // Anon user. Can't load by only user ID when that's 0.
804 $user = User::newFromName( '192.168.12.34', false );
805 // Make sure an actor ID exists
806 $this->getServiceContainer()->getActorNormalization()->acquireActorId( $user, $this->getDb() );
808 $test = User::newFromAnyId( null, '192.168.12.34', null );
809 $this->assertSame( $user->getId(), $test->getId() );
810 $this->assertSame( $user->getName(), $test->getName() );
811 $this->assertSame( $user->getActorId(), $test->getActorId() );
812 $test = User::newFromAnyId( null, null, $user->getActorId() );
813 $this->assertSame( $user->getId(), $test->getId() );
814 $this->assertSame( $user->getName(), $test->getName() );
815 $this->assertSame( $user->getActorId(), $test->getActorId() );
817 // Bogus data should still "work" as long as nothing triggers a ->load(),
818 // and accessing the specified data shouldn't do that.
819 $test = User::newFromAnyId( 123456, 'Bogus', 654321 );
820 $this->assertSame( 123456, $test->getId() );
821 $this->assertSame( 'Bogus', $test->getName() );
822 $this->assertSame( 654321, $test->getActorId() );
824 // Loading remote user by name from remote wiki should succeed
825 $test = User::newFromAnyId( null, 'Bogus', null, 'foo' );
826 $this->assertSame( 0, $test->getId() );
827 $this->assertSame( 'Bogus', $test->getName() );
828 $this->assertSame( 0, $test->getActorId() );
829 $test = User::newFromAnyId( 123456, 'Bogus', 654321, 'foo' );
830 $this->assertSame( 0, $test->getId() );
831 $this->assertSame( 0, $test->getActorId() );
833 // Exceptional cases
834 try {
835 User::newFromAnyId( null, null, null );
836 $this->fail( 'Expected exception not thrown' );
837 } catch ( InvalidArgumentException $ex ) {
839 try {
840 User::newFromAnyId( 0, null, 0 );
841 $this->fail( 'Expected exception not thrown' );
842 } catch ( InvalidArgumentException $ex ) {
845 // Loading remote user by id from remote wiki should fail
846 try {
847 User::newFromAnyId( 123456, null, 654321, 'foo' );
848 $this->fail( 'Expected exception not thrown' );
849 } catch ( InvalidArgumentException $ex ) {
854 * @covers \MediaWiki\User\User::newFromIdentity
856 public function testNewFromIdentity() {
857 // Registered user
858 $user = $this->user;
860 $this->assertSame( $user, User::newFromIdentity( $user ) );
862 // ID only
863 $identity = new UserIdentityValue( $user->getId(), '' );
864 $result = User::newFromIdentity( $identity );
865 $this->assertInstanceOf( User::class, $result );
866 $this->assertSame( $user->getId(), $result->getId(), 'ID' );
867 $this->assertSame( $user->getName(), $result->getName(), 'Name' );
868 $this->assertSame( $user->getActorId(), $result->getActorId(), 'Actor' );
870 // Name only
871 $identity = new UserIdentityValue( 0, $user->getName() );
872 $result = User::newFromIdentity( $identity );
873 $this->assertInstanceOf( User::class, $result );
874 $this->assertSame( $user->getId(), $result->getId(), 'ID' );
875 $this->assertSame( $user->getName(), $result->getName(), 'Name' );
876 $this->assertSame( $user->getActorId(), $result->getActorId(), 'Actor' );
880 * @covers \MediaWiki\User\User::newFromConfirmationCode
882 public function testNewFromConfirmationCode() {
883 $user = User::newFromConfirmationCode( 'NotARealConfirmationCode' );
884 $this->assertNull(
885 $user,
886 'Invalid confirmation codes result in null users when reading from replicas'
889 $user = User::newFromConfirmationCode( 'OtherFakeCode', IDBAccessObject::READ_LATEST );
890 $this->assertNull(
891 $user,
892 'Invalid confirmation codes result in null users when reading from master'
897 * @covers \MediaWiki\User\User::newFromName
898 * @covers \MediaWiki\User\User::getName
899 * @covers \MediaWiki\User\User::getUserPage
900 * @covers \MediaWiki\User\User::getTalkPage
901 * @covers \MediaWiki\User\User::getTitleKey
902 * @covers \MediaWiki\User\User::whoIs
903 * @dataProvider provideNewFromName
905 public function testNewFromName( $name, $titleKey ) {
906 $user = User::newFromName( $name );
907 $this->assertSame( $user->getName(), $name );
908 $this->assertEquals( $user->getUserPage(), Title::makeTitle( NS_USER, $name ) );
909 $this->assertEquals( $user->getTalkPage(), Title::makeTitle( NS_USER_TALK, $name ) );
910 $this->assertSame( $user->getTitleKey(), $titleKey );
912 $status = $user->addToDatabase();
913 $this->assertStatusOK( $status, 'User can be added to the database' );
914 $this->assertSame( $name, User::whoIs( $user->getId() ) );
917 public static function provideNewFromName() {
918 return [
919 [ 'Example1', 'Example1' ],
920 [ 'MediaWiki easter egg', 'MediaWiki_easter_egg' ],
921 [ 'See T22281 for more', 'See_T22281_for_more' ],
922 [ 'DannyS712', 'DannyS712' ],
927 * @covers \MediaWiki\User\User::newFromName
929 public function testNewFromName_extra() {
930 $user = User::newFromName( '1.2.3.4' );
931 $this->assertFalse( $user, 'IP addresses are not valid user names' );
933 $user = User::newFromName( 'DannyS712', true );
934 $otherUser = User::newFromName( 'DannyS712', 'valid' );
935 $this->assertTrue(
936 $user->equals( $otherUser ),
937 'true maps to valid for backwards compatibility'
942 * @covers \MediaWiki\User\User::newFromSession
943 * @covers \MediaWiki\User\User::getRequest
945 public function testSessionAndRequest() {
946 $req1 = new WebRequest;
947 $this->setRequest( $req1 );
948 $user = User::newFromSession();
949 $request = $user->getRequest();
951 $this->assertSame(
952 $req1,
953 $request,
954 'Creating a user without a request defaults to $wgRequest'
956 $req2 = new WebRequest;
957 $this->assertNotSame(
958 $req1,
959 $req2,
960 'passing a request that does not match $wgRequest'
962 $user = User::newFromSession( $req2 );
963 $request = $user->getRequest();
964 $this->assertSame(
965 $req2,
966 $request,
967 'Creating a user by passing a WebRequest successfully sets the request, ' .
968 'instead of using $wgRequest'
973 * @covers \MediaWiki\User\User::newFromRow
974 * @covers \MediaWiki\User\User::loadFromRow
976 public function testNewFromRow() {
977 // TODO: Create real tests here for loadFromRow
978 $row = (object)[];
979 $user = User::newFromRow( $row );
980 $this->assertInstanceOf( User::class, $user, 'newFromRow returns a user object' );
984 * @covers \MediaWiki\User\User::newFromRow
985 * @covers \MediaWiki\User\User::loadFromRow
987 public function testNewFromRow_bad() {
988 $this->expectException( InvalidArgumentException::class );
989 $this->expectExceptionMessage( '$row must be an object' );
990 User::newFromRow( [] );
994 * @covers \MediaWiki\User\User::getBlock
995 * @covers \MediaWiki\User\User::isHidden
997 public function testBlockInstanceCache() {
998 $this->hideDeprecated( User::class . '::isBlockedFrom' );
999 // First, check the user isn't blocked
1000 $user = $this->getMutableTestUser()->getUser();
1001 $ut = Title::makeTitle( NS_USER_TALK, $user->getName() );
1002 $this->assertNull( $user->getBlock( false ) );
1003 $this->assertFalse( $user->isHidden() );
1005 // Block the user
1006 $blocker = $this->getTestSysop()->getUser();
1007 $block = new DatabaseBlock( [
1008 'hideName' => true,
1009 'allowUsertalk' => false,
1010 'reason' => 'Because',
1011 ] );
1012 $block->setTarget( $user );
1013 $block->setBlocker( $blocker );
1014 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1015 $res = $blockStore->insertBlock( $block );
1016 $this->assertTrue( (bool)$res['id'], 'Failed to insert block' );
1018 // Clear cache and confirm it loaded the block properly
1019 $user->clearInstanceCache();
1020 $this->assertInstanceOf( DatabaseBlock::class, $user->getBlock( false ) );
1021 $this->assertTrue( $user->isHidden() );
1023 // Unblock
1024 $blockStore->deleteBlock( $block );
1026 // Clear cache and confirm it loaded the not-blocked properly
1027 $user->clearInstanceCache();
1028 $this->assertNull( $user->getBlock( false ) );
1029 $this->assertFalse( $user->isHidden() );
1033 * @covers \MediaWiki\User\User::getBlock
1035 public function testCompositeBlocks() {
1036 $user = $this->getMutableTestUser()->getUser();
1037 $request = $user->getRequest();
1038 $this->setSessionUser( $user, $request );
1040 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1041 $ipBlock = new DatabaseBlock( [
1042 'address' => $user->getRequest()->getIP(),
1043 'by' => $this->getTestSysop()->getUser(),
1044 'createAccount' => true,
1045 ] );
1046 $blockStore->insertBlock( $ipBlock );
1048 $userBlock = new DatabaseBlock( [
1049 'address' => $user,
1050 'by' => $this->getTestSysop()->getUser(),
1051 'createAccount' => false,
1052 ] );
1053 $blockStore->insertBlock( $userBlock );
1055 $block = $user->getBlock();
1056 $this->assertInstanceOf( CompositeBlock::class, $block );
1057 $this->assertTrue( $block->isCreateAccountBlocked() );
1058 $this->assertTrue( $block->appliesToPasswordReset() );
1059 $this->assertTrue( $block->appliesToNamespace( NS_MAIN ) );
1063 * @covers \MediaWiki\User\User::getBlock
1065 public function testUserBlock() {
1066 $user = $this->getMutableTestUser()->getUser();
1067 $request = $user->getRequest();
1068 $this->setSessionUser( $user, $request );
1070 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1071 $ipBlock = new DatabaseBlock( [
1072 'address' => $user,
1073 'by' => $this->getTestSysop()->getUser(),
1074 'createAccount' => true,
1075 ] );
1076 $blockStore->insertBlock( $ipBlock );
1078 $block = $user->getBlock();
1079 $this->assertNotNull( $block, 'getuserBlock' );
1080 $this->assertNotNull( $block->getTargetUserIdentity(), 'getTargetUserIdentity()' );
1081 $this->assertSame( $user->getName(), $block->getTargetUserIdentity()->getName() );
1084 public static function provideIsBlockedFrom() {
1085 return [
1086 'Sitewide block, basic operation' => [ 'Test page', true ],
1087 'Sitewide block, not allowing user talk' => [
1088 self::USER_TALK_PAGE, true, [
1089 'allowUsertalk' => false,
1092 'Sitewide block, allowing user talk' => [
1093 self::USER_TALK_PAGE, false, [
1094 'allowUsertalk' => true,
1097 'Sitewide block, allowing user talk but $wgBlockAllowsUTEdit is false' => [
1098 self::USER_TALK_PAGE, true, [
1099 'allowUsertalk' => true,
1100 'blockAllowsUTEdit' => false,
1103 'Partial block, blocking the page' => [
1104 'Test page', true, [
1105 'pageRestrictions' => [ 'Test page' ],
1108 'Partial block, not blocking the page' => [
1109 'Test page 2', false, [
1110 'pageRestrictions' => [ 'Test page' ],
1113 'Partial block, not allowing user talk but user talk page is not blocked' => [
1114 self::USER_TALK_PAGE, false, [
1115 'allowUsertalk' => false,
1116 'pageRestrictions' => [ 'Test page' ],
1119 'Partial block, allowing user talk but user talk page is blocked' => [
1120 self::USER_TALK_PAGE, true, [
1121 'allowUsertalk' => true,
1122 'pageRestrictions' => [ self::USER_TALK_PAGE ],
1125 'Partial block, user talk page is not blocked but $wgBlockAllowsUTEdit is false' => [
1126 self::USER_TALK_PAGE, false, [
1127 'allowUsertalk' => false,
1128 'pageRestrictions' => [ 'Test page' ],
1129 'blockAllowsUTEdit' => false,
1132 'Partial block, user talk page is blocked and $wgBlockAllowsUTEdit is false' => [
1133 self::USER_TALK_PAGE, true, [
1134 'allowUsertalk' => true,
1135 'pageRestrictions' => [ self::USER_TALK_PAGE ],
1136 'blockAllowsUTEdit' => false,
1139 'Partial user talk namespace block, not allowing user talk' => [
1140 self::USER_TALK_PAGE, true, [
1141 'allowUsertalk' => false,
1142 'namespaceRestrictions' => [ NS_USER_TALK ],
1145 'Partial user talk namespace block, allowing user talk' => [
1146 self::USER_TALK_PAGE, false, [
1147 'allowUsertalk' => true,
1148 'namespaceRestrictions' => [ NS_USER_TALK ],
1151 'Partial user talk namespace block, where $wgBlockAllowsUTEdit is false' => [
1152 self::USER_TALK_PAGE, true, [
1153 'allowUsertalk' => true,
1154 'namespaceRestrictions' => [ NS_USER_TALK ],
1155 'blockAllowsUTEdit' => false,
1162 * @covers \MediaWiki\User\User::isBlockedFromEmailuser
1163 * @covers \MediaWiki\User\User::isAllowedToCreateAccount
1164 * @dataProvider provideIsBlockedFromAction
1165 * @param bool $blockFromEmail Whether to block email access.
1166 * @param bool $blockFromAccountCreation Whether to block account creation.
1168 public function testIsBlockedFromAction( $blockFromEmail, $blockFromAccountCreation ) {
1169 $this->hideDeprecated( User::class . '::isBlockedFromEmailuser' );
1170 $user = $this->getMutableTestUser( 'accountcreator' )->getUser();
1172 $block = new DatabaseBlock( [
1173 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
1174 'sitewide' => true,
1175 'blockEmail' => $blockFromEmail,
1176 'createAccount' => $blockFromAccountCreation
1177 ] );
1178 $block->setTarget( $user );
1179 $block->setBlocker( $this->getTestSysop()->getUser() );
1180 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1181 $blockStore->insertBlock( $block );
1183 $this->assertSame( $blockFromEmail, $user->isBlockedFromEmailuser() );
1184 $this->assertSame( !$blockFromAccountCreation, $user->isAllowedToCreateAccount() );
1187 public static function provideIsBlockedFromAction() {
1188 return [
1189 'Block email access and account creation' => [ true, true ],
1190 'Block only email access' => [ true, false ],
1191 'Block only account creation' => [ false, true ],
1192 'Allow email access and account creation' => [ false, false ],
1197 * @covers \MediaWiki\User\User::isBlockedFromUpload
1198 * @dataProvider provideIsBlockedFromUpload
1199 * @param bool $sitewide Whether to block sitewide.
1200 * @param bool $expected Whether the user is expected to be blocked from uploads.
1202 public function testIsBlockedFromUpload( $sitewide, $expected ) {
1203 $user = $this->getMutableTestUser()->getUser();
1205 $block = new DatabaseBlock( [
1206 'expiry' => wfTimestamp( TS_MW, wfTimestamp() + ( 40 * 60 * 60 ) ),
1207 'sitewide' => $sitewide,
1208 ] );
1209 $block->setTarget( $user );
1210 $block->setBlocker( $this->getTestSysop()->getUser() );
1211 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1212 $blockStore->insertBlock( $block );
1214 $this->assertSame( $expected, $user->isBlockedFromUpload() );
1217 public static function provideIsBlockedFromUpload() {
1218 return [
1219 'sitewide blocks block uploads' => [ true, true ],
1220 'partial blocks allow uploads' => [ false, false ],
1225 * @covers \MediaWiki\User\User::isSystemUser
1227 public function testIsSystemUser() {
1228 $this->assertFalse( $this->user->isSystemUser(), 'Normal users are not system users' );
1230 $user = User::newSystemUser( __METHOD__ );
1231 $this->assertTrue( $user->isSystemUser(), 'Users created with newSystemUser() are system users' );
1235 * @covers \MediaWiki\User\User::newSystemUser
1236 * @dataProvider provideNewSystemUser
1237 * @param string $exists How/whether to create the user before calling User::newSystemUser
1238 * - 'missing': Do not create the user
1239 * - 'actor': Create an anonymous actor
1240 * - 'user': Create a non-system user
1241 * - 'system': Create a system user
1242 * @param string $options Options to User::newSystemUser
1243 * @param array $testOpts Test options
1244 * @param string $expect 'user', 'exception', or 'null'
1246 public function testNewSystemUser( $exists, $options, $testOpts, $expect ) {
1247 $this->filterDeprecated( '/User::newSystemUser options/' );
1248 $origUser = null;
1249 $actorId = null;
1251 switch ( $exists ) {
1252 case 'missing':
1253 $name = 'TestNewSystemUser ' . TestUserRegistry::getNextId();
1254 break;
1256 case 'actor':
1257 $name = 'TestNewSystemUser ' . TestUserRegistry::getNextId();
1258 $this->getDb()->newInsertQueryBuilder()
1259 ->insertInto( 'actor' )
1260 ->row( [ 'actor_name' => $name ] )
1261 ->caller( __METHOD__ )
1262 ->execute();
1263 $actorId = (int)$this->getDb()->insertId();
1264 break;
1266 case 'user':
1267 $origUser = $this->getMutableTestUser()->getUser();
1268 $name = $origUser->getName();
1269 $actorId = $origUser->getActorId();
1270 break;
1272 case 'system':
1273 $name = 'TestNewSystemUser ' . TestUserRegistry::getNextId();
1274 $user = User::newSystemUser( $name ); // Heh.
1275 $actorId = $user->getActorId();
1276 // Use this hook as a proxy for detecting when a "steal" happens.
1277 $this->setTemporaryHook( 'InvalidateEmailComplete', function () {
1278 $this->fail( 'InvalidateEmailComplete hook should not have been called' );
1279 } );
1280 break;
1283 $globals = $testOpts['globals'] ?? [];
1284 if ( !empty( $testOpts['reserved'] ) ) {
1285 $globals[MainConfigNames::ReservedUsernames] = [ $name ];
1287 $this->overrideConfigValues( $globals );
1288 $userNameUtils = $this->getServiceContainer()->getUserNameUtils();
1289 $this->assertSame( empty( $testOpts['reserved'] ), $userNameUtils->isUsable( $name ) );
1290 $this->assertTrue( $userNameUtils->isValid( $name ) );
1292 if ( $expect === 'exception' ) {
1293 // T248195: Duplicate entry errors will log the exception, don't fail because of that.
1294 $this->setNullLogger( 'rdbms' );
1295 $this->expectException( Exception::class );
1297 $user = User::newSystemUser( $name, $options );
1298 if ( $expect === 'null' ) {
1299 $this->assertNull( $user );
1300 if ( $origUser ) {
1301 $this->assertNotSame(
1302 User::INVALID_TOKEN, TestingAccessWrapper::newFromObject( $origUser )->mToken
1304 $this->assertNotSame( '', $origUser->getEmail() );
1305 $this->assertFalse( $origUser->isSystemUser(), 'Normal users should not be system users' );
1307 } else {
1308 $this->assertInstanceOf( User::class, $user );
1309 $this->assertSame( $name, $user->getName() );
1310 if ( $actorId !== null ) {
1311 $this->assertSame( $actorId, $user->getActorId() );
1313 $this->assertSame( User::INVALID_TOKEN, TestingAccessWrapper::newFromObject( $user )->mToken );
1314 $this->assertSame( '', $user->getEmail() );
1315 $this->assertTrue( $user->isSystemUser(), 'Newly created system users should be system users' );
1319 public static function provideNewSystemUser() {
1320 return [
1321 'Basic creation' => [ 'missing', [], [], 'user' ],
1322 'No creation' => [ 'missing', [ 'create' => false ], [], 'null' ],
1323 'Validation fail' => [
1324 'missing',
1325 [ 'validate' => 'usable' ],
1326 [ 'reserved' => true ],
1327 'null'
1329 'No stealing' => [ 'user', [], [], 'null' ],
1330 'Stealing allowed' => [ 'user', [ 'steal' => true ], [], 'user' ],
1331 'Stealing an already-system user' => [ 'system', [ 'steal' => true ], [], 'user' ],
1332 'Anonymous actor (T236444)' => [ 'actor', [], [ 'reserved' => true ], 'user' ],
1333 'System user (T236444), reserved' => [ 'system', [], [ 'reserved' => true ], 'user' ],
1334 'Reserved but no anonymous actor' => [ 'missing', [], [ 'reserved' => true ], 'user' ],
1335 'Anonymous actor but no creation' => [ 'actor', [ 'create' => false ], [], 'null' ],
1336 'Anonymous actor but not reserved' => [ 'actor', [], [], 'exception' ],
1341 * @covers \MediaWiki\User\User::getName
1342 * @covers \MediaWiki\User\User::setName
1344 public function testUserName() {
1345 $user = User::newFromName( 'DannyS712' );
1346 $this->assertSame(
1347 'DannyS712',
1348 $user->getName(),
1349 'Santiy check: Users created using ::newFromName should return the name used'
1352 $user->setName( 'FooBarBaz' );
1353 $this->assertSame(
1354 'FooBarBaz',
1355 $user->getName(),
1356 'Changing a username via ::setName should be reflected in ::getName'
1361 * @covers \MediaWiki\User\User::getEmail
1362 * @covers \MediaWiki\User\User::setEmail
1363 * @covers \MediaWiki\User\User::invalidateEmail
1365 public function testUserEmail() {
1366 $user = $this->user;
1368 $user->setEmail( 'TestEmail@mediawiki.org' );
1369 $this->assertSame(
1370 'TestEmail@mediawiki.org',
1371 $user->getEmail(),
1372 'Setting an email via ::setEmail should be reflected in ::getEmail'
1375 $this->setTemporaryHook( 'UserSetEmail', function ( $user, &$email ) {
1376 $this->fail(
1377 'UserSetEmail hook should not be called when the new email ' .
1378 'is the same as the old email.'
1380 } );
1381 $user->setEmail( 'TestEmail@mediawiki.org' );
1383 $this->removeTemporaryHook( 'UserSetEmail' );
1385 $this->setTemporaryHook( 'UserSetEmail', static function ( $user, &$email ) {
1386 $email = 'SettingIntercepted@mediawiki.org';
1387 } );
1388 $user->setEmail( 'NewEmail@mediawiki.org' );
1389 $this->assertSame(
1390 'SettingIntercepted@mediawiki.org',
1391 $user->getEmail(),
1392 'Hooks can override setting email addresses'
1395 $this->setTemporaryHook( 'UserGetEmail', static function ( $user, &$email ) {
1396 $email = 'GettingIntercepted@mediawiki.org';
1397 } );
1398 $this->assertSame(
1399 'GettingIntercepted@mediawiki.org',
1400 $user->getEmail(),
1401 'Hooks can override getting email address'
1404 $this->removeTemporaryHook( 'UserGetEmail' );
1405 $this->removeTemporaryHook( 'UserSetEmail' );
1407 $user->invalidateEmail();
1408 $this->assertSame(
1410 $user->getEmail(),
1411 'After invalidation, a user email should be an empty string'
1416 * @covers \MediaWiki\User\User::setEmailWithConfirmation
1418 public function testSetEmailWithConfirmation_basic() {
1419 $user = $this->getTestUser()->getUser();
1420 $startingEmail = 'startingemail@mediawiki.org';
1421 $user->setEmail( $startingEmail );
1423 $this->overrideConfigValues( [
1424 MainConfigNames::EnableEmail => false,
1425 MainConfigNames::EmailAuthentication => false
1426 ] );
1427 $status = $user->setEmailWithConfirmation( 'test1@mediawiki.org' );
1428 $this->assertStatusError( 'emaildisabled', $status,
1429 'Cannot set email when email is disabled'
1431 $this->assertSame(
1432 $user->getEmail(),
1433 $startingEmail,
1434 'Email has not changed'
1437 $this->overrideConfigValue( MainConfigNames::EnableEmail, true );
1438 $status = $user->setEmailWithConfirmation( $startingEmail );
1439 $this->assertTrue(
1440 $status->getValue(),
1441 'Returns true if the email specified is the current email'
1443 $this->assertSame(
1444 $user->getEmail(),
1445 $startingEmail,
1446 'Email has not changed'
1451 * @covers \MediaWiki\User\User::isItemLoaded
1452 * @covers \MediaWiki\User\User::setItemLoaded
1454 public function testItemLoaded() {
1455 $user = User::newFromName( 'DannyS712' );
1456 $this->assertTrue(
1457 $user->isItemLoaded( 'name', 'only' ),
1458 'Users created by name have user names loaded'
1460 $this->assertFalse(
1461 $user->isItemLoaded( 'all', 'all' ),
1462 'Not everything is loaded yet'
1464 $user->load();
1465 $this->assertTrue(
1466 $user->isItemLoaded( 'FooBar', 'all' ),
1467 'All items now loaded'
1472 * @covers \MediaWiki\User\User::requiresHTTPS
1473 * @dataProvider provideRequiresHTTPS
1475 public function testRequiresHTTPS( $preference, bool $expected ) {
1476 $this->overrideConfigValues( [
1477 MainConfigNames::SecureLogin => true,
1478 MainConfigNames::ForceHTTPS => false,
1479 ] );
1481 $user = User::newFromName( 'UserWhoMayRequireHTTPS' );
1482 $user->addToDatabase();
1483 $this->getServiceContainer()->getUserOptionsManager()->setOption(
1484 $user,
1485 'prefershttps',
1486 $preference
1488 $user->saveSettings();
1490 $this->assertTrue( $user->isRegistered() );
1491 $this->assertSame( $expected, $user->requiresHTTPS() );
1494 public static function provideRequiresHTTPS() {
1495 return [
1496 'Wants, requires' => [ true, true ],
1497 'Does not want, not required' => [ false, false ],
1502 * @covers \MediaWiki\User\User::requiresHTTPS
1504 public function testRequiresHTTPS_disabled() {
1505 $this->overrideConfigValues( [
1506 MainConfigNames::SecureLogin => false,
1507 MainConfigNames::ForceHTTPS => false,
1508 ] );
1510 $user = User::newFromName( 'UserWhoMayRequireHTTP' );
1511 $user->addToDatabase();
1512 $this->getServiceContainer()->getUserOptionsManager()->setOption(
1513 $user,
1514 'prefershttps',
1515 true
1517 $user->saveSettings();
1519 $this->assertTrue( $user->isRegistered() );
1520 $this->assertFalse(
1521 $user->requiresHTTPS(),
1522 'User preference ignored if wgSecureLogin is false'
1527 * @covers \MediaWiki\User\User::requiresHTTPS
1529 public function testRequiresHTTPS_forced() {
1530 $this->overrideConfigValues( [
1531 MainConfigNames::SecureLogin => true,
1532 MainConfigNames::ForceHTTPS => true,
1533 ] );
1535 $user = User::newFromName( 'UserWhoMayRequireHTTP' );
1536 $user->addToDatabase();
1537 $this->getServiceContainer()->getUserOptionsManager()->setOption(
1538 $user,
1539 'prefershttps',
1540 false
1542 $user->saveSettings();
1544 $this->assertTrue( $user->isRegistered() );
1545 $this->assertTrue(
1546 $user->requiresHTTPS(),
1547 'User preference ignored if wgForceHTTPS is true'
1552 * @covers \MediaWiki\User\User::addToDatabase
1554 public function testAddToDatabase_bad() {
1555 $user = new User();
1556 $this->expectException( RuntimeException::class );
1557 $this->expectExceptionMessage(
1558 'User name field is not set.'
1560 $user->addToDatabase();
1564 * @covers \MediaWiki\User\User::pingLimiter
1566 public function testPingLimiter() {
1567 $user = $this->getTestUser()->getUser();
1569 $limiter = $this->createNoOpMock( RateLimiter::class, [ 'limit', 'isLimitable' ] );
1570 $limiter->method( 'isLimitable' )->willReturn( true );
1571 $limiter->method( 'limit' )->willReturnCallback(
1572 function ( RateLimitSubject $subject, $action ) use ( $user ) {
1573 $this->assertSame( $user, $subject->getUser() );
1574 return $action === 'limited';
1578 $this->setService( 'RateLimiter', $limiter );
1580 $this->assertTrue( $user->pingLimiter( 'limited' ) );
1581 $this->assertFalse( $user->pingLimiter( 'unlimited' ) );
1585 * @covers \MediaWiki\User\User::loadFromDatabase
1586 * @covers \MediaWiki\User\User::loadDefaults
1588 public function testBadUserID() {
1589 $user = User::newFromId( 999999999 );
1590 $this->assertSame( 'Unknown user', $user->getName() );
1594 * @covers \MediaWiki\User\User::probablyCan
1595 * @covers \MediaWiki\User\User::definitelyCan
1596 * @covers \MediaWiki\User\User::authorizeRead
1597 * @covers \MediaWiki\User\User::authorizeWrite
1599 public function testAuthorityMethods() {
1600 $user = $this->getTestUser()->getUser();
1601 $page = Title::makeTitle( NS_MAIN, 'Test' );
1602 $this->assertFalse( $user->probablyCan( 'create', $page ) );
1603 $this->assertFalse( $user->definitelyCan( 'create', $page ) );
1604 $this->assertFalse( $user->authorizeRead( 'create', $page ) );
1605 $this->assertFalse( $user->authorizeWrite( 'create', $page ) );
1607 $this->overrideUserPermissions( $user, 'createpage' );
1608 $this->assertTrue( $user->probablyCan( 'create', $page ) );
1609 $this->assertTrue( $user->definitelyCan( 'create', $page ) );
1610 $this->assertTrue( $user->authorizeRead( 'create', $page ) );
1611 $this->assertTrue( $user->authorizeWrite( 'create', $page ) );
1615 * @covers \MediaWiki\User\User::isAllowed
1616 * @covers \MediaWiki\User\User::__sleep
1618 public function testSerializationRoudTripWithAuthority() {
1619 $user = $this->getTestUser()->getUser();
1620 $isAllowed = $user->isAllowed( 'read' ); // Memoize the Authority
1621 $unserializedUser = unserialize( serialize( $user ) );
1622 $this->assertSame( $user->getId(), $unserializedUser->getId() );
1623 $this->assertSame( $isAllowed, $unserializedUser->isAllowed( 'read' ) );
1626 public static function provideIsTemp() {
1627 return [
1628 [ '~2024-1', true ],
1629 [ '~1', true ],
1630 [ 'Some user', false ],
1635 * @covers \MediaWiki\User\User::isTemp
1636 * @dataProvider provideIsTemp
1638 public function testIsTemp( $name, $expected ) {
1639 $this->enableAutoCreateTempUser();
1640 $user = new User;
1641 $user->setName( $name );
1642 $this->assertSame( $expected, $user->isTemp() );
1646 * @covers \MediaWiki\User\User::isTemp
1648 public function testSetIsTempInLoadDefaults() {
1649 $this->enableAutoCreateTempUser();
1650 $user = new User();
1651 $user->loadDefaults();
1652 $this->assertSame( false, $user->isTemp() );
1653 $user->loadDefaults( '~2024-1' );
1654 $this->assertSame( true, $user->isTemp() );
1658 * @covers \MediaWiki\User\User::isNamed
1660 public function testIsNamed() {
1661 $this->enableAutoCreateTempUser();
1663 // Temp user is not named
1664 $user = new User;
1665 $user->setName( '~1' );
1666 $this->assertFalse( $user->isNamed() );
1668 // Registered user is named
1669 $user = $this->getMutableTestUser()->getUser();
1670 $this->assertTrue( $user->isNamed() );
1672 // Anon is not named
1673 $user = new User;
1674 $this->assertFalse( $user->isNamed() );
1677 public static function provideAddToDatabase_temp() {
1678 return [
1679 [ '~1', '1' ],
1680 [ 'Some user', '0' ]
1685 * @covers \MediaWiki\User\User::addToDatabase
1686 * @dataProvider provideAddToDatabase_temp
1688 public function testAddToDatabase_temp( $name, $expected ) {
1689 $this->enableAutoCreateTempUser();
1691 $user = User::newFromName( $name );
1692 $user->addToDatabase();
1693 $field = $this->getDb()->newSelectQueryBuilder()
1694 ->select( 'user_is_temp' )
1695 ->from( 'user' )
1696 ->where( [ 'user_name' => $name ] )
1697 ->caller( __METHOD__ )
1698 ->fetchField();
1700 $this->assertSame( $expected, $field );
1704 * @covers \MediaWiki\User\User::spreadAnyEditBlock
1705 * @covers \MediaWiki\User\User::spreadBlock
1707 public function testSpreadAnyEditBlockForAnonUser() {
1708 $hookCalled = false;
1709 $this->setTemporaryHook( 'SpreadAnyEditBlock', static function () use ( &$hookCalled ){
1710 $hookCalled = true;
1711 } );
1712 $user = new User;
1713 $user->setName( '1.2.3.4' );
1714 $user->spreadAnyEditBlock();
1715 $this->assertFalse( $hookCalled );
1719 * @covers \MediaWiki\User\User::spreadAnyEditBlock
1720 * @covers \MediaWiki\User\User::spreadBlock
1721 * @dataProvider provideBlockWasSpreadValues
1723 public function testSpreadAnyEditBlockForUnblockedUser( $mockBlockWasSpreadHookValue ) {
1724 // Assert that the SpreadAnyEditBlock hook gets called with the right arguments when
1725 // ::spreadAnyEditBlock is called for a registered user.
1726 $hookCalled = false;
1727 $this->setTemporaryHook(
1728 'SpreadAnyEditBlock',
1729 function ( $user, &$blockWasSpread ) use ( &$hookCalled, $mockBlockWasSpreadHookValue ) {
1730 $hookCalled = true;
1731 $blockWasSpread = $mockBlockWasSpreadHookValue;
1732 $this->assertSame( $this->user, $user );
1735 $this->assertSame( $mockBlockWasSpreadHookValue, $this->user->spreadAnyEditBlock() );
1736 $this->assertTrue( $hookCalled );
1739 public static function provideBlockWasSpreadValues() {
1740 return [
1741 'SpreadAnyEditBlock hook handler sets $blockWasSpread to true' => [ true ],
1742 'No SpreadAnyEditBlock hook handler spread a block' => [ false ],
1747 * @covers \MediaWiki\User\User::spreadAnyEditBlock
1748 * @covers \MediaWiki\User\User::spreadBlock
1750 public function testSpreadAnyEditBlockForBlockedUser() {
1751 $this->getServiceContainer()->getBlockUserFactory()->newBlockUser(
1752 $this->user, $this->getTestSysop()->getAuthority(), 'indefinite', '', [ 'isAutoblocking' => true ]
1753 )->placeBlockUnsafe();
1754 RequestContext::getMain()->getRequest()->setIP( '1.2.3.4' );
1755 $this->assertTrue( $this->user->spreadAnyEditBlock() );
1756 $this->assertNotNull( $this->getServiceContainer()->getBlockManager()->getIpBlock( '1.2.3.4', true ) );