Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.46% |
106 / 127 |
|
46.67% |
7 / 15 |
CRAP | |
0.00% |
0 / 1 |
UserFactory | |
83.46% |
106 / 127 |
|
46.67% |
7 / 15 |
51.36 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
newFromName | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
newAnonymous | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
newFromNameOrIp | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
newFromId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
newFromActorId | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
newFromUserIdentity | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 | |||
newFromAnyId | |
65.52% |
19 / 29 |
|
0.00% |
0 / 1 |
12.32 | |||
newFromConfirmationCode | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
newFromRow | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromAuthority | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
newTempPlaceholder | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
newUnsavedTempUser | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
invalidateCache | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
3.01 | |||
getUserTableConnection | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
5.58 |
1 | <?php |
2 | /** |
3 | * Factory for creating User objects without static coupling. |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\User; |
24 | |
25 | use InvalidArgumentException; |
26 | use MediaWiki\Config\ServiceOptions; |
27 | use MediaWiki\Logger\LoggerFactory; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\Permissions\Authority; |
30 | use RuntimeException; |
31 | use stdClass; |
32 | use Wikimedia\Rdbms\IDatabase; |
33 | use Wikimedia\Rdbms\IDBAccessObject; |
34 | use Wikimedia\Rdbms\ILBFactory; |
35 | use Wikimedia\Rdbms\ILoadBalancer; |
36 | |
37 | /** |
38 | * Creates User objects. |
39 | * |
40 | * @since 1.35 |
41 | */ |
42 | class UserFactory implements UserRigorOptions { |
43 | |
44 | /** |
45 | * RIGOR_* constants are inherited from UserRigorOptions |
46 | */ |
47 | |
48 | /** @internal */ |
49 | public const CONSTRUCTOR_OPTIONS = [ |
50 | MainConfigNames::SharedDB, |
51 | MainConfigNames::SharedTables, |
52 | ]; |
53 | |
54 | private ServiceOptions $options; |
55 | private ILBFactory $loadBalancerFactory; |
56 | private ILoadBalancer $loadBalancer; |
57 | private UserNameUtils $userNameUtils; |
58 | |
59 | private ?User $lastUserFromIdentity = null; |
60 | |
61 | public function __construct( |
62 | ServiceOptions $options, |
63 | ILBFactory $loadBalancerFactory, |
64 | UserNameUtils $userNameUtils |
65 | ) { |
66 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
67 | $this->options = $options; |
68 | $this->loadBalancerFactory = $loadBalancerFactory; |
69 | $this->loadBalancer = $loadBalancerFactory->getMainLB(); |
70 | $this->userNameUtils = $userNameUtils; |
71 | } |
72 | |
73 | /** |
74 | * Factory method for creating users by name, replacing static User::newFromName |
75 | * |
76 | * This is slightly less efficient than newFromId(), so use newFromId() if |
77 | * you have both an ID and a name handy. |
78 | * |
79 | * @note unlike User::newFromName, this returns null instead of false for invalid usernames |
80 | * |
81 | * @since 1.35 |
82 | * @since 1.36 returns null instead of false for invalid user names |
83 | * |
84 | * @param string $name Username, validated by Title::newFromText |
85 | * @param string $validate Validation strategy, one of the RIGOR_* constants. For no |
86 | * validation, use RIGOR_NONE. If you just want to create valid user who can be either a named |
87 | * user or an IP, consider using newFromNameOrIp() instead of calling this with RIGOR_NONE. |
88 | * @return ?User User object, or null if the username is invalid (e.g. if it contains |
89 | * illegal characters or is an IP address). If the username is not present in the database, |
90 | * the result will be a user object with a name, a user id of 0, and default settings. |
91 | */ |
92 | public function newFromName( |
93 | string $name, |
94 | string $validate = self::RIGOR_VALID |
95 | ): ?User { |
96 | // RIGOR_* constants are the same here and in the UserNameUtils class |
97 | $canonicalName = $this->userNameUtils->getCanonical( $name, $validate ); |
98 | if ( $canonicalName === false ) { |
99 | return null; |
100 | } |
101 | |
102 | $user = new User(); |
103 | $user->mName = $canonicalName; |
104 | $user->mFrom = 'name'; |
105 | $user->setItemLoaded( 'name' ); |
106 | return $user; |
107 | } |
108 | |
109 | /** |
110 | * Returns a new anonymous User based on ip. |
111 | * |
112 | * @since 1.35 |
113 | * |
114 | * @param string|null $ip IP address |
115 | * @return User |
116 | */ |
117 | public function newAnonymous( ?string $ip = null ): User { |
118 | if ( $ip ) { |
119 | if ( !$this->userNameUtils->isIP( $ip ) ) { |
120 | throw new InvalidArgumentException( 'Invalid IP address' ); |
121 | } |
122 | $user = new User(); |
123 | $user->setName( $ip ); |
124 | } else { |
125 | $user = new User(); |
126 | } |
127 | return $user; |
128 | } |
129 | |
130 | /** |
131 | * Returns either an anonymous or a named User based on the supplied argument |
132 | * |
133 | * If IP is supplied, an anonymous user will be created, otherwise a valid named user. |
134 | * If you don't want to have the named user validated, use self::newFromName(). |
135 | * If you want to create simple anonymous user without providing the IP, use self::newAnonymous() |
136 | * |
137 | * @since 1.44 |
138 | * |
139 | * @param string $name IP address or username |
140 | * @return User|null |
141 | */ |
142 | public function newFromNameOrIp( string $name ): ?User { |
143 | if ( $this->userNameUtils->isIP( $name ) ) { |
144 | return $this->newAnonymous( $name ); |
145 | } |
146 | |
147 | return $this->newFromName( $name ); |
148 | } |
149 | |
150 | /** |
151 | * Factory method for creation from a given user ID, replacing User::newFromId |
152 | * |
153 | * @since 1.35 |
154 | * |
155 | * @param int $id Valid user ID |
156 | * @return User |
157 | */ |
158 | public function newFromId( int $id ): User { |
159 | $user = new User(); |
160 | $user->mId = $id; |
161 | $user->mFrom = 'id'; |
162 | $user->setItemLoaded( 'id' ); |
163 | return $user; |
164 | } |
165 | |
166 | /** |
167 | * Factory method for creation from a given actor ID, replacing User::newFromActorId |
168 | * |
169 | * @since 1.35 |
170 | * |
171 | * @param int $actorId |
172 | * @return User |
173 | */ |
174 | public function newFromActorId( int $actorId ): User { |
175 | $user = new User(); |
176 | $user->mActorId = $actorId; |
177 | $user->mFrom = 'actor'; |
178 | $user->setItemLoaded( 'actor' ); |
179 | return $user; |
180 | } |
181 | |
182 | /** |
183 | * Factory method for creation from a given UserIdentity, replacing User::newFromIdentity |
184 | * |
185 | * @since 1.35 |
186 | * |
187 | * @param UserIdentity $userIdentity |
188 | * @return User |
189 | */ |
190 | public function newFromUserIdentity( UserIdentity $userIdentity ): User { |
191 | if ( $userIdentity instanceof User ) { |
192 | return $userIdentity; |
193 | } |
194 | |
195 | $id = $userIdentity->getId(); |
196 | $name = $userIdentity->getName(); |
197 | // Cache the $userIdentity we converted last. This avoids redundant conversion |
198 | // in cases where we would be converting the same UserIdentity over and over, |
199 | // for instance because we need to access data preferences when formatting |
200 | // timestamps in a listing. |
201 | if ( |
202 | $this->lastUserFromIdentity |
203 | && $this->lastUserFromIdentity->getId() === $id |
204 | && $this->lastUserFromIdentity->getName() === $name |
205 | && $this->lastUserFromIdentity->getWikiId() === $userIdentity->getWikiId() |
206 | ) { |
207 | return $this->lastUserFromIdentity; |
208 | } |
209 | |
210 | $this->lastUserFromIdentity = $this->newFromAnyId( |
211 | $id === 0 ? null : $id, |
212 | $name === '' ? null : $name, |
213 | null, |
214 | $userIdentity->getWikiId() |
215 | ); |
216 | |
217 | return $this->lastUserFromIdentity; |
218 | } |
219 | |
220 | /** |
221 | * Factory method for creation from an ID, name, and/or actor ID, replacing User::newFromAnyId |
222 | * |
223 | * @note This does not check that the ID, name, and actor ID all correspond to |
224 | * the same user. |
225 | * |
226 | * @since 1.35 |
227 | * |
228 | * @param ?int $userId |
229 | * @param ?string $userName |
230 | * @param ?int $actorId |
231 | * @param string|false $dbDomain |
232 | * @return User |
233 | * @throws InvalidArgumentException if none of userId, userName, and actorId are specified |
234 | */ |
235 | public function newFromAnyId( |
236 | ?int $userId, |
237 | ?string $userName, |
238 | ?int $actorId = null, |
239 | $dbDomain = false |
240 | ): User { |
241 | // Stop-gap solution for the problem described in T222212. |
242 | // Force the User ID and Actor ID to zero for users loaded from the database |
243 | // of another wiki, to prevent subtle data corruption and confusing failure modes. |
244 | // FIXME this assumes the same username belongs to the same user on all wikis |
245 | if ( $dbDomain !== false ) { |
246 | LoggerFactory::getInstance( 'user' )->warning( |
247 | 'UserFactory::newFromAnyId called with cross-wiki user data', |
248 | [ 'userId' => $userId, 'userName' => $userName, 'actorId' => $actorId, |
249 | 'dbDomain' => $dbDomain, 'exception' => new RuntimeException() ] |
250 | ); |
251 | $userId = 0; |
252 | $actorId = 0; |
253 | } |
254 | |
255 | $user = new User; |
256 | $user->mFrom = 'defaults'; |
257 | |
258 | if ( $actorId !== null ) { |
259 | $user->mActorId = $actorId; |
260 | if ( $actorId !== 0 ) { |
261 | $user->mFrom = 'actor'; |
262 | } |
263 | $user->setItemLoaded( 'actor' ); |
264 | } |
265 | |
266 | if ( $userName !== null && $userName !== '' ) { |
267 | $user->mName = $userName; |
268 | $user->mFrom = 'name'; |
269 | $user->setItemLoaded( 'name' ); |
270 | } |
271 | |
272 | if ( $userId !== null ) { |
273 | $user->mId = $userId; |
274 | if ( $userId !== 0 ) { |
275 | $user->mFrom = 'id'; |
276 | } |
277 | $user->setItemLoaded( 'id' ); |
278 | } |
279 | |
280 | if ( $user->mFrom === 'defaults' ) { |
281 | throw new InvalidArgumentException( |
282 | 'Cannot create a user with no name, no ID, and no actor ID' |
283 | ); |
284 | } |
285 | |
286 | return $user; |
287 | } |
288 | |
289 | /** |
290 | * Factory method to fetch the user for a given email confirmation code, replacing User::newFromConfirmationCode |
291 | * |
292 | * This code is generated when an account is created or its e-mail address has changed. |
293 | * If the code is invalid or has expired, returns null. |
294 | * |
295 | * @since 1.35 |
296 | * |
297 | * @param string $confirmationCode |
298 | * @param int $flags |
299 | * @return User|null |
300 | */ |
301 | public function newFromConfirmationCode( |
302 | string $confirmationCode, |
303 | int $flags = IDBAccessObject::READ_NORMAL |
304 | ): ?User { |
305 | if ( ( $flags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) { |
306 | $db = $this->loadBalancer->getConnection( DB_PRIMARY ); |
307 | } else { |
308 | $db = $this->loadBalancer->getConnection( DB_REPLICA ); |
309 | } |
310 | |
311 | $id = $db->newSelectQueryBuilder() |
312 | ->select( 'user_id' ) |
313 | ->from( 'user' ) |
314 | ->where( [ 'user_email_token' => md5( $confirmationCode ) ] ) |
315 | ->andWhere( $db->expr( 'user_email_token_expires', '>', $db->timestamp() ) ) |
316 | ->recency( $flags ) |
317 | ->caller( __METHOD__ )->fetchField(); |
318 | |
319 | if ( !$id ) { |
320 | return null; |
321 | } |
322 | |
323 | return $this->newFromId( (int)$id ); |
324 | } |
325 | |
326 | /** |
327 | * @see User::newFromRow |
328 | * |
329 | * @since 1.36 |
330 | * |
331 | * @param stdClass $row A row from the user table |
332 | * @param array|null $data Further data to load into the object |
333 | * @return User |
334 | */ |
335 | public function newFromRow( $row, $data = null ): User { |
336 | return User::newFromRow( $row, $data ); |
337 | } |
338 | |
339 | /** |
340 | * @internal for transition from User to Authority as performer concept. |
341 | * @param Authority $authority |
342 | * @return User |
343 | */ |
344 | public function newFromAuthority( Authority $authority ): User { |
345 | if ( $authority instanceof User ) { |
346 | return $authority; |
347 | } |
348 | return $this->newFromUserIdentity( $authority->getUser() ); |
349 | } |
350 | |
351 | /** |
352 | * Create a placeholder user for an anonymous user who will be upgraded to |
353 | * a temporary user. This will throw an exception if temp user autocreation |
354 | * is disabled. |
355 | * |
356 | * @since 1.39 |
357 | * @return User |
358 | */ |
359 | public function newTempPlaceholder(): User { |
360 | $user = new User(); |
361 | $user->setName( $this->userNameUtils->getTempPlaceholder() ); |
362 | return $user; |
363 | } |
364 | |
365 | /** |
366 | * Create an unsaved temporary user with a previously acquired name or a placeholder name. |
367 | * |
368 | * @since 1.39 |
369 | * @param ?string $name If null, a placeholder name is used |
370 | * @return User |
371 | */ |
372 | public function newUnsavedTempUser( ?string $name ): User { |
373 | $user = new User(); |
374 | $user->setName( $name ?? $this->userNameUtils->getTempPlaceholder() ); |
375 | return $user; |
376 | } |
377 | |
378 | /** |
379 | * Purge user related caches, "touch" the user table to invalidate further caches |
380 | * @since 1.41 |
381 | * @param UserIdentity $userIdentity |
382 | */ |
383 | public function invalidateCache( UserIdentity $userIdentity ): void { |
384 | if ( !$userIdentity->isRegistered() ) { |
385 | return; |
386 | } |
387 | |
388 | $wikiId = $userIdentity->getWikiId(); |
389 | if ( $wikiId === UserIdentity::LOCAL ) { |
390 | $legacyUser = $this->newFromUserIdentity( $userIdentity ); |
391 | // Update user_touched within User class to manage state of User::mTouched for CAS check |
392 | $legacyUser->invalidateCache(); |
393 | } else { |
394 | // cross-wiki invalidation |
395 | $userId = $userIdentity->getId( $wikiId ); |
396 | |
397 | $dbw = $this->getUserTableConnection( ILoadBalancer::DB_PRIMARY, $wikiId ); |
398 | $dbw->newUpdateQueryBuilder() |
399 | ->update( 'user' ) |
400 | ->set( [ 'user_touched' => $dbw->timestamp() ] ) |
401 | ->where( [ 'user_id' => $userId ] ) |
402 | ->caller( __METHOD__ )->execute(); |
403 | |
404 | $dbw->onTransactionPreCommitOrIdle( |
405 | static function () use ( $wikiId, $userId ) { |
406 | User::purge( $wikiId, $userId ); |
407 | }, |
408 | __METHOD__ |
409 | ); |
410 | } |
411 | } |
412 | |
413 | /** |
414 | * @param int $mode |
415 | * @param string|false $wikiId |
416 | * @return IDatabase |
417 | */ |
418 | private function getUserTableConnection( $mode, $wikiId ): IDatabase { |
419 | if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) { |
420 | $wikiId = UserIdentity::LOCAL; |
421 | } |
422 | |
423 | if ( $this->options->get( MainConfigNames::SharedDB ) && |
424 | in_array( 'user', $this->options->get( MainConfigNames::SharedTables ) ) |
425 | ) { |
426 | // The main LB is aliased for the shared database in Setup.php |
427 | $lb = $this->loadBalancer; |
428 | } else { |
429 | $lb = $this->loadBalancerFactory->getMainLB( $wikiId ); |
430 | } |
431 | |
432 | return $lb->getConnection( $mode, [], $wikiId ); |
433 | } |
434 | } |