Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.94% covered (success)
93.94%
31 / 33
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
MultiFormatUserIdentityLookup
93.94% covered (success)
93.94%
31 / 33
33.33% covered (danger)
33.33%
1 / 3
15.05
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserIdentity
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
11
 parseUserDesignator
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
3.01
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\User;
8
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\MainConfigNames;
11use MediaWiki\Permissions\Authority;
12use MediaWiki\Status\Status;
13use MediaWiki\WikiMap\WikiMap;
14use Wikimedia\IPUtils;
15
16/**
17 * A service to look up user identities based on the user input.
18 * This class is similar to UserIdentityLookup but allows to search for users using
19 * multiple input formats (e.g. user ID, user name, user name with interwiki suffix).
20 *
21 * This service is designed to be of higher level than UserIdentityLookup and handy in
22 * places like special pages or API modules where the input can be in multiple formats.
23 * Additionally, this service allows to check for user rights, e.g. whether a hidden user
24 * should be returned or not.
25 *
26 * @since 1.45
27 * @ingroup User
28 */
29class MultiFormatUserIdentityLookup {
30
31    /** @internal */
32    public const CONSTRUCTOR_OPTIONS = [
33        MainConfigNames::LocalDatabases,
34        MainConfigNames::UserrightsInterwikiDelimiter
35    ];
36
37    public function __construct(
38        private readonly ActorStoreFactory $actorStoreFactory,
39        private readonly UserFactory $userFactory,
40        private readonly ServiceOptions $options,
41    ) {
42        $this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
43    }
44
45    /**
46     * Looks up for a user identity based on the passed designator string.
47     *
48     * The designator can be in one of the following formats:
49     * - username (e.g. "Example"),
50     * - user ID (e.g. "#123"),
51     * - username or id with interwiki suffix (e.g. "Example@wiki", "#123@wiki"), where the separator
52     *   between those two parts is defined by the `wgUserrightsInterwikiDelimiter` config option.
53     *
54     * For interwiki users, this service assures that the remote wiki exists and is considered
55     * a local database (i.e. is listed in `wgLocalDatabases`).
56     * @param string $designator
57     * @param Authority|null $viewer
58     * @return Status<UserIdentity>
59     */
60    public function getUserIdentity( string $designator, ?Authority $viewer = null ): Status {
61        [ $name, $wikiId ] = $this->parseUserDesignator( $designator );
62
63        // Check if the wikiId is valid
64        if ( $wikiId !== UserIdentity::LOCAL ) {
65            $localDatabases = $this->options->get( MainConfigNames::LocalDatabases );
66            if ( !in_array( $wikiId, $localDatabases ) ) {
67                return Status::newFatal( 'userrights-nodatabase', $wikiId );
68            }
69        }
70
71        if ( $name === '' ) {
72            return Status::newFatal( 'nouserspecified' );
73        }
74
75        if ( IPUtils::isValid( $name ) ) {
76            return Status::newGood( UserIdentityValue::newAnonymous( $name, $wikiId ) );
77        }
78
79        $userIdentityLookup = $this->actorStoreFactory->getUserIdentityLookup( $wikiId );
80        if ( $name[0] == '#' ) {
81            $id = intval( substr( $name, 1 ) );
82            $user = $userIdentityLookup->getUserIdentityByUserId( $id );
83        } else {
84            $user = $userIdentityLookup->getUserIdentityByName( $name );
85        }
86
87        if ( !$user ) {
88            return Status::newFatal( 'nosuchusershort', $designator );
89        }
90
91        // If an authority is specified, check if the viewer is allowed to see the user
92        // If they can't, pretend the user doesn't exist
93        if (
94            $viewer !== null &&
95            $user->getWikiId() === UserIdentity::LOCAL &&
96            $this->userFactory->newFromUserIdentity( $user )->isHidden() &&
97            !$viewer->isAllowed( 'hideuser' )
98        ) {
99            return Status::newFatal( 'nosuchusershort', $designator );
100        }
101
102        return Status::newGood( $user );
103    }
104
105    /**
106     * Parses the user designator into the name and wiki parts.
107     * If the wikiId refers to local wiki, ensure that the wikiId is set to UserIdentity::LOCAL.
108     * @return array{0:string,1:string|false} [name, wikiId]
109     */
110    private function parseUserDesignator( string $designator ): array {
111        $interwikiSeparator = $this->options->get( MainConfigNames::UserrightsInterwikiDelimiter );
112        $designatorParts = explode( $interwikiSeparator, $designator );
113
114        $name = trim( $designator );
115        $wikiId = UserIdentity::LOCAL;
116        if ( count( $designatorParts ) >= 2 ) {
117            $name = trim( $designatorParts[0] );
118            $wikiId = trim( $designatorParts[1] );
119
120            if ( WikiMap::isCurrentWikiId( $wikiId ) ) {
121                $wikiId = UserIdentity::LOCAL;
122            }
123        }
124        return [ $name, $wikiId ];
125    }
126}