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