Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
61.70% |
29 / 47 |
|
45.45% |
5 / 11 |
CRAP | |
0.00% |
0 / 1 |
CampaignsCentralUserLookup | |
61.70% |
29 / 47 |
|
45.45% |
5 / 11 |
45.77 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newFromUserIdentity | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
newFromAuthority | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
newFromLocalUsername | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getUserName | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
existsAndIsVisible | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
existsLocally | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNames | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getIDs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNamesIncludingDeletedAndSuppressed | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
5 | |||
isValidLocalUsername | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\MWEntity; |
6 | |
7 | use InvalidArgumentException; |
8 | use MediaWiki\User\CentralId\CentralIdLookup; |
9 | use MediaWiki\User\User; |
10 | use MediaWiki\User\UserFactory; |
11 | use MediaWiki\User\UserIdentity; |
12 | use MediaWiki\User\UserNameUtils; |
13 | |
14 | /** |
15 | * This class is used to retrieve data about global user accounts, like MW's CentralIdLookup. |
16 | */ |
17 | class 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 | * Given a map whose keys are normalized local usernames, returns a copy of that map where every user with a |
156 | * global account has the corresponding value replaced by their central user ID. Users without a global account |
157 | * have their values unchanged. |
158 | * |
159 | * @param array<string,mixed> $localNamesMap |
160 | * @return array<string,mixed> |
161 | */ |
162 | public function getIDs( array $localNamesMap ): array { |
163 | return $this->centralIDLookup->lookupUserNames( $localNamesMap ); |
164 | } |
165 | |
166 | /** |
167 | * Returns the usernames of the users with the given central user IDs. Suppressed and non-existing users are |
168 | * included in the return value, with self::USER_NOT_FOUND or self::USER_HIDDEN as the value. |
169 | * |
170 | * @param array<int,null> $centralIDsMap The central IDs are used as keys, the values must be null |
171 | * @return array<int,string> Same keys as given to the method, but the values are the names. |
172 | */ |
173 | public function getNamesIncludingDeletedAndSuppressed( array $centralIDsMap ): array { |
174 | $ret = array_intersect_key( $this->nameByIDCache, $centralIDsMap ); |
175 | $remainingIDsMap = array_diff_key( $centralIDsMap, $this->nameByIDCache ); |
176 | if ( !$remainingIDsMap ) { |
177 | return $ret; |
178 | } |
179 | $remainingNames = $this->centralIDLookup->lookupCentralIds( $remainingIDsMap ); |
180 | foreach ( $remainingNames as $id => $name ) { |
181 | if ( $name === null ) { |
182 | $ret[$id] = self::USER_NOT_FOUND; |
183 | } elseif ( $name === '' ) { |
184 | $ret[$id] = self::USER_HIDDEN; |
185 | } else { |
186 | $ret[$id] = $name; |
187 | } |
188 | $this->nameByIDCache[$id] = $ret[$id]; |
189 | } |
190 | return $ret; |
191 | } |
192 | |
193 | /** |
194 | * @param string $userName |
195 | * @return bool |
196 | * @todo This method should possibly be moved to a separate service in the future. |
197 | */ |
198 | public function isValidLocalUsername( string $userName ): bool { |
199 | return $this->userNameUtils->getCanonical( $userName ) !== false; |
200 | } |
201 | } |