Merge ".mailmap: Correct two contributor names"
[mediawiki.git] / includes / user / ActorStore.php
blob6837aed555b47e1ce10bb138f9838cec418e7f93
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
21 namespace MediaWiki\User;
23 use CannotCreateActorException;
24 use InvalidArgumentException;
25 use MediaWiki\Block\HideUserUtils;
26 use MediaWiki\DAO\WikiAwareEntity;
27 use MediaWiki\User\TempUser\TempUserConfig;
28 use Psr\Log\LoggerInterface;
29 use stdClass;
30 use Wikimedia\Assert\Assert;
31 use Wikimedia\IPUtils;
32 use Wikimedia\Rdbms\DBQueryError;
33 use Wikimedia\Rdbms\IDatabase;
34 use Wikimedia\Rdbms\IDBAccessObject;
35 use Wikimedia\Rdbms\ILoadBalancer;
36 use Wikimedia\Rdbms\IReadableDatabase;
38 /**
39 * Service to read or write data in the actor table.
41 * @since 1.36
42 * @ingroup User
44 class ActorStore implements UserIdentityLookup, ActorNormalization {
46 public const UNKNOWN_USER_NAME = 'Unknown user';
48 private const LOCAL_CACHE_SIZE = 100;
50 private ILoadBalancer $loadBalancer;
51 private UserNameUtils $userNameUtils;
52 private TempUserConfig $tempUserConfig;
53 private LoggerInterface $logger;
54 private HideUserUtils $hideUserUtils;
56 /** @var string|false */
57 private $wikiId;
59 private ActorCache $cache;
61 private bool $allowCreateIpActors;
63 /**
64 * @param ILoadBalancer $loadBalancer
65 * @param UserNameUtils $userNameUtils
66 * @param TempUserConfig $tempUserConfig
67 * @param LoggerInterface $logger
68 * @param HideUserUtils $hideUserUtils
69 * @param string|false $wikiId
71 public function __construct(
72 ILoadBalancer $loadBalancer,
73 UserNameUtils $userNameUtils,
74 TempUserConfig $tempUserConfig,
75 LoggerInterface $logger,
76 HideUserUtils $hideUserUtils,
77 $wikiId = WikiAwareEntity::LOCAL
78 ) {
79 Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' );
81 $this->loadBalancer = $loadBalancer;
82 $this->userNameUtils = $userNameUtils;
83 $this->tempUserConfig = $tempUserConfig;
84 $this->logger = $logger;
85 $this->hideUserUtils = $hideUserUtils;
86 $this->wikiId = $wikiId;
88 $this->cache = new ActorCache( self::LOCAL_CACHE_SIZE );
90 $this->allowCreateIpActors = !$this->tempUserConfig->isEnabled();
93 /**
94 * Instantiate a new UserIdentity object based on a $row from the actor table.
96 * Use this method when an actor row was already fetched from the DB via a join.
97 * This method just constructs a new instance and does not try fetching missing
98 * values from the DB again, use {@link UserIdentityLookup} for that.
100 * @param stdClass $row with the following fields:
101 * - int actor_id
102 * - string actor_name
103 * - int|null actor_user
104 * @return UserIdentity
105 * @throws InvalidArgumentException
107 public function newActorFromRow( stdClass $row ): UserIdentity {
108 $actorId = (int)$row->actor_id;
109 $userId = isset( $row->actor_user ) ? (int)$row->actor_user : 0;
110 if ( $actorId === 0 ) {
111 throw new InvalidArgumentException( "Actor ID is 0 for {$row->actor_name} and {$userId}" );
114 $normalizedName = $this->normalizeUserName( $row->actor_name );
115 if ( $normalizedName === null ) {
116 $this->logger->warning( 'Encountered invalid actor name in database', [
117 'user_id' => $userId,
118 'actor_id' => $actorId,
119 'actor_name' => $row->actor_name,
120 'wiki_id' => $this->wikiId ?: 'local'
121 ] );
122 // TODO: once we have guaranteed db only contains valid actor names,
123 // we can skip normalization here - T273933
124 if ( $row->actor_name === '' ) {
125 throw new InvalidArgumentException( "Actor name can not be empty for {$userId} and {$actorId}" );
129 $actor = new UserIdentityValue( $userId, $row->actor_name, $this->wikiId );
130 $this->cache->add( $actorId, $actor );
131 return $actor;
135 * Instantiate a new UserIdentity object based on field values from a DB row.
137 * Until {@link ActorMigration} is completed, the actor table joins alias actor field names
138 * to legacy field names. This method is convenience to construct the UserIdentity based on
139 * legacy field names. It's more relaxed with typing then ::newFromRow to better support legacy
140 * code, so always prefer ::newFromRow in new code. Eventually, once {@link ActorMigration}
141 * is completed and all queries use explicit join with actor table, this method will be
142 * deprecated and removed.
144 * @throws InvalidArgumentException
145 * @param int|null $userId
146 * @param string|null $name
147 * @param int|null $actorId
148 * @return UserIdentity
150 public function newActorFromRowFields( $userId, $name, $actorId ): UserIdentity {
151 // For backwards compatibility we are quite relaxed about what to accept,
152 // but try not to create entirely incorrect objects. As we move more code
153 // from ActorMigration aliases to proper join with the actor table,
154 // we should use ::newActorFromRow more, and eventually deprecate this method.
155 $userId = $userId === null ? 0 : (int)$userId;
156 $name ??= '';
157 if ( $actorId === null ) {
158 throw new InvalidArgumentException( "Actor ID is null for {$name} and {$userId}" );
160 if ( (int)$actorId === 0 ) {
161 throw new InvalidArgumentException( "Actor ID is 0 for {$name} and {$userId}" );
164 $normalizedName = $this->normalizeUserName( $name );
165 if ( $normalizedName === null ) {
166 $this->logger->warning( 'Encountered invalid actor name in database', [
167 'user_id' => $userId,
168 'actor_id' => $actorId,
169 'actor_name' => $name,
170 'wiki_id' => $this->wikiId ?: 'local'
171 ] );
172 // TODO: once we have guaranteed the DB entries only exist for normalized names,
173 // we can skip normalization here - T273933
174 if ( $name === '' ) {
175 throw new InvalidArgumentException( "Actor name can not be empty for {$userId} and {$actorId}" );
179 $actorId = (int)$actorId;
180 $actor = new UserIdentityValue(
181 $userId,
182 $name,
183 $this->wikiId
186 $this->cache->add( $actorId, $actor );
187 return $actor;
191 * @param UserIdentity $actor
192 * @internal for use in User object only
194 public function deleteUserIdentityFromCache( UserIdentity $actor ) {
195 $this->cache->remove( $actor );
199 * Find an actor by $id.
201 * @param int $actorId
202 * @param IReadableDatabase $db The database connection to operate on.
203 * The database must correspond to ActorStore's wiki ID.
204 * @return UserIdentity|null Returns null if no actor with this $actorId exists in the database.
206 public function getActorById( int $actorId, IReadableDatabase $db ): ?UserIdentity {
207 $this->checkDatabaseDomain( $db );
209 if ( !$actorId ) {
210 return null;
213 return $this->cache->getActor( ActorCache::KEY_ACTOR_ID, $actorId ) ??
214 $this->newSelectQueryBuilder( $db )
215 ->caller( __METHOD__ )
216 ->conds( [ 'actor_id' => $actorId ] )
217 ->fetchUserIdentity() ??
218 // The actor ID mostly comes from DB, so if we can't find an actor by ID,
219 // it's most likely due to lagged replica and not cause it doesn't actually exist.
220 // Probably we just inserted it? Try primary database.
221 $this->newSelectQueryBuilder( IDBAccessObject::READ_LATEST )
222 ->caller( __METHOD__ )
223 ->conds( [ 'actor_id' => $actorId ] )
224 ->fetchUserIdentity();
228 * Find an actor by $name
230 * @param string $name
231 * @param int $queryFlags one of IDBAccessObject constants
232 * @return UserIdentity|null
234 public function getUserIdentityByName(
235 string $name,
236 int $queryFlags = IDBAccessObject::READ_NORMAL
237 ): ?UserIdentity {
238 $normalizedName = $this->normalizeUserName( $name );
239 if ( $normalizedName === null ) {
240 return null;
243 return $this->cache->getActor( ActorCache::KEY_USER_NAME, $normalizedName ) ??
244 $this->newSelectQueryBuilder( $queryFlags )
245 ->caller( __METHOD__ )
246 ->whereUserNames( $normalizedName )
247 ->fetchUserIdentity();
251 * Find an actor by $userId
253 * @param int $userId
254 * @param int $queryFlags one of IDBAccessObject constants
255 * @return UserIdentity|null
257 public function getUserIdentityByUserId(
258 int $userId,
259 int $queryFlags = IDBAccessObject::READ_NORMAL
260 ): ?UserIdentity {
261 if ( !$userId ) {
262 return null;
265 return $this->cache->getActor( ActorCache::KEY_USER_ID, $userId ) ??
266 $this->newSelectQueryBuilder( $queryFlags )
267 ->caller( __METHOD__ )
268 ->whereUserIds( $userId )
269 ->fetchUserIdentity();
273 * Attach the actor ID to $user for backwards compatibility.
275 * @todo remove this method when no longer needed (T273974).
277 * @param UserIdentity $user
278 * @param int $id
279 * @param bool $assigned whether a new actor ID was just assigned.
281 private function attachActorId( UserIdentity $user, int $id, bool $assigned ) {
282 if ( $user instanceof User ) {
283 $user->setActorId( $id );
284 if ( $assigned ) {
285 $user->invalidateCache();
291 * Detach the actor ID from $user for backwards compatibility.
293 * @todo remove this method when no longer needed (T273974).
295 * @param UserIdentity $user
297 private function detachActorId( UserIdentity $user ) {
298 if ( $user instanceof User ) {
299 $user->setActorId( 0 );
304 * Find the actor_id of the given $user.
306 * @param UserIdentity $user
307 * @param IReadableDatabase $db The database connection to operate on.
308 * The database must correspond to ActorStore's wiki ID.
309 * @return int|null
311 public function findActorId( UserIdentity $user, IReadableDatabase $db ): ?int {
312 // TODO: we want to assert this user belongs to the correct wiki,
313 // but User objects are always local and we used to use them
314 // on a non-local DB connection. We need to first deprecate this
315 // possibility and then throw on mismatching User object - T273972
316 // $user->assertWiki( $this->wikiId );
317 $this->deprecateInvalidCrossWikiParam( $user );
319 // TODO: In the future we would be able to assume UserIdentity name is ok
320 // and will be able to skip normalization here - T273933
321 $name = $this->normalizeUserName( $user->getName() );
322 if ( $name === null ) {
323 $this->logger->warning( 'Encountered a UserIdentity with invalid name', [
324 'user_name' => $user->getName()
325 ] );
326 return null;
329 $id = $this->findActorIdInternal( $name, $db );
331 // Set the actor ID in the User object. To be removed, see T274148.
332 if ( $id && $user instanceof User ) {
333 $user->setActorId( $id );
336 return $id;
340 * Find the actor_id of the given $name.
342 * @param string $name
343 * @param IReadableDatabase $db The database connection to operate on.
344 * The database must correspond to ActorStore's wiki ID.
345 * @return int|null
347 public function findActorIdByName( $name, IReadableDatabase $db ): ?int {
348 $name = $this->normalizeUserName( $name );
349 if ( $name === null ) {
350 return null;
353 return $this->findActorIdInternal( $name, $db );
357 * Find actor_id of the given $user using the passed $db connection.
359 * @param string $name
360 * @param IReadableDatabase $db The database connection to operate on.
361 * The database must correspond to ActorStore's wiki ID.
362 * @param bool $lockInShareMode
363 * @return int|null
365 private function findActorIdInternal(
366 string $name,
367 IReadableDatabase $db,
368 bool $lockInShareMode = false
369 ): ?int {
370 // Note: UserIdentity::getActorId will be deprecated and removed,
371 // and this is the replacement for it. Can't call User::getActorId, cause
372 // User always thinks it's local, so we could end up fetching the ID
373 // from the wrong database.
375 $cachedValue = $this->cache->getActorId( ActorCache::KEY_USER_NAME, $name );
376 if ( $cachedValue ) {
377 return $cachedValue;
380 $queryBuilder = $db->newSelectQueryBuilder()
381 ->select( [ 'actor_user', 'actor_name', 'actor_id' ] )
382 ->from( 'actor' )
383 ->where( [ 'actor_name' => $name ] );
384 if ( $lockInShareMode ) {
385 $queryBuilder->lockInShareMode();
388 $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
390 if ( !$row || !$row->actor_id ) {
391 return null;
393 // to cache row
394 $this->newActorFromRow( $row );
396 return (int)$row->actor_id;
400 * Attempt to assign an actor ID to the given $user. If it is already assigned,
401 * return the existing ID.
403 * @note If called within a transaction, the returned ID might become invalid
404 * if the transaction is rolled back, so it should not be passed outside of the
405 * transaction context.
407 * @param UserIdentity $user
408 * @param IDatabase $dbw The database connection to acquire the ID from.
409 * The database must correspond to ActorStore's wiki ID.
410 * @return int actor ID greater then 0
411 * @throws CannotCreateActorException if no actor ID has been assigned to this $user
413 public function acquireActorId( UserIdentity $user, IDatabase $dbw ): int {
414 $this->checkDatabaseDomain( $dbw );
415 [ $userId, $userName ] = $this->validateActorForInsertion( $user );
417 // allow cache to be used, because if it is in the cache, it already has an actor ID
418 $existingActorId = $this->findActorIdInternal( $userName, $dbw );
419 if ( $existingActorId ) {
420 $this->attachActorId( $user, $existingActorId, false );
421 return $existingActorId;
424 $dbw->newInsertQueryBuilder()
425 ->insertInto( 'actor' )
426 ->ignore()
427 ->row( [ 'actor_user' => $userId, 'actor_name' => $userName ] )
428 ->caller( __METHOD__ )->execute();
430 if ( $dbw->affectedRows() ) {
431 $actorId = $dbw->insertId();
432 } else {
433 // Outdated cache?
434 // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
435 $actorId = $this->findActorIdInternal( $userName, $dbw, true );
436 if ( !$actorId ) {
437 throw new CannotCreateActorException(
438 'Failed to create actor ID for ' .
439 'user_id={userId} user_name="{userName}"',
440 [ 'userId' => $userId, 'userName' => $userName ]
445 $this->attachActorId( $user, $actorId, true );
446 // Cache row we've just created
447 $cachedUserIdentity = $this->newActorFromRowFields( $userId, $userName, $actorId );
448 $this->setUpRollbackHandler( $dbw, $cachedUserIdentity, $user );
449 return $actorId;
453 * Create a new actor for the given $user. If an actor with this name already exists,
454 * this method throws.
456 * @note If called within a transaction, the returned ID might become invalid
457 * if the transaction is rolled back, so it should not be passed outside of the
458 * transaction context.
460 * @param UserIdentity $user
461 * @param IDatabase $dbw
462 * @return int actor ID greater then 0
463 * @throws CannotCreateActorException if an actor with this name already exist.
464 * @internal for use in user account creation only.
466 public function createNewActor( UserIdentity $user, IDatabase $dbw ): int {
467 $this->checkDatabaseDomain( $dbw );
468 [ $userId, $userName ] = $this->validateActorForInsertion( $user );
470 try {
471 $dbw->newInsertQueryBuilder()
472 ->insertInto( 'actor' )
473 ->row( [ 'actor_user' => $userId, 'actor_name' => $userName ] )
474 ->caller( __METHOD__ )->execute();
475 } catch ( DBQueryError $e ) {
476 // We rely on the database to crash on unique actor_name constraint.
477 throw new CannotCreateActorException( $e->getMessage() );
479 $actorId = $dbw->insertId();
481 $this->attachActorId( $user, $actorId, true );
482 // Cache row we've just created
483 $cachedUserIdentity = $this->newActorFromRowFields( $userId, $userName, $actorId );
484 $this->setUpRollbackHandler( $dbw, $cachedUserIdentity, $user );
486 return $actorId;
490 * Attempt to assign an ID to an actor for a system user. If an actor ID already
491 * exists, return it.
493 * @note For reserved user names this method will overwrite the user ID of the
494 * existing anon actor.
496 * @note If called within a transaction, the returned ID might become invalid
497 * if the transaction is rolled back, so it should not be passed outside of the
498 * transaction context.
500 * @param UserIdentity $user
501 * @param IDatabase $dbw
502 * @return int actor ID greater then zero
503 * @throws CannotCreateActorException if the existing actor is associated with registered user.
504 * @internal for use in user account creation only.
506 public function acquireSystemActorId( UserIdentity $user, IDatabase $dbw ): int {
507 $this->checkDatabaseDomain( $dbw );
508 [ $userId, $userName ] = $this->validateActorForInsertion( $user );
510 $existingActorId = $this->findActorIdInternal( $userName, $dbw );
511 if ( $existingActorId ) {
512 // It certainly will be cached if we just found it.
513 $existingActor = $this->cache->getActor( ActorCache::KEY_ACTOR_ID, $existingActorId );
515 // If we already have an existing actor with a matching user ID
516 // just return it, nothing to do here.
517 if ( $existingActor->getId( $this->wikiId ) === $user->getId( $this->wikiId ) ) {
518 return $existingActorId;
521 // Allow overwriting user ID for already existing actor with reserved user name, see T236444
522 if ( $this->userNameUtils->isUsable( $userName ) || $existingActor->isRegistered() ) {
523 throw new CannotCreateActorException(
524 'Cannot replace user for existing actor: ' .
525 'actor_id={existingActorId}, new user_id={userId}',
526 [ 'existingActorId' => $existingActorId, 'userId' => $userId ]
530 $dbw->newInsertQueryBuilder()
531 ->insertInto( 'actor' )
532 ->row( [ 'actor_name' => $userName, 'actor_user' => $userId ] )
533 ->onDuplicateKeyUpdate()
534 ->uniqueIndexFields( [ 'actor_name' ] )
535 ->set( [ 'actor_user' => $userId ] )
536 ->caller( __METHOD__ )->execute();
537 if ( !$dbw->affectedRows() ) {
538 throw new CannotCreateActorException(
539 'Failed to replace user for actor: ' .
540 'actor_id={existingActorId}, new user_id={userId}',
541 [ 'existingActorId' => $existingActorId, 'userId' => $userId ]
544 $actorId = $dbw->insertId() ?: $existingActorId;
546 $this->cache->remove( $user );
547 $this->attachActorId( $user, $actorId, true );
548 // Cache row we've just created
549 $cachedUserIdentity = $this->newActorFromRowFields( $userId, $userName, $actorId );
550 $this->setUpRollbackHandler( $dbw, $cachedUserIdentity, $user );
551 return $actorId;
555 * Delete the actor from the actor table
557 * @warning this method does very limited validation and is extremely
558 * dangerous since it can break referential integrity of the database
559 * if used incorrectly. Use at your own risk!
561 * @since 1.37
562 * @param UserIdentity $actor
563 * @param IDatabase $dbw
564 * @return bool true on success, false if nothing was deleted.
566 public function deleteActor( UserIdentity $actor, IDatabase $dbw ): bool {
567 $this->checkDatabaseDomain( $dbw );
568 $this->deprecateInvalidCrossWikiParam( $actor );
570 $normalizedName = $this->normalizeUserName( $actor->getName() );
571 if ( $normalizedName === null ) {
572 throw new InvalidArgumentException(
573 "Unable to normalize the provided actor name {$actor->getName()}"
576 $dbw->newDeleteQueryBuilder()
577 ->deleteFrom( 'actor' )
578 ->where( [ 'actor_name' => $normalizedName ] )
579 ->caller( __METHOD__ )->execute();
580 if ( $dbw->affectedRows() !== 0 ) {
581 $this->cache->remove( $actor );
582 return true;
584 return false;
588 * Returns a canonical form of user name suitable for storage.
590 * @internal
591 * @param string $name
593 * @return string|null
595 public function normalizeUserName( string $name ): ?string {
596 if ( $this->userNameUtils->isIP( $name ) ) {
597 return IPUtils::sanitizeIP( $name );
598 } elseif ( ExternalUserNames::isExternal( $name ) ) {
599 // TODO: ideally, we should probably canonicalize external usernames,
600 // but it was not done before, so we can not start doing it unless we
601 // fix existing DB rows - T273933
602 return $name;
603 } else {
604 $normalized = $this->userNameUtils->getCanonical( $name );
605 return $normalized === false ? null : $normalized;
610 * Validates actor before insertion.
612 * @param UserIdentity $user
613 * @return array [ $normalizedUserId, $normalizedName ]
615 private function validateActorForInsertion( UserIdentity $user ): array {
616 // TODO: we want to assert this user belongs to the correct wiki,
617 // but User objects are always local and we used to use them
618 // on a non-local DB connection. We need to first deprecate this
619 // possibility and then throw on mismatching User object - T273972
620 // $user->assertWiki( $this->wikiId );
621 $this->deprecateInvalidCrossWikiParam( $user );
623 $userName = $this->normalizeUserName( $user->getName() );
624 if ( $userName === null || $userName === '' ) {
625 $userIdForErrorMessage = $user->getId( $this->wikiId );
626 throw new CannotCreateActorException(
627 'Cannot create an actor for a user with no name: ' .
628 'user_id={userId} user_name="{userName}"',
629 [ 'userId' => $userIdForErrorMessage, 'userName' => $user->getName() ]
633 $userId = $user->getId( $this->wikiId ) ?: null;
634 if ( $userId === null && $this->userNameUtils->isUsable( $user->getName() ) ) {
635 throw new CannotCreateActorException(
636 'Cannot create an actor for a usable name that is not an existing user: ' .
637 'user_name="{userName}"',
638 [ 'userName' => $user->getName() ]
642 if ( !$this->allowCreateIpActors && $this->userNameUtils->isIP( $userName ) ) {
643 throw new CannotCreateActorException(
644 'Cannot create an actor for an IP user when temporary accounts are enabled'
647 return [ $userId, $userName ];
651 * Clear in-process caches if transaction gets rolled back.
653 * @param IDatabase $dbw
654 * @param UserIdentity $cachedActor
655 * @param UserIdentity $originalActor
657 private function setUpRollbackHandler(
658 IDatabase $dbw,
659 UserIdentity $cachedActor,
660 UserIdentity $originalActor
662 if ( $dbw->trxLevel() ) {
663 // If called within a transaction and it was rolled back, the cached actor ID
664 // becomes invalid, so cache needs to be invalidated as well. See T277795.
665 $dbw->onTransactionResolution(
666 function ( int $trigger ) use ( $cachedActor, $originalActor ) {
667 if ( $trigger === IDatabase::TRIGGER_ROLLBACK ) {
668 $this->cache->remove( $cachedActor );
669 $this->detachActorId( $originalActor );
672 __METHOD__
678 * Throws an exception if the given database connection does not belong to the wiki this
679 * ActorStore is bound to.
681 * @param IReadableDatabase $db
683 private function checkDatabaseDomain( IReadableDatabase $db ) {
684 $dbDomain = $db->getDomainID();
685 $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
686 if ( $dbDomain !== $storeDomain ) {
687 throw new InvalidArgumentException(
688 "DB connection domain '$dbDomain' does not match '$storeDomain'"
694 * In case all reasonable attempts of initializing a proper actor from the
695 * database have failed, entities can be attributed to special 'Unknown user' actor.
697 * @return UserIdentity
699 public function getUnknownActor(): UserIdentity {
700 $actor = $this->getUserIdentityByName( self::UNKNOWN_USER_NAME );
701 if ( $actor ) {
702 return $actor;
704 $actor = new UserIdentityValue( 0, self::UNKNOWN_USER_NAME, $this->wikiId );
706 $db = $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId );
707 $this->acquireActorId( $actor, $db );
708 return $actor;
712 * Returns a specialized SelectQueryBuilder for querying the UserIdentity objects.
714 * @param IReadableDatabase|int $dbOrQueryFlags The database connection to perform the query on,
715 * or one of IDBAccessObject::READ_* constants.
716 * @return UserSelectQueryBuilder
718 public function newSelectQueryBuilder( $dbOrQueryFlags = IDBAccessObject::READ_NORMAL ): UserSelectQueryBuilder {
719 if ( $dbOrQueryFlags instanceof IReadableDatabase ) {
720 [ $db, $flags ] = [ $dbOrQueryFlags, IDBAccessObject::READ_NORMAL ];
721 $this->checkDatabaseDomain( $db );
722 } else {
723 if ( ( $dbOrQueryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
724 $db = $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId );
725 } else {
726 $db = $this->loadBalancer->getConnection( DB_REPLICA, [], $this->wikiId );
728 $flags = $dbOrQueryFlags;
731 $builder = new UserSelectQueryBuilder(
732 $db,
733 $this,
734 $this->tempUserConfig,
735 $this->hideUserUtils
737 return $builder->recency( $flags );
741 * @internal For use immediately after construction only
742 * @param bool $allow
744 public function setAllowCreateIpActors( bool $allow ): void {
745 $this->allowCreateIpActors = $allow;
749 * Emits a deprecation warning if $user does not belong to the
750 * same wiki this store belongs to.
752 * @param UserIdentity $user
754 private function deprecateInvalidCrossWikiParam( UserIdentity $user ) {
755 if ( $user->getWikiId() !== $this->wikiId ) {
756 $expected = $this->wikiIdToString( $user->getWikiId() );
757 $actual = $this->wikiIdToString( $this->wikiId );
758 wfDeprecatedMsg(
759 'Deprecated passing invalid cross-wiki user. ' .
760 "Expected: {$expected}, Actual: {$actual}.",
761 '1.37'
767 * Convert $wikiId to a string for logging.
769 * @param string|false $wikiId
770 * @return string
772 private function wikiIdToString( $wikiId ): string {
773 return $wikiId === WikiAwareEntity::LOCAL ? 'the local wiki' : "'{$wikiId}'";