MediaWiki  master
ActorStore.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\User;
22 
26 use InvalidArgumentException;
27 use MapCacheLRU;
29 use Psr\Log\LoggerInterface;
30 use stdClass;
31 use User;
32 use Wikimedia\Assert\Assert;
33 use Wikimedia\IPUtils;
36 
44 
45  public const UNKNOWN_USER_NAME = 'Unknown user';
46 
47  private const LOCAL_CACHE_SIZE = 5;
48 
50  private $loadBalancer;
51 
53  private $userNameUtils;
54 
56  private $logger;
57 
59  private $wikiId;
60 
63 
65  private $actorsByUserId;
66 
68  private $actorsByName;
69 
76  public function __construct(
79  LoggerInterface $logger,
80  $wikiId = WikiAwareEntity::LOCAL
81  ) {
82  Assert::parameterType( 'string|boolean', $wikiId, '$wikiId' );
83  Assert::parameter( $wikiId !== true, '$wikiId', 'must be false or a string' );
84 
85  $this->loadBalancer = $loadBalancer;
86  $this->userNameUtils = $userNameUtils;
87  $this->logger = $logger;
88  $this->wikiId = $wikiId;
89 
90  $this->actorsByActorId = new MapCacheLRU( self::LOCAL_CACHE_SIZE );
91  $this->actorsByUserId = new MapCacheLRU( self::LOCAL_CACHE_SIZE );
92  $this->actorsByName = new MapCacheLRU( self::LOCAL_CACHE_SIZE );
93  }
94 
109  public function newActorFromRow( stdClass $row ): UserIdentity {
110  $actorId = (int)$row->actor_id;
111  $userId = isset( $row->actor_user ) ? (int)$row->actor_user : 0;
112  if ( $actorId === 0 ) {
113  throw new InvalidArgumentException( "Actor ID is 0 for {$row->actor_name} and {$userId}" );
114  }
115 
116  $normalizedName = $this->normalizeUserName( $row->actor_name );
117  if ( !$normalizedName ) {
118  $this->logger->warning( 'Encountered invalid actor name in database', [
119  'user_id' => $userId,
120  'actor_id' => $actorId,
121  'actor_name' => $row->actor_name,
122  'wiki_id' => $this->wikiId ?: 'local'
123  ] );
124  // TODO: once we have guaranteed db only contains valid actor names,
125  // we can skip normalization here - T273933
126  if ( $row->actor_name === '' ) {
127  throw new InvalidArgumentException( "Actor name can not be empty for {$userId} and {$actorId}" );
128  }
129  }
130 
131  $actor = new UserIdentityValue( $userId, $row->actor_name, $actorId, $this->wikiId );
132  $this->addUserIdentityToCache( $actorId, $actor );
133  return $actor;
134  }
135 
152  public function newActorFromRowFields( $userId, $name, $actorId ): UserIdentity {
153  // For backwards compatibility we are quite relaxed about what to accept,
154  // but try not to create entirely insane objects. As we move more code
155  // from ActorMigration aliases to proper join with the actor table,
156  // we should use ::newActorFromRow more, and eventually deprecate this method.
157  $userId = $userId === null ? 0 : (int)$userId;
158  $name = $name ?: '';
159  if ( $actorId === null ) {
160  throw new InvalidArgumentException( "Actor ID is null for {$name} and {$userId}" );
161  }
162  if ( (int)$actorId === 0 ) {
163  throw new InvalidArgumentException( "Actor ID is 0 for {$name} and {$userId}" );
164  }
165 
166  $normalizedName = $this->normalizeUserName( $name );
167  if ( !$normalizedName ) {
168  $this->logger->warning( 'Encountered invalid actor name in database', [
169  'user_id' => $userId,
170  'actor_id' => $actorId,
171  'actor_name' => $name,
172  'wiki_id' => $this->wikiId ?: 'local'
173  ] );
174  // TODO: once we have guaranteed the DB entries only exist for normalized names,
175  // we can skip normalization here - T273933
176  if ( $name === '' ) {
177  throw new InvalidArgumentException( "Actor name can not be empty for {$userId} and {$actorId}" );
178  }
179  }
180 
181  $actorId = (int)$actorId;
182  $actor = new UserIdentityValue(
183  $userId,
184  $name,
185  $actorId,
186  $this->wikiId
187  );
188 
189  $this->addUserIdentityToCache( $actorId, $actor );
190  return $actor;
191  }
192 
197  private function addUserIdentityToCache( int $actorId, UserIdentity $actor ) {
198  $this->actorsByActorId->set( $actorId, [ $actor, $actorId ] );
199  $userId = $actor->getId( $this->wikiId );
200  if ( $userId ) {
201  $this->actorsByUserId->set( $userId, [ $actor, $actorId ] );
202  }
203  $this->actorsByName->set( $actor->getName(), [ $actor, $actorId ] );
204  }
205 
214  public function getActorById( int $actorId, IDatabase $db ): ?UserIdentity {
215  $this->checkDatabaseDomain( $db );
216 
217  if ( !$actorId ) {
218  return null;
219  }
220 
221  $cachedValue = $this->actorsByActorId->get( $actorId );
222  if ( $cachedValue ) {
223  return $cachedValue[0];
224  }
225 
226  $actor = $this->newSelectQueryBuilder( $db )
227  ->caller( __METHOD__ )
228  ->conds( [ 'actor_id' => $actorId ] )
229  ->fetchUserIdentity();
230 
231  // The actor ID mostly comes from DB, so if we can't find an actor by ID,
232  // it's most likely due to lagged replica and not cause it doesn't actually exist.
233  // Probably we just inserted it? Try master.
234  if ( !$actor ) {
235  $actor = $this->newSelectQueryBuilderForQueryFlags( self::READ_LATEST )
236  ->caller( __METHOD__ )
237  ->conds( [ 'actor_id' => $actorId ] )
238  ->fetchUserIdentity();
239  }
240  return $actor;
241  }
242 
251  public function getUserIdentityByName( string $name, int $queryFlags = self::READ_NORMAL ): ?UserIdentity {
252  if ( $name === '' ) {
253  throw new InvalidArgumentException( 'Empty string passed as actor name' );
254  }
255 
256  $name = $this->normalizeUserName( $name );
257  if ( !$name ) {
258  throw new InvalidArgumentException( "Unable to normalize the provided actor name {$name}" );
259  }
260 
261  $cachedValue = $this->actorsByName->get( $name );
262  if ( $cachedValue ) {
263  return $cachedValue[0];
264  }
265 
266  return $this->newSelectQueryBuilderForQueryFlags( $queryFlags )
267  ->caller( __METHOD__ )
268  ->userNames( $name )
269  ->fetchUserIdentity();
270  }
271 
279  public function getUserIdentityByUserId( int $userId, int $queryFlags = self::READ_NORMAL ): ?UserIdentity {
280  if ( !$userId ) {
281  return null;
282  }
283 
284  $cachedValue = $this->actorsByUserId->get( $userId );
285  if ( $cachedValue ) {
286  return $cachedValue[0];
287  }
288 
289  return $this->newSelectQueryBuilderForQueryFlags( $queryFlags )
290  ->caller( __METHOD__ )
291  ->userIds( $userId )
292  ->fetchUserIdentity();
293  }
294 
303  private function attachActorId( UserIdentity $user, int $id ) {
304  if ( $user instanceof UserIdentityValue ) {
305  $user->setActorId( $id );
306  } elseif ( $user instanceof User ) {
307  $user->setActorId( $id );
308  }
309  }
310 
319  public function findActorId( UserIdentity $user, IDatabase $db ): ?int {
320  // TODO: we want to assert this user belongs to the correct wiki,
321  // but User objects are always local and we used to use them
322  // on a non-local DB connection. We need to first deprecate this
323  // possibility and then throw on mismatching User object - T273972
324  // $user->assertWiki( $this->wikiId );
325 
326  // TODO: In the future we would be able to assume UserIdentity name is ok
327  // and will be able to skip normalization here - T273933
328  $name = $this->normalizeUserName( $user->getName() );
329  if ( !$name ) {
330  $this->logger->warning( 'Encountered a UserIdentity with invalid name', [
331  'user_name' => $user->getName()
332  ] );
333  return null;
334  }
335 
336  $id = $this->findActorIdInternal( $name, $db );
337 
338  if ( $id ) {
339  $this->attachActorId( $user, $id );
340  }
341 
342  return $id;
343  }
344 
353  public function findActorIdByName( $name, IDatabase $db ): ?int {
354  // NOTE: $name may be user-supplied, need full normalization
355  $name = $this->normalizeUserName( $name, UserNameUtils::RIGOR_VALID );
356  if ( !$name ) {
357  return null;
358  }
359 
360  $id = $this->findActorIdInternal( $name, $db );
361 
362  return $id;
363  }
364 
374  private function findActorIdInternal(
375  string $name,
376  IDatabase $db,
377  array $queryOptions = []
378  ): ?int {
379  // Note: UserIdentity::getActorId will be deprecated and removed,
380  // and this is the replacement for it. Can't call User::getActorId, cause
381  // User always thinks it's local, so we could end up fetching the ID
382  // from the wrong database.
383 
384  $cachedValue = $this->actorsByName->get( $name );
385  if ( $cachedValue ) {
386  return $cachedValue[1];
387  }
388 
389  $row = $db->selectRow(
390  'actor',
391  [ 'actor_user', 'actor_name', 'actor_id' ],
392  [ 'actor_name' => $name ],
393  __METHOD__,
394  $queryOptions
395  );
396 
397  if ( !$row || !$row->actor_id ) {
398  return null;
399  }
400 
401  $id = (int)$row->actor_id;
402 
403  // to cache row
404  $this->newActorFromRow( $row );
405 
406  return $id;
407  }
408 
423  public function acquireActorId( UserIdentity $user, IDatabase $dbw = null ): int {
424  if ( $dbw ) {
425  $this->checkDatabaseDomain( $dbw );
426  } else {
427  // TODO: Remove after fixing it in all extensions and seeing it live for one train.
428  // Does not need full deprecation since this method is new in 1.36.
430  'Calling acquireActorId() without the $dbw parameter is deprecated',
431  '1.36'
432  );
433  [ $dbw, ] = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
434  }
435  // TODO: we want to assert this user belongs to the correct wiki,
436  // but User objects are always local and we used to use them
437  // on a non-local DB connection. We need to first deprecate this
438  // possibility and then throw on mismatching User object - T273972
439  // $user->assertWiki( $this->wikiId );
440 
441  $userName = $this->normalizeUserName( $user->getName() );
442  if ( $userName === null || $userName === '' ) {
443  $userIdForErrorMessage = $user->getId( $this->wikiId );
444  throw new CannotCreateActorException(
445  'Cannot create an actor for a user with no name: ' .
446  "user_id={$userIdForErrorMessage} user_name=\"{$user->getName()}\""
447  );
448  }
449 
450  // allow cache to be used, because if it is in the cache, it already has an actor ID
451  $existingActorId = $this->findActorIdInternal( $userName, $dbw );
452  if ( $existingActorId ) {
453  $this->attachActorId( $user, $existingActorId );
454  return $existingActorId;
455  }
456 
457  $userId = $user->getId( $this->wikiId ) ?: null;
458  if ( $userId === null && $this->userNameUtils->isUsable( $user->getName() ) ) {
459  throw new CannotCreateActorException(
460  'Cannot create an actor for a usable name that is not an existing user: ' .
461  "user_name=\"{$user->getName()}\""
462  );
463  }
464 
465  $dbw->insert(
466  'actor',
467  [
468  'actor_user' => $userId,
469  'actor_name' => $userName,
470  ],
471  __METHOD__,
472  [ 'IGNORE' ] );
473  if ( $dbw->affectedRows() ) {
474  $actorId = (int)$dbw->insertId();
475  } else {
476  // Outdated cache?
477  // Use LOCK IN SHARE MODE to bypass any MySQL REPEATABLE-READ snapshot.
478  $actorId = $this->findActorIdInternal(
479  $userName,
480  $dbw,
481  [ 'LOCK IN SHARE MODE' ]
482  );
483  if ( !$actorId ) {
484  throw new CannotCreateActorException(
485  "Failed to create actor ID for " .
486  "user_id={$userId} user_name=\"{$userName}\""
487  );
488  }
489  }
490 
491  // Cache row we've just created
492  $this->newActorFromRowFields( $userId, $userName, $actorId );
493  $this->attachActorId( $user, $actorId );
494 
495  return $actorId;
496  }
497 
507  public function normalizeUserName( string $name, $rigor = UserNameUtils::RIGOR_NONE ): ?string {
508  if ( $this->userNameUtils->isIP( $name ) ) {
509  return IPUtils::sanitizeIP( $name );
510  } elseif ( ExternalUserNames::isExternal( $name ) ) {
511  // TODO: ideally, we should probably canonicalize external usernames,
512  // but it was not done before, so we can not start doing it unless we
513  // fix existing DB rows - T273933
514  return $name;
515  } elseif ( $rigor !== UserNameUtils::RIGOR_NONE ) {
516  return $this->userNameUtils->getCanonical( $name, $rigor ) ?: null;
517  } else {
518  return $name;
519  }
520  }
521 
526  private function getDBConnectionRefForQueryFlags( int $queryFlags ): array {
527  [ $mode, $options ] = DBAccessObjectUtils::getDBOptions( $queryFlags );
528  return [ $this->loadBalancer->getConnectionRef( $mode, [], $this->wikiId ), $options ];
529  }
530 
537  private function checkDatabaseDomain( IDatabase $db ) {
538  $dbDomain = $db->getDomainID();
539  $storeDomain = $this->loadBalancer->resolveDomainID( $this->wikiId );
540  if ( $dbDomain !== $storeDomain ) {
541  throw new InvalidArgumentException(
542  "DB connection domain '$dbDomain' does not match '$storeDomain'"
543  );
544  }
545  }
546 
553  public function getUnknownActor(): UserIdentity {
554  $actor = $this->getUserIdentityByName( self::UNKNOWN_USER_NAME );
555  if ( $actor ) {
556  return $actor;
557  }
558  $actor = new UserIdentityValue( 0, self::UNKNOWN_USER_NAME, 0, $this->wikiId );
559 
560  [ $db, ] = $this->getDBConnectionRefForQueryFlags( self::READ_LATEST );
561  $this->acquireActorId( $actor, $db );
562  return $actor;
563  }
564 
572  [ $db, $options ] = $this->getDBConnectionRefForQueryFlags( $queryFlags );
573  $queryBuilder = $this->newSelectQueryBuilder( $db );
574  $queryBuilder->options( $options );
575  return $queryBuilder;
576  }
577 
587  public function newSelectQueryBuilder( IDatabase $db = null ): UserSelectQueryBuilder {
588  if ( $db ) {
589  $this->checkDatabaseDomain( $db );
590  } else {
591  [ $db, ] = $this->getDBConnectionRefForQueryFlags( self::READ_NORMAL );
592  }
593 
594  return new UserSelectQueryBuilder( $db, $this );
595  }
596 }
MediaWiki\User\UserIdentityValue
Value object representing a user's identity.
Definition: UserIdentityValue.php:37
MediaWiki\User\ActorStore\__construct
__construct(ILoadBalancer $loadBalancer, UserNameUtils $userNameUtils, LoggerInterface $logger, $wikiId=WikiAwareEntity::LOCAL)
Definition: ActorStore.php:76
MediaWiki\User\ActorStore\newActorFromRowFields
newActorFromRowFields( $userId, $name, $actorId)
Instantiate a new UserIdentity object based on field values from a DB row.
Definition: ActorStore.php:152
if
if(ini_get( 'mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Definition: Setup.php:87
ExternalUserNames
Class to parse and build external user names.
Definition: ExternalUserNames.php:29
MediaWiki\User\UserIdentity\getId
getId( $wikiId=self::LOCAL)
DBAccessObjectUtils\getDBOptions
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
Definition: DBAccessObjectUtils.php:52
MediaWiki\User\ActorStore\newSelectQueryBuilderForQueryFlags
newSelectQueryBuilderForQueryFlags( $queryFlags)
Returns a specialized SelectQueryBuilder for querying the UserIdentity objects.
Definition: ActorStore.php:571
MediaWiki\User\ActorStore\findActorIdInternal
findActorIdInternal(string $name, IDatabase $db, array $queryOptions=[])
Find actor_id of the given $user using the passed $db connection.
Definition: ActorStore.php:374
MediaWiki\DAO\WikiAwareEntity
Marker interface for entities aware of the wiki they belong to.
Definition: WikiAwareEntity.php:34
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
MediaWiki\User\ActorStore\normalizeUserName
normalizeUserName(string $name, $rigor=UserNameUtils::RIGOR_NONE)
Returns a canonical form of user name suitable for storage.
Definition: ActorStore.php:507
wfDeprecatedMsg
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
Definition: GlobalFunctions.php:1066
MediaWiki\User\ActorStore\$loadBalancer
ILoadBalancer $loadBalancer
Definition: ActorStore.php:50
MediaWiki\User\ActorStore\findActorIdByName
findActorIdByName( $name, IDatabase $db)
Find the actor_id of the given $name.
Definition: ActorStore.php:353
MediaWiki\User\ActorStore\$wikiId
string false $wikiId
Definition: ActorStore.php:59
MapCacheLRU
Handles a simple LRU key/value map with a maximum number of entries.
Definition: MapCacheLRU.php:37
MediaWiki\User\UserIdentity\getName
getName()
MediaWiki\User\ActorStore\$actorsByName
MapCacheLRU $actorsByName
string user name => [ UserIdentity, int actor ID ]
Definition: ActorStore.php:68
MediaWiki\User\ActorStore\getDBConnectionRefForQueryFlags
getDBConnectionRefForQueryFlags(int $queryFlags)
Definition: ActorStore.php:526
DBAccessObjectUtils
Helper class for DAO classes.
Definition: DBAccessObjectUtils.php:29
MediaWiki\User\ActorStore\UNKNOWN_USER_NAME
const UNKNOWN_USER_NAME
Definition: ActorStore.php:45
MediaWiki\User\ActorStore\LOCAL_CACHE_SIZE
const LOCAL_CACHE_SIZE
Definition: ActorStore.php:47
MediaWiki\User\ActorStore\getUserIdentityByName
getUserIdentityByName(string $name, int $queryFlags=self::READ_NORMAL)
Find an actor by $name.
Definition: ActorStore.php:251
MediaWiki\User\UserIdentityLookup
Definition: UserIdentityLookup.php:33
Wikimedia\Rdbms\IDatabase\selectRow
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
MediaWiki\User\ActorStore\newSelectQueryBuilder
newSelectQueryBuilder(IDatabase $db=null)
Returns a specialized SelectQueryBuilder for querying the UserIdentity objects.
Definition: ActorStore.php:587
Wikimedia\Rdbms\IDatabase\getDomainID
getDomainID()
Return the currently selected domain ID.
MediaWiki\User\ActorStore\$actorsByUserId
MapCacheLRU $actorsByUserId
int user ID => [ UserIdentity, int actor ID ]
Definition: ActorStore.php:65
CannotCreateActorException
Exception thrown when an actor can't be created.
Definition: CannotCreateActorException.php:29
MediaWiki\User
Definition: ActorNormalization.php:21
MediaWiki\User\ActorStore\attachActorId
attachActorId(UserIdentity $user, int $id)
Attach the actor ID to $user for backwards compatibility.
Definition: ActorStore.php:303
MediaWiki\User\ActorStore\getUnknownActor
getUnknownActor()
In case all reasonable attempts of initializing a proper actor from the database have failed,...
Definition: ActorStore.php:553
MediaWiki\User\ActorStore\$logger
LoggerInterface $logger
Definition: ActorStore.php:56
MediaWiki\User\UserSelectQueryBuilder
Definition: UserSelectQueryBuilder.php:29
MediaWiki\User\UserNameUtils
UserNameUtils service.
Definition: UserNameUtils.php:42
MediaWiki\User\ActorStore\$actorsByActorId
MapCacheLRU $actorsByActorId
int actor ID => [ UserIdentity, int actor ID ]
Definition: ActorStore.php:62
MediaWiki\User\ActorStore\getActorById
getActorById(int $actorId, IDatabase $db)
Find an actor by $id.
Definition: ActorStore.php:214
MediaWiki\User\ActorStore\checkDatabaseDomain
checkDatabaseDomain(IDatabase $db)
Throws an exception if the given database connection does not belong to the wiki this RevisionStore i...
Definition: ActorStore.php:537
MediaWiki\User\ActorStore\$userNameUtils
UserNameUtils $userNameUtils
Definition: ActorStore.php:53
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:66
MediaWiki\User\ActorStore\acquireActorId
acquireActorId(UserIdentity $user, IDatabase $dbw=null)
Attempt to assign an actor ID to the given $user If it is already assigned, return the existing ID.
Definition: ActorStore.php:423
MediaWiki\User\ActorStore\addUserIdentityToCache
addUserIdentityToCache(int $actorId, UserIdentity $actor)
Definition: ActorStore.php:197
ExternalUserNames\isExternal
static isExternal( $username)
Tells whether the username is external or not.
Definition: ExternalUserNames.php:147
MediaWiki\User\ActorStore\findActorId
findActorId(UserIdentity $user, IDatabase $db)
Find the actor_id of the given $user.
Definition: ActorStore.php:319
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
MediaWiki\User\ActorStore
Definition: ActorStore.php:43
MediaWiki\User\ActorStore\newActorFromRow
newActorFromRow(stdClass $row)
Instantiate a new UserIdentity object based on a $row from the actor table.
Definition: ActorStore.php:109
MediaWiki\User\ActorStore\getUserIdentityByUserId
getUserIdentityByUserId(int $userId, int $queryFlags=self::READ_NORMAL)
Find an actor by $userId.
Definition: ActorStore.php:279
MediaWiki\User\ActorNormalization
Service for dealing with the actor table.
Definition: ActorNormalization.php:32