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