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 DBAccessObjectUtils;
26use IDBAccessObject;
27use InvalidArgumentException;
28use MediaWiki\Permissions\Authority;
29use stdClass;
30use User;
31use Wikimedia\Rdbms\ILoadBalancer;
32
33/**
34 * Creates User objects.
35 *
36 * For now, there is nothing much interesting in this class. It was meant for preventing static User
37 * methods causing problems in unit tests.
38 *
39 * @since 1.35
40 */
41class UserFactory implements IDBAccessObject, UserRigorOptions {
42
43	/**
44	 * RIGOR_* constants are inherited from UserRigorOptions
45	 * READ_* constants are inherited from IDBAccessObject
46	 */
47
48	/** @var ILoadBalancer */
49	private $loadBalancer;
50
51	/** @var UserNameUtils */
52	private $userNameUtils;
53
54	/** @var User|null */
55	private $lastUserFromIdentity = null;
56
57	/**
58	 * @param ILoadBalancer $loadBalancer
59	 * @param UserNameUtils $userNameUtils
60	 */
61	public function __construct(
62		ILoadBalancer $loadBalancer,
63		UserNameUtils $userNameUtils
64	) {
65		$this->loadBalancer = $loadBalancer;
66		$this->userNameUtils = $userNameUtils;
67	}
68
69	/**
70	 * Factory method for creating users by name, replacing static User::newFromName
71	 *
72	 * This is slightly less efficient than newFromId(), so use newFromId() if
73	 * you have both an ID and a name handy.
74	 *
75	 * @note unlike User::newFromName, this returns null instead of false for invalid usernames
76	 *
77	 * @since 1.35
78	 * @since 1.36 returns null instead of false for invalid user names
79	 *
80	 * @param string $name Username, validated by Title::newFromText
81	 * @param string $validate Validation strategy, one of the RIGOR_* constants. For no
82	 *    validation, use RIGOR_NONE.
83	 * @return ?User User object, or null if the username is invalid (e.g. if it contains
84	 *  illegal characters or is an IP address). If the username is not present in the database,
85	 *  the result will be a user object with a name, a user id of 0, and default settings.
86	 */
87	public function newFromName(
88		string $name,
89		string $validate = self::RIGOR_VALID
90	) : ?User {
91		// RIGOR_* constants are the same here and in the UserNameUtils class
92		$canonicalName = $this->userNameUtils->getCanonical( $name, $validate );
93		if ( $canonicalName === false ) {
94			return null;
95		}
96
97		$user = new User();
98		$user->mName = $canonicalName;
99		$user->mFrom = 'name';
100		$user->setItemLoaded( 'name' );
101		return $user;
102	}
103
104	/**
105	 * Returns a new anonymous User based on ip.
106	 *
107	 * @since 1.35
108	 *
109	 * @param string|null $ip IP address
110	 * @return User
111	 */
112	public function newAnonymous( $ip = null ) : User {
113		if ( $ip ) {
114			$validIp = $this->userNameUtils->isIP( $ip );
115			if ( $validIp ) {
116				$user = $this->newFromName( $ip, self::RIGOR_NONE );
117			} else {
118				throw new InvalidArgumentException( 'Invalid IP address' );
119			}
120		} else {
121			$user = new User();
122		}
123		return $user;
124	}
125
126	/**
127	 * Factory method for creation from a given user ID, replacing User::newFromId
128	 *
129	 * @since 1.35
130	 *
131	 * @param int $id Valid user ID
132	 * @return User The corresponding User object
133	 */
134	public function newFromId( int $id ) : User {
135		$user = new User();
136		$user->mId = $id;
137		$user->mFrom = 'id';
138		$user->setItemLoaded( 'id' );
139		return $user;
140	}
141
142	/**
143	 * Factory method for creation from a given actor ID, replacing User::newFromActorId
144	 *
145	 * @since 1.35
146	 *
147	 * @param int $actorId
148	 * @return User
149	 */
150	public function newFromActorId( int $actorId ) : User {
151		$user = new User();
152		$user->mActorId = $actorId;
153		$user->mFrom = 'actor';
154		$user->setItemLoaded( 'actor' );
155		return $user;
156	}
157
158	/**
159	 * Factory method for creation fom a given UserIdentity, replacing User::newFromIdentity
160	 *
161	 * @since 1.35
162	 *
163	 * @param UserIdentity $userIdentity
164	 * @return User
165	 */
166	public function newFromUserIdentity( UserIdentity $userIdentity ) : User {
167		if ( $userIdentity instanceof User ) {
168			return $userIdentity;
169		}
170
171		// Cache the $userIdentity we converted last. This avoids redundant conversion
172		// in cases where we would be converting the same UserIdentity over and over,
173		// for instance because we need to access data preferences when formatting
174		// timestamps in a listing.
175		if (
176			$this->lastUserFromIdentity
177			&& $this->lastUserFromIdentity->getId() == $userIdentity->getId()
178			&& $this->lastUserFromIdentity->getName() == $userIdentity->getName()
179		) {
180			return $this->lastUserFromIdentity;
181		}
182
183		$this->lastUserFromIdentity = $this->newFromAnyId(
184			$userIdentity->getId() === 0 ? null : $userIdentity->getId(),
185			$userIdentity->getName() === '' ? null : $userIdentity->getName(),
186			null
187		);
188
189		return $this->lastUserFromIdentity;
190	}
191
192	/**
193	 * Factory method for creation from an ID, name, and/or actor ID, replacing User::newFromAnyId
194	 *
195	 * @note This does not check that the ID, name, and actor ID all correspond to
196	 * the same user.
197	 *
198	 * @since 1.35
199	 *
200	 * @param ?int $userId
201	 * @param ?string $userName
202	 * @param ?int $actorId
203	 * @param bool|string $dbDomain
204	 * @return User
205	 * @throws InvalidArgumentException if none of userId, userName, and actorId are specified
206	 */
207	public function newFromAnyId(
208		?int $userId,
209		?string $userName,
210		?int $actorId = null,
211		$dbDomain = false
212	) : User {
213		// Stop-gap solution for the problem described in T222212.
214		// Force the User ID and Actor ID to zero for users loaded from the database
215		// of another wiki, to prevent subtle data corruption and confusing failure modes.
216		if ( $dbDomain !== false ) {
217			$userId = 0;
218			$actorId = 0;
219		}
220
221		$user = new User;
222		$user->mFrom = 'defaults';
223
224		if ( $actorId !== null ) {
225			$user->mActorId = $actorId;
226			if ( $actorId !== 0 ) {
227				$user->mFrom = 'actor';
228			}
229			$user->setItemLoaded( 'actor' );
230		}
231
232		if ( $userName !== null && $userName !== '' ) {
233			$user->mName = $userName;
234			$user->mFrom = 'name';
235			$user->setItemLoaded( 'name' );
236		}
237
238		if ( $userId !== null ) {
239			$user->mId = $userId;
240			if ( $userId !== 0 ) {
241				$user->mFrom = 'id';
242			}
243			$user->setItemLoaded( 'id' );
244		}
245
246		if ( $user->mFrom === 'defaults' ) {
247			throw new InvalidArgumentException(
248				'Cannot create a user with no name, no ID, and no actor ID'
249			);
250		}
251
252		return $user;
253	}
254
255	/**
256	 * Factory method to fetch the user for a given email confirmation code, replacing User::newFromConfirmationCode
257	 *
258	 * This code is generated when an account is created or its e-mail address has changed.
259	 * If the code is invalid or has expired, returns null.
260	 *
261	 * @since 1.35
262	 *
263	 * @param string $confirmationCode
264	 * @param int $flags
265	 * @return User|null
266	 */
267	public function newFromConfirmationCode(
268		string $confirmationCode,
269		int $flags = self::READ_NORMAL
270	) {
271		list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
272
273		$db = $this->loadBalancer->getConnectionRef( $index );
274
275		$id = $db->selectField(
276			'user',
277			'user_id',
278			[
279				'user_email_token' => md5( $confirmationCode ),
280				'user_email_token_expires > ' . $db->addQuotes( $db->timestamp() ),
281			],
282			__METHOD__,
283			$options
284		);
285
286		if ( !$id ) {
287			return null;
288		}
289
290		return $this->newFromId( (int)$id );
291	}
292
293	/**
294	 * @see User::newFromRow
295	 *
296	 * @since 1.36
297	 *
298	 * @param stdClass $row A row from the user table
299	 * @param array|null $data Further data to load into the object
300	 * @return User
301	 */
302	public function newFromRow( $row, $data = null ) {
303		return User::newFromRow( $row, $data );
304	}
305
306	/**
307	 * @internal for transition from User to Authority as performer concept.
308	 * @param Authority $authority
309	 * @return User
310	 */
311	public function newFromAuthority( Authority $authority ): User {
312		if ( $authority instanceof User ) {
313			return $authority;
314		}
315		return $this->newFromUserIdentity( $authority->getUser() );
316	}
317
318}
319