From d032bb52cd8c0de3066f84497d80f8fdf37ddadc Mon Sep 17 00:00:00 2001 From: Brad Jorsch Date: Thu, 12 Nov 2015 18:21:19 -0500 Subject: [PATCH] Add a central ID lookup service Anything that wants to be "central" right now has to depend on CentralAuth, and then either can't work without CentralAuth or has to branch all over the place based on whether CentralAuth is present. Most of the time all it really needs is a mapping from local users to central user IDs and back or the ability to query whether the local user is attached on some other wiki, so let's make an interface for that in core. See I52aa0460 for an example implementation (CentralAuth), and Ibd192e29 for an example use (OAuth). Bug: T111302 Change-Id: I49568358ec35fdfd0b9e53e441adabded5c7b80f --- RELEASE-NOTES-1.27 | 3 + autoload.php | 10 +- includes/DefaultSettings.php | 15 ++ includes/user/CentralIdLookup.php | 208 ++++++++++++++++++ includes/user/LocalIdLookup.php | 119 ++++++++++ includes/{ => user}/User.php | 0 includes/{ => user}/UserArray.php | 0 includes/{ => user}/UserArrayFromResult.php | 0 includes/{ => user}/UserRightsProxy.php | 0 .../includes/user/CentralIdLookupTest.php | 181 +++++++++++++++ .../includes/user/LocalIdLookupTest.php | 149 +++++++++++++ .../{ => user}/UserArrayFromResultTest.php | 0 .../phpunit/includes/{ => user}/UserTest.php | 4 +- 13 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 includes/user/CentralIdLookup.php create mode 100644 includes/user/LocalIdLookup.php rename includes/{ => user}/User.php (100%) rename includes/{ => user}/UserArray.php (100%) rename includes/{ => user}/UserArrayFromResult.php (100%) rename includes/{ => user}/UserRightsProxy.php (100%) create mode 100644 tests/phpunit/includes/user/CentralIdLookupTest.php create mode 100644 tests/phpunit/includes/user/LocalIdLookupTest.php rename tests/phpunit/includes/{ => user}/UserArrayFromResultTest.php (100%) rename tests/phpunit/includes/{ => user}/UserTest.php (99%) diff --git a/RELEASE-NOTES-1.27 b/RELEASE-NOTES-1.27 index 262bbbcf70..8d0853ed6e 100644 --- a/RELEASE-NOTES-1.27 +++ b/RELEASE-NOTES-1.27 @@ -84,6 +84,9 @@ production. * Added MWTimestamp::getTimezoneString() which returns the localized timezone string, if available. To localize this string, see the comments of $wgLocaltimezone in includes/DefaultSettings.php. +* Added CentralIdLookup, a service that allows extensions needing a concept of + "central" users to get that without having to know about specific central + authentication extensions. === External library changes in 1.27 === ==== Upgraded external libraries ==== diff --git a/autoload.php b/autoload.php index 2844dc7306..c37cbaa5a4 100644 --- a/autoload.php +++ b/autoload.php @@ -198,6 +198,7 @@ $wgAutoloadLocalClasses = array( 'CdbException' => __DIR__ . '/includes/compat/CdbCompat.php', 'CdbReader' => __DIR__ . '/includes/compat/CdbCompat.php', 'CdbWriter' => __DIR__ . '/includes/compat/CdbCompat.php', + 'CentralIdLookup' => __DIR__ . '/includes/user/CentralIdLookup.php', 'CgzCopyTransaction' => __DIR__ . '/maintenance/storage/recompressTracked.php', 'ChangePassword' => __DIR__ . '/maintenance/changePassword.php', 'ChangeTags' => __DIR__ . '/includes/changetags/ChangeTags.php', @@ -693,6 +694,7 @@ $wgAutoloadLocalClasses = array( 'LocalFileDeleteBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php', 'LocalFileMoveBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php', 'LocalFileRestoreBatch' => __DIR__ . '/includes/filerepo/file/LocalFile.php', + 'LocalIdLookup' => __DIR__ . '/includes/user/LocalIdLookup.php', 'LocalRepo' => __DIR__ . '/includes/filerepo/LocalRepo.php', 'LocalSettingsGenerator' => __DIR__ . '/includes/installer/LocalSettingsGenerator.php', 'LocalisationCache' => __DIR__ . '/includes/cache/LocalisationCache.php', @@ -1320,9 +1322,9 @@ $wgAutoloadLocalClasses = array( 'UploadStashZeroLengthFileException' => __DIR__ . '/includes/upload/UploadStash.php', 'UppercaseCollation' => __DIR__ . '/includes/Collation.php', 'UsageException' => __DIR__ . '/includes/api/ApiMain.php', - 'User' => __DIR__ . '/includes/User.php', - 'UserArray' => __DIR__ . '/includes/UserArray.php', - 'UserArrayFromResult' => __DIR__ . '/includes/UserArrayFromResult.php', + 'User' => __DIR__ . '/includes/user/User.php', + 'UserArray' => __DIR__ . '/includes/user/UserArray.php', + 'UserArrayFromResult' => __DIR__ . '/includes/user/UserArrayFromResult.php', 'UserBlockedError' => __DIR__ . '/includes/exception/UserBlockedError.php', 'UserCache' => __DIR__ . '/includes/cache/UserCache.php', 'UserDupes' => __DIR__ . '/maintenance/userDupes.inc', @@ -1330,7 +1332,7 @@ $wgAutoloadLocalClasses = array( 'UserNotLoggedIn' => __DIR__ . '/includes/exception/UserNotLoggedIn.php', 'UserOptions' => __DIR__ . '/maintenance/userOptions.inc', 'UserPasswordPolicy' => __DIR__ . '/includes/password/UserPasswordPolicy.php', - 'UserRightsProxy' => __DIR__ . '/includes/UserRightsProxy.php', + 'UserRightsProxy' => __DIR__ . '/includes/user/UserRightsProxy.php', 'UsercreateTemplate' => __DIR__ . '/includes/templates/Usercreate.php', 'UserloginTemplate' => __DIR__ . '/includes/templates/Userlogin.php', 'UserrightsPage' => __DIR__ . '/includes/specials/SpecialUserrights.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 9730265e92..3d859a9e50 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4333,6 +4333,21 @@ $wgActiveUserDays = 30; * @{ */ +/** + * Central ID lookup providers + * Key is the provider ID, value is a specification for ObjectFactory + * @since 1.27 + */ +$wgCentralIdLookupProviders = array( + 'local' => array( 'class' => 'LocalIdLookup' ), +); + +/** + * Central ID lookup provider to use by default + * @var string + */ +$wgCentralIdLookupProvider = 'local'; + /** * Password policy for local wiki users. A user's effective policy * is the superset of all policy statements from the policies for the diff --git a/includes/user/CentralIdLookup.php b/includes/user/CentralIdLookup.php new file mode 100644 index 0000000000..638a3e2a50 --- /dev/null +++ b/includes/user/CentralIdLookup.php @@ -0,0 +1,208 @@ +providerId = $providerId; + self::$instances[$providerId] = $provider; + } + } + } + + return self::$instances[$providerId]; + } + + final public function getProviderId() { + return $this->providerId; + } + + /** + * Check that the "audience" parameter is valid + * @param int|User $audience One of the audience constants, or a specific user + * @return User|null User to check against, or null if no checks are needed + * @throws InvalidArgumentException + */ + protected function checkAudience( $audience ) { + if ( $audience instanceof User ) { + return $audience; + } + if ( $audience === self::AUDIENCE_PUBLIC ) { + return new User; + } + if ( $audience === self::AUDIENCE_RAW ) { + return null; + } + throw new InvalidArgumentException( 'Invalid audience' ); + } + + /** + * Check that a User is attached on the specified wiki. + * + * If unattached local accounts don't exist in your extension, this comes + * down to a check whether the central account exists at all and that + * $wikiId is using the same central database. + * + * @param User $user + * @param string|null $wikiId Wiki to check attachment status. If null, check the current wiki. + * @return bool + */ + abstract public function isAttached( User $user, $wikiId = null ); + + /** + * Given central user IDs, return the (local) user names + * @note There's no requirement that the user names actually exist locally, + * or if they do that they're actually attached to the central account. + * @param array $idToName Array with keys being central user IDs + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return array Copy of $idToName with values set to user names (or + * empty-string if the user exists but $audience lacks the rights needed + * to see it). IDs not corresponding to a user are unchanged. + */ + abstract public function lookupCentralIds( + array $idToName, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ); + + /** + * Given (local) user names, return the central IDs + * @note There's no requirement that the user names actually exist locally, + * or if they do that they're actually attached to the central account. + * @param array $nameToId Array with keys being canonicalized user names + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return array Copy of $nameToId with values set to central IDs. + * Names not corresponding to a user (or $audience lacks the rights needed + * to see it) are unchanged. + */ + abstract public function lookupUserNames( + array $nameToId, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ); + + /** + * Given a central user ID, return the (local) user name + * @note There's no requirement that the user name actually exists locally, + * or if it does that it's actually attached to the central account. + * @param int $id Central user ID + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return string|null User name, or empty string if $audience lacks the + * rights needed to see it, or null if $id doesn't correspond to a user + */ + public function nameFromCentralId( + $id, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + $idToName = $this->lookupCentralIds( array( $id => null ), $audience, $flags ); + return $idToName[$id]; + } + + /** + * Given a (local) user name, return the central ID + * @note There's no requirement that the user name actually exists locally, + * or if it does that it's actually attached to the central account. + * @param string $name Canonicalized user name + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return int User ID; 0 if the name does not correspond to a user or + * $audience lacks the rights needed to see it. + */ + public function centralIdFromName( + $name, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + $nameToId = $this->lookupUserNames( array( $name => 0 ), $audience, $flags ); + return $nameToId[$name]; + } + + /** + * Given a central user ID, return a local User object + * @note Unlike nameFromCentralId(), this does guarantee that the local + * user exists and is attached to the central account. + * @param int $id Central user ID + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return User|null Local user, or null if: $id doesn't correspond to a + * user, $audience lacks the rights needed to see the user, the user + * doesn't exist locally, or the user isn't locally attached. + */ + public function localUserFromCentralId( + $id, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + $name = $this->nameFromCentralId( $id, $audience, $flags ); + if ( $name !== null && $name !== '' ) { + $user = User::newFromName( $name ); + if ( $user && $user->getId() && $this->isAttached( $user ) ) { + return $user; + } + } + return null; + } + + /** + * Given a local User object, return the central ID + * @note Unlike centralIdFromName(), this does guarantee that the local + * user is attached to the central account. + * @param User $user Local user + * @param int|User $audience One of the audience constants, or a specific user + * @param int $flags IDBAccessObject read flags + * @return int User ID; 0 if the local user does not correspond to a + * central user, $audience lacks the rights needed to see it, or the + * central user isn't locally attached. + */ + public function centralIdFromLocalUser( + User $user, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + return $this->isAttached( $user ) + ? $this->centralIdFromName( $user->getName(), $audience, $flags ) + : 0; + } + +} diff --git a/includes/user/LocalIdLookup.php b/includes/user/LocalIdLookup.php new file mode 100644 index 0000000000..04c5b905ab --- /dev/null +++ b/includes/user/LocalIdLookup.php @@ -0,0 +1,119 @@ +getId() ) { + return false; + } + + // Easy case, we're checking locally + if ( $wikiId === null || $wikiId === wfWikiId() ) { + return true; + } + + // Assume that shared user tables are set up as described above, if + // they're being used at all. + return $wgSharedDB !== null && + in_array( 'user', $wgSharedTables, true ) && + in_array( $wikiId, $wgLocalDatabases, true ); + } + + public function lookupCentralIds( + array $idToName, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + if ( !$idToName ) { + return array(); + } + + $audience = $this->checkAudience( $audience ); + $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); + $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) + ? array( 'LOCK IN SHARE MODE' ) + : array(); + + $tables = array( 'user' ); + $fields = array( 'user_id', 'user_name' ); + $where = array( + 'user_id' => array_map( 'intval', array_keys( $idToName ) ), + ); + $join = array(); + if ( $audience && !$audience->isAllowed( 'hideuser' ) ) { + $tables[] = 'ipblocks'; + $join['ipblocks'] = array( 'LEFT JOIN', 'ipb_user=user_id' ); + $fields[] = 'ipb_deleted'; + } + + $res = $db->select( $tables, $fields, $where, __METHOD__, $options, $join ); + foreach ( $res as $row ) { + $idToName[$row->user_id] = empty( $row->ipb_deleted ) ? $row->user_name : ''; + } + + return $idToName; + } + + public function lookupUserNames( + array $nameToId, $audience = self::AUDIENCE_PUBLIC, $flags = self::READ_NORMAL + ) { + if ( !$nameToId ) { + return array(); + } + + $audience = $this->checkAudience( $audience ); + $db = wfGetDB( ( $flags & self::READ_LATEST ) ? DB_MASTER : DB_SLAVE ); + $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) + ? array( 'LOCK IN SHARE MODE' ) + : array(); + + $tables = array( 'user' ); + $fields = array( 'user_id', 'user_name' ); + $where = array( + 'user_name' => array_map( 'strval', array_keys( $nameToId ) ), + ); + $join = array(); + if ( $audience && !$audience->isAllowed( 'hideuser' ) ) { + $tables[] = 'ipblocks'; + $join['ipblocks'] = array( 'LEFT JOIN', 'ipb_user=user_id' ); + $where[] = 'ipb_deleted = 0 OR ipb_deleted IS NULL'; + } + + $res = $db->select( $tables, $fields, $where, __METHOD__, $options, $join ); + foreach ( $res as $row ) { + $nameToId[$row->user_name] = (int)$row->user_id; + } + + return $nameToId; + } +} diff --git a/includes/User.php b/includes/user/User.php similarity index 100% rename from includes/User.php rename to includes/user/User.php diff --git a/includes/UserArray.php b/includes/user/UserArray.php similarity index 100% rename from includes/UserArray.php rename to includes/user/UserArray.php diff --git a/includes/UserArrayFromResult.php b/includes/user/UserArrayFromResult.php similarity index 100% rename from includes/UserArrayFromResult.php rename to includes/user/UserArrayFromResult.php diff --git a/includes/UserRightsProxy.php b/includes/user/UserRightsProxy.php similarity index 100% rename from includes/UserRightsProxy.php rename to includes/user/UserRightsProxy.php diff --git a/tests/phpunit/includes/user/CentralIdLookupTest.php b/tests/phpunit/includes/user/CentralIdLookupTest.php new file mode 100644 index 0000000000..386e7ab918 --- /dev/null +++ b/tests/phpunit/includes/user/CentralIdLookupTest.php @@ -0,0 +1,181 @@ +getMockForAbstractClass( 'CentralIdLookup' ); + + $this->setMwGlobals( array( + 'wgCentralIdLookupProviders' => array( + 'local' => array( 'class' => 'LocalIdLookup' ), + 'local2' => array( 'class' => 'LocalIdLookup' ), + 'mock' => array( 'factory' => function () use ( $mock ) { + return $mock; + } ), + 'bad' => array( 'class' => 'stdClass' ), + ), + 'wgCentralIdLookupProvider' => 'mock', + ) ); + + $this->assertSame( $mock, CentralIdLookup::factory() ); + $this->assertSame( $mock, CentralIdLookup::factory( 'mock' ) ); + $this->assertSame( 'mock', $mock->getProviderId() ); + + $local = CentralIdLookup::factory( 'local' ); + $this->assertNotSame( $mock, $local ); + $this->assertInstanceOf( 'LocalIdLookup', $local ); + $this->assertSame( $local, CentralIdLookup::factory( 'local' ) ); + $this->assertSame( 'local', $local->getProviderId() ); + + $local2 = CentralIdLookup::factory( 'local2' ); + $this->assertNotSame( $local, $local2 ); + $this->assertInstanceOf( 'LocalIdLookup', $local2 ); + $this->assertSame( 'local2', $local2->getProviderId() ); + + $this->assertNull( CentralIdLookup::factory( 'unconfigured' ) ); + $this->assertNull( CentralIdLookup::factory( 'bad' ) ); + } + + public function testCheckAudience() { + $mock = TestingAccessWrapper::newFromObject( + $this->getMockForAbstractClass( 'CentralIdLookup' ) + ); + + $user = User::newFromName( 'UTSysop' ); + $this->assertSame( $user, $mock->checkAudience( $user ) ); + + $user = $mock->checkAudience( CentralIdLookup::AUDIENCE_PUBLIC ); + $this->assertInstanceOf( 'User', $user ); + $this->assertSame( 0, $user->getId() ); + + $this->assertNull( $mock->checkAudience( CentralIdLookup::AUDIENCE_RAW ) ); + + try { + $mock->checkAudience( 100 ); + $this->fail( 'Expected exception not thrown' ); + } catch ( InvalidArgumentException $ex ) { + $this->assertSame( 'Invalid audience', $ex->getMessage() ); + } + } + + public function testNameFromCentralId() { + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->once() )->method( 'lookupCentralIds' ) + ->with( + $this->equalTo( array( 15 => null ) ), + $this->equalTo( CentralIdLookup::AUDIENCE_RAW ), + $this->equalTo( CentralIdLookup::READ_LATEST ) + ) + ->will( $this->returnValue( array( 15 => 'FooBar' ) ) ); + + $this->assertSame( + 'FooBar', + $mock->nameFromCentralId( 15, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST ) + ); + } + + /** + * @dataProvider provideLocalUserFromCentralId + * @param string $name + * @param bool $succeeds + */ + public function testLocalUserFromCentralId( $name, $succeeds ) { + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( true ) ); + $mock->expects( $this->once() )->method( 'lookupCentralIds' ) + ->with( + $this->equalTo( array( 42 => null ) ), + $this->equalTo( CentralIdLookup::AUDIENCE_RAW ), + $this->equalTo( CentralIdLookup::READ_LATEST ) + ) + ->will( $this->returnValue( array( 42 => $name ) ) ); + + $user = $mock->localUserFromCentralId( + 42, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST + ); + if ( $succeeds ) { + $this->assertInstanceOf( 'User', $user ); + $this->assertSame( $name, $user->getName() ); + } else { + $this->assertNull( $user ); + } + + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( false ) ); + $mock->expects( $this->once() )->method( 'lookupCentralIds' ) + ->with( + $this->equalTo( array( 42 => null ) ), + $this->equalTo( CentralIdLookup::AUDIENCE_RAW ), + $this->equalTo( CentralIdLookup::READ_LATEST ) + ) + ->will( $this->returnValue( array( 42 => $name ) ) ); + $this->assertNull( + $mock->localUserFromCentralId( 42, CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST ) + ); + } + + public static function provideLocalUserFromCentralId() { + return array( + array( 'UTSysop', true ), + array( 'UTDoesNotExist', false ), + array( null, false ), + array( '', false ), + array( '', false ), + ); + } + + public function testCentralIdFromName() { + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->once() )->method( 'lookupUserNames' ) + ->with( + $this->equalTo( array( 'FooBar' => 0 ) ), + $this->equalTo( CentralIdLookup::AUDIENCE_RAW ), + $this->equalTo( CentralIdLookup::READ_LATEST ) + ) + ->will( $this->returnValue( array( 'FooBar' => 23 ) ) ); + + $this->assertSame( + 23, + $mock->centralIdFromName( 'FooBar', CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST ) + ); + } + + public function testCentralIdFromLocalUser() { + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( true ) ); + $mock->expects( $this->once() )->method( 'lookupUserNames' ) + ->with( + $this->equalTo( array( 'FooBar' => 0 ) ), + $this->equalTo( CentralIdLookup::AUDIENCE_RAW ), + $this->equalTo( CentralIdLookup::READ_LATEST ) + ) + ->will( $this->returnValue( array( 'FooBar' => 23 ) ) ); + + $this->assertSame( + 23, + $mock->centralIdFromLocalUser( + User::newFromName( 'FooBar' ), CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST + ) + ); + + $mock = $this->getMockForAbstractClass( 'CentralIdLookup' ); + $mock->expects( $this->any() )->method( 'isAttached' ) + ->will( $this->returnValue( false ) ); + $mock->expects( $this->never() )->method( 'lookupUserNames' ); + + $this->assertSame( + 0, + $mock->centralIdFromLocalUser( + User::newFromName( 'FooBar' ), CentralIdLookup::AUDIENCE_RAW, CentralIdLookup::READ_LATEST + ) + ); + } + +} diff --git a/tests/phpunit/includes/user/LocalIdLookupTest.php b/tests/phpunit/includes/user/LocalIdLookupTest.php new file mode 100644 index 0000000000..2bea575c75 --- /dev/null +++ b/tests/phpunit/includes/user/LocalIdLookupTest.php @@ -0,0 +1,149 @@ +stashMwGlobals( array( 'wgGroupPermissions' ) ); + $wgGroupPermissions['local-id-lookup-test']['hideuser'] = true; + } + + public function addDBData() { + for ( $i = 1; $i <= 4; $i++ ) { + $user = User::newFromName( "UTLocalIdLookup$i" ); + if ( $user->getId() == 0 ) { + $user->addToDatabase(); + } + $this->localUsers["UTLocalIdLookup$i"] = $user->getId(); + } + + User::newFromName( 'UTLocalIdLookup1' )->addGroup( 'local-id-lookup-test' ); + + $block = new Block( array( + 'address' => 'UTLocalIdLookup3', + 'by' => User::idFromName( 'UTSysop' ), + 'reason' => __METHOD__, + 'expiry' => '1 day', + 'hideName' => false, + ) ); + $block->insert(); + + $block = new Block( array( + 'address' => 'UTLocalIdLookup4', + 'by' => User::idFromName( 'UTSysop' ), + 'reason' => __METHOD__, + 'expiry' => '1 day', + 'hideName' => true, + ) ); + $block->insert(); + } + + public function testLookupCentralIds() { + $lookup = new LocalIdLookup(); + $user1 = User::newFromName( 'UTLocalIdLookup1' ); + $user2 = User::newFromName( 'UTLocalIdLookup2' ); + + $this->assertTrue( $user1->isAllowed( 'hideuser' ), 'sanity check' ); + $this->assertFalse( $user2->isAllowed( 'hideuser' ), 'sanity check' ); + + $this->assertSame( array(), $lookup->lookupCentralIds( array() ) ); + + $expect = array_flip( $this->localUsers ); + $expect[123] = 'X'; + ksort( $expect ); + + $expect2 = $expect; + $expect2[$this->localUsers['UTLocalIdLookup4']] = ''; + + $arg = array_fill_keys( array_keys( $expect ), 'X' ); + + $this->assertSame( $expect2, $lookup->lookupCentralIds( $arg ) ); + $this->assertSame( $expect, $lookup->lookupCentralIds( $arg, CentralIdLookup::AUDIENCE_RAW ) ); + $this->assertSame( $expect, $lookup->lookupCentralIds( $arg, $user1 ) ); + $this->assertSame( $expect2, $lookup->lookupCentralIds( $arg, $user2 ) ); + } + + public function testLookupUserNames() { + $lookup = new LocalIdLookup(); + $user1 = User::newFromName( 'UTLocalIdLookup1' ); + $user2 = User::newFromName( 'UTLocalIdLookup2' ); + + $this->assertTrue( $user1->isAllowed( 'hideuser' ), 'sanity check' ); + $this->assertFalse( $user2->isAllowed( 'hideuser' ), 'sanity check' ); + + $this->assertSame( array(), $lookup->lookupUserNames( array() ) ); + + $expect = $this->localUsers; + $expect['UTDoesNotExist'] = 'X'; + ksort( $expect ); + + $expect2 = $expect; + $expect2['UTLocalIdLookup4'] = 'X'; + + $arg = array_fill_keys( array_keys( $expect ), 'X' ); + + $this->assertSame( $expect2, $lookup->lookupUserNames( $arg ) ); + $this->assertSame( $expect, $lookup->lookupUserNames( $arg, CentralIdLookup::AUDIENCE_RAW ) ); + $this->assertSame( $expect, $lookup->lookupUserNames( $arg, $user1 ) ); + $this->assertSame( $expect2, $lookup->lookupUserNames( $arg, $user2 ) ); + } + + public function testIsAttached() { + $lookup = new LocalIdLookup(); + $user1 = User::newFromName( 'UTLocalIdLookup1' ); + $user2 = User::newFromName( 'DoesNotExist' ); + + $this->assertTrue( $lookup->isAttached( $user1 ) ); + $this->assertFalse( $lookup->isAttached( $user2 ) ); + + $wiki = wfWikiId(); + $this->assertTrue( $lookup->isAttached( $user1, $wiki ) ); + $this->assertFalse( $lookup->isAttached( $user2, $wiki ) ); + + $wiki = 'not-' . wfWikiId(); + $this->assertFalse( $lookup->isAttached( $user1, $wiki ) ); + $this->assertFalse( $lookup->isAttached( $user2, $wiki ) ); + } + + /** + * @dataProvider provideIsAttachedShared + * @param bool $sharedDB $wgSharedDB is set + * @param bool $sharedTable $wgSharedTables contains 'user' + * @param bool $localDBSet $wgLocalDatabases contains the shared DB + */ + public function testIsAttachedShared( $sharedDB, $sharedTable, $localDBSet ) { + global $wgDBName; + $this->setMwGlobals( array( + 'wgSharedDB' => $sharedDB ? $wgDBName : null, + 'wgSharedTables' => $sharedTable ? array( 'user' ) : array(), + 'wgLocalDatabases' => $localDBSet ? array( 'shared' ) : array(), + ) ); + + $lookup = new LocalIdLookup(); + $this->assertSame( + $sharedDB && $sharedTable && $localDBSet, + $lookup->isAttached( User::newFromName( 'UTLocalIdLookup1' ), 'shared' ) + ); + } + + public static function provideIsAttachedShared() { + $ret = array(); + for ( $i = 0; $i < 7; $i++ ) { + $ret[] = array( + (bool)( $i & 1 ), + (bool)( $i & 2 ), + (bool)( $i & 4 ), + ); + } + return $ret; + } + +} diff --git a/tests/phpunit/includes/UserArrayFromResultTest.php b/tests/phpunit/includes/user/UserArrayFromResultTest.php similarity index 100% rename from tests/phpunit/includes/UserArrayFromResultTest.php rename to tests/phpunit/includes/user/UserArrayFromResultTest.php diff --git a/tests/phpunit/includes/UserTest.php b/tests/phpunit/includes/user/UserTest.php similarity index 99% rename from tests/phpunit/includes/UserTest.php rename to tests/phpunit/includes/user/UserTest.php index 4c6f083bb7..45c4b8c5fa 100644 --- a/tests/phpunit/includes/UserTest.php +++ b/tests/phpunit/includes/user/UserTest.php @@ -509,11 +509,11 @@ class UserTest extends MediaWikiTestCase { $setcookieInvocation = end( $setcookieInvocations ); $actualExpiry = $setcookieInvocation->parameters[2]; - // TODO: ± 300 seconds compensates for + // TODO: ± 600 seconds compensates for // slow-running tests. However, the dependency on the time // function should be removed. This requires some way // to mock/isolate User->setExtendedLoginCookie's call to time() - $this->assertEquals( $expectedExpiry, $actualExpiry, '', 300 ); + $this->assertEquals( $expectedExpiry, $actualExpiry, '', 600 ); } } -- 2.20.1