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