Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
60.87% covered (warning)
60.87%
28 / 46
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CampaignsCentralUserLookup
60.87% covered (warning)
60.87%
28 / 46
40.00% covered (danger)
40.00%
4 / 10
43.97
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 newFromUserIdentity
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 newFromAuthority
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newFromLocalUsername
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getUserName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 existsAndIsVisible
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 existsLocally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNames
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getNamesIncludingDeletedAndSuppressed
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
5
 isValidLocalUsername
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\MWEntity;
6
7use InvalidArgumentException;
8use MediaWiki\User\CentralId\CentralIdLookup;
9use MediaWiki\User\User;
10use MediaWiki\User\UserFactory;
11use MediaWiki\User\UserIdentity;
12use MediaWiki\User\UserNameUtils;
13
14/**
15 * This class is used to retrieve data about global user accounts, like MW's CentralIdLookup.
16 */
17class CampaignsCentralUserLookup {
18    public const SERVICE_NAME = 'CampaignEventsCentralUserLookup';
19
20    public const USER_NOT_FOUND = '[not found]';
21    public const USER_HIDDEN = '[hidden]';
22
23    private CentralIdLookup $centralIDLookup;
24    private UserFactory $userFactory;
25    private UserNameUtils $userNameUtils;
26
27    /**
28     * @var array<int,string> Cache of usernames by central user ID. Values can be either usernames, or the special
29     * values self::USER_NOT_FOUND and self::USER_HIDDEN.
30     */
31    private array $nameByIDCache = [];
32
33    /**
34     * @param CentralIdLookup $centralIdLookup
35     * @param UserFactory $userFactory
36     * @param UserNameUtils $userNameUtils
37     */
38    public function __construct(
39        CentralIdLookup $centralIdLookup,
40        UserFactory $userFactory,
41        UserNameUtils $userNameUtils
42    ) {
43        $this->centralIDLookup = $centralIdLookup;
44        $this->userFactory = $userFactory;
45        $this->userNameUtils = $userNameUtils;
46    }
47
48    /**
49     * Returns the central user corresponding to the given local user, if it exists. This method should be
50     * avoided if possible, because we should only work with (the current) Authority and CentralUser.
51     * @param UserIdentity $userIdentity
52     * @return CentralUser
53     * @throws UserNotGlobalException
54     */
55    public function newFromUserIdentity( UserIdentity $userIdentity ): CentralUser {
56        // @note This does not check if the user is deleted. This seems easier, and
57        // the CentralAuth provider ignored $audience anyway.
58        // TODO This should be improved somehow (T312821)
59        $centralID = $this->centralIDLookup->centralIdFromLocalUser( $userIdentity, CentralIdLookup::AUDIENCE_RAW );
60        if ( $centralID === 0 ) {
61            throw new UserNotGlobalException( $userIdentity->getId() );
62        }
63        return new CentralUser( $centralID );
64    }
65
66    /**
67     * Returns the central user corresponding to the given authority, if it exists. NOTE: Make sure to handle
68     * the exception, if the user is not guaranteed to have a global account.
69     * @param ICampaignsAuthority $authority
70     * @return CentralUser
71     * @throws UserNotGlobalException
72     */
73    public function newFromAuthority( ICampaignsAuthority $authority ): CentralUser {
74        $mwUser = $this->userFactory->newFromId( $authority->getLocalUserID() );
75        return $this->newFromUserIdentity( $mwUser );
76    }
77
78    /**
79     * Returns the central user corresponding to the given username, if it exists. NOTE: Make sure to handle
80     * the exception, if the user is not guaranteed to have a global account.
81     * Callers must ensure that the username is valid
82     * @param string $userName
83     * @return CentralUser
84     * @throws UserNotGlobalException
85     */
86    public function newFromLocalUsername( string $userName ): CentralUser {
87        $mwUser = $this->userFactory->newFromName( $userName );
88        if ( !$mwUser instanceof User ) {
89            throw new InvalidArgumentException(
90                "Invalid username: $userName"
91            );
92        }
93        return $this->newFromUserIdentity( $mwUser );
94    }
95
96    /**
97     * @param CentralUser $user
98     * @return string
99     * @throws CentralUserNotFoundException
100     * @throws HiddenCentralUserException
101     */
102    public function getUserName( CentralUser $user ): string {
103        $centralID = $user->getCentralID();
104        $ret = $this->getNamesIncludingDeletedAndSuppressed( [ $centralID => null ] )[$centralID];
105        if ( $ret === self::USER_NOT_FOUND ) {
106            throw new CentralUserNotFoundException( $centralID );
107        }
108        if ( $ret === self::USER_HIDDEN ) {
109            throw new HiddenCentralUserException( $centralID );
110        }
111        return $ret;
112    }
113
114    /**
115     * Checks whether the given CentralUser actually exists and is visible.
116     * @param CentralUser $user
117     * @return bool
118     */
119    public function existsAndIsVisible( CentralUser $user ): bool {
120        try {
121            $this->getUserName( $user );
122            return true;
123        } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) {
124            return false;
125        }
126    }
127
128    /**
129     * Checks whether the given central user is attached, i.e. it exists on the current wiki.
130     * @param CentralUser $user
131     * @return bool
132     */
133    public function existsLocally( CentralUser $user ): bool {
134        // NOTE: we can't really use isAttached here, because that takes a (local) UserIdentity, and the purpose
135        // of this method is to tell us if a local user exists at all.
136        return $this->centralIDLookup->localUserFromCentralId( $user->getCentralID() ) !== null;
137    }
138
139    /**
140     * Returns the usernames of the users with the given central user IDs. Suppressed and non-existing users are
141     * excluded from the return value.
142     *
143     * @param array<int,null> $centralIDsMap The central IDs are used as keys, the values must be null
144     * @return array<int,string> Same keys as given to the method, but the values are the names.
145     */
146    public function getNames( array $centralIDsMap ): array {
147        $allNames = $this->getNamesIncludingDeletedAndSuppressed( $centralIDsMap );
148        return array_filter(
149            $allNames,
150            static fn ( $name ) => $name !== self::USER_HIDDEN && $name !== self::USER_NOT_FOUND
151        );
152    }
153
154    /**
155     * Returns the usernames of the users with the given central user IDs. Suppressed and non-existing users are
156     * included in the return value, with self::USER_NOT_FOUND or self::USER_HIDDEN as the value.
157     *
158     * @param array<int,null> $centralIDsMap The central IDs are used as keys, the values must be null
159     * @return array<int,string> Same keys as given to the method, but the values are the names.
160     */
161    public function getNamesIncludingDeletedAndSuppressed( array $centralIDsMap ): array {
162        $ret = array_intersect_key( $this->nameByIDCache, $centralIDsMap );
163        $remainingIDsMap = array_diff_key( $centralIDsMap, $this->nameByIDCache );
164        if ( !$remainingIDsMap ) {
165            return $ret;
166        }
167        $remainingNames = $this->centralIDLookup->lookupCentralIds( $remainingIDsMap );
168        foreach ( $remainingNames as $id => $name ) {
169            if ( $name === null ) {
170                $ret[$id] = self::USER_NOT_FOUND;
171            } elseif ( $name === '' ) {
172                $ret[$id] = self::USER_HIDDEN;
173            } else {
174                $ret[$id] = $name;
175            }
176            $this->nameByIDCache[$id] = $ret[$id];
177        }
178        return $ret;
179    }
180
181    /**
182     * @param string $userName
183     * @return bool
184     * @todo This method should possibly be moved to a separate service in the future.
185     */
186    public function isValidLocalUsername( string $userName ): bool {
187        return $this->userNameUtils->getCanonical( $userName ) !== false;
188    }
189}