wiki.techinc.nl/includes/user/UserFactory.php
Amir Sarabadani c04f1d64d6 Remove IDBAccessObject from being implemented in many classes
This is inconsistent with the access pattern of other constants in
MediaWiki. it's also confusing (e.g. it's unclear to a newcomer why
UserFactory is implementing IDBAccessObject) and it's prone to clashes
(e.g. BagOStuff class has a clashing constant).

It has been already announced: https://w.wiki/9DAX

Bug: T354194
Change-Id: Ic2357634b8385d65b55db2b557191419b06c40e0
2024-02-19 10:50:02 +01:00

416 lines
11 KiB
PHP

<?php
/**
* Factory for creating User objects without static coupling.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
namespace MediaWiki\User;
use DBAccessObjectUtils;
use IDBAccessObject;
use InvalidArgumentException;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\MainConfigNames;
use MediaWiki\Permissions\Authority;
use stdClass;
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\Rdbms\ILBFactory;
use Wikimedia\Rdbms\ILoadBalancer;
/**
* Creates User objects.
*
* @since 1.35
*/
class UserFactory implements UserRigorOptions {
/**
* RIGOR_* constants are inherited from UserRigorOptions
* READ_* constants are inherited from IDBAccessObject
*/
/** @internal */
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::SharedDB,
MainConfigNames::SharedTables,
];
/** @var ServiceOptions */
private $options;
/** @var ILBFactory */
private $loadBalancerFactory;
/** @var ILoadBalancer */
private $loadBalancer;
/** @var UserNameUtils */
private $userNameUtils;
/** @var User|null */
private $lastUserFromIdentity = null;
/**
* @param ServiceOptions $options
* @param ILBFactory $loadBalancerFactory
* @param UserNameUtils $userNameUtils
*/
public function __construct(
ServiceOptions $options,
ILBFactory $loadBalancerFactory,
UserNameUtils $userNameUtils
) {
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->loadBalancerFactory = $loadBalancerFactory;
$this->loadBalancer = $loadBalancerFactory->getMainLB();
$this->userNameUtils = $userNameUtils;
}
/**
* Factory method for creating users by name, replacing static User::newFromName
*
* This is slightly less efficient than newFromId(), so use newFromId() if
* you have both an ID and a name handy.
*
* @note unlike User::newFromName, this returns null instead of false for invalid usernames
*
* @since 1.35
* @since 1.36 returns null instead of false for invalid user names
*
* @param string $name Username, validated by Title::newFromText
* @param string $validate Validation strategy, one of the RIGOR_* constants. For no
* validation, use RIGOR_NONE.
* @return ?User User object, or null if the username is invalid (e.g. if it contains
* illegal characters or is an IP address). If the username is not present in the database,
* the result will be a user object with a name, a user id of 0, and default settings.
*/
public function newFromName(
string $name,
string $validate = self::RIGOR_VALID
): ?User {
// RIGOR_* constants are the same here and in the UserNameUtils class
$canonicalName = $this->userNameUtils->getCanonical( $name, $validate );
if ( $canonicalName === false ) {
return null;
}
$user = new User();
$user->mName = $canonicalName;
$user->mFrom = 'name';
$user->setItemLoaded( 'name' );
return $user;
}
/**
* Returns a new anonymous User based on ip.
*
* @since 1.35
*
* @param string|null $ip IP address
* @return User
*/
public function newAnonymous( ?string $ip = null ): User {
if ( $ip ) {
if ( !$this->userNameUtils->isIP( $ip ) ) {
throw new InvalidArgumentException( 'Invalid IP address' );
}
$user = new User();
$user->setName( $ip );
} else {
$user = new User();
}
return $user;
}
/**
* Factory method for creation from a given user ID, replacing User::newFromId
*
* @since 1.35
*
* @param int $id Valid user ID
* @return User
*/
public function newFromId( int $id ): User {
$user = new User();
$user->mId = $id;
$user->mFrom = 'id';
$user->setItemLoaded( 'id' );
return $user;
}
/**
* Factory method for creation from a given actor ID, replacing User::newFromActorId
*
* @since 1.35
*
* @param int $actorId
* @return User
*/
public function newFromActorId( int $actorId ): User {
$user = new User();
$user->mActorId = $actorId;
$user->mFrom = 'actor';
$user->setItemLoaded( 'actor' );
return $user;
}
/**
* Factory method for creation fom a given UserIdentity, replacing User::newFromIdentity
*
* @since 1.35
*
* @param UserIdentity $userIdentity
* @return User
*/
public function newFromUserIdentity( UserIdentity $userIdentity ): User {
if ( $userIdentity instanceof User ) {
return $userIdentity;
}
$id = $userIdentity->getId();
$name = $userIdentity->getName();
// Cache the $userIdentity we converted last. This avoids redundant conversion
// in cases where we would be converting the same UserIdentity over and over,
// for instance because we need to access data preferences when formatting
// timestamps in a listing.
if (
$this->lastUserFromIdentity
&& $this->lastUserFromIdentity->getId() === $id
&& $this->lastUserFromIdentity->getName() === $name
) {
return $this->lastUserFromIdentity;
}
$this->lastUserFromIdentity = $this->newFromAnyId(
$id === 0 ? null : $id,
$name === '' ? null : $name,
null
);
return $this->lastUserFromIdentity;
}
/**
* Factory method for creation from an ID, name, and/or actor ID, replacing User::newFromAnyId
*
* @note This does not check that the ID, name, and actor ID all correspond to
* the same user.
*
* @since 1.35
*
* @param ?int $userId
* @param ?string $userName
* @param ?int $actorId
* @param string|false $dbDomain
* @return User
* @throws InvalidArgumentException if none of userId, userName, and actorId are specified
*/
public function newFromAnyId(
?int $userId,
?string $userName,
?int $actorId = null,
$dbDomain = false
): User {
// Stop-gap solution for the problem described in T222212.
// Force the User ID and Actor ID to zero for users loaded from the database
// of another wiki, to prevent subtle data corruption and confusing failure modes.
if ( $dbDomain !== false ) {
$userId = 0;
$actorId = 0;
}
$user = new User;
$user->mFrom = 'defaults';
if ( $actorId !== null ) {
$user->mActorId = $actorId;
if ( $actorId !== 0 ) {
$user->mFrom = 'actor';
}
$user->setItemLoaded( 'actor' );
}
if ( $userName !== null && $userName !== '' ) {
$user->mName = $userName;
$user->mFrom = 'name';
$user->setItemLoaded( 'name' );
}
if ( $userId !== null ) {
$user->mId = $userId;
if ( $userId !== 0 ) {
$user->mFrom = 'id';
}
$user->setItemLoaded( 'id' );
}
if ( $user->mFrom === 'defaults' ) {
throw new InvalidArgumentException(
'Cannot create a user with no name, no ID, and no actor ID'
);
}
return $user;
}
/**
* Factory method to fetch the user for a given email confirmation code, replacing User::newFromConfirmationCode
*
* This code is generated when an account is created or its e-mail address has changed.
* If the code is invalid or has expired, returns null.
*
* @since 1.35
*
* @param string $confirmationCode
* @param int $flags
* @return User|null
*/
public function newFromConfirmationCode(
string $confirmationCode,
int $flags = IDBAccessObject::READ_NORMAL
) {
[ $index, ] = DBAccessObjectUtils::getDBOptions( $flags );
$db = $this->loadBalancer->getConnectionRef( $index );
$id = $db->newSelectQueryBuilder()
->select( 'user_id' )
->from( 'user' )
->where( [ 'user_email_token' => md5( $confirmationCode ) ] )
->andWhere( $db->expr( 'user_email_token_expires', '>', $db->timestamp() ) )
->recency( $flags )
->caller( __METHOD__ )->fetchField();
if ( !$id ) {
return null;
}
return $this->newFromId( (int)$id );
}
/**
* @see User::newFromRow
*
* @since 1.36
*
* @param stdClass $row A row from the user table
* @param array|null $data Further data to load into the object
* @return User
*/
public function newFromRow( $row, $data = null ) {
return User::newFromRow( $row, $data );
}
/**
* @internal for transition from User to Authority as performer concept.
* @param Authority $authority
* @return User
*/
public function newFromAuthority( Authority $authority ): User {
if ( $authority instanceof User ) {
return $authority;
}
return $this->newFromUserIdentity( $authority->getUser() );
}
/**
* Create a placeholder user for an anonymous user who will be upgraded to
* a temporary user. This will throw an exception if temp user autocreation
* is disabled.
*
* @since 1.39
* @return User
*/
public function newTempPlaceholder() {
$user = new User();
$user->setName( $this->userNameUtils->getTempPlaceholder() );
return $user;
}
/**
* Create an unsaved temporary user with a previously acquired name or a placeholder name.
*
* @since 1.39
* @param ?string $name If null, a placeholder name is used
* @return User
*/
public function newUnsavedTempUser( ?string $name ) {
$user = new User();
$user->setName( $name ?? $this->userNameUtils->getTempPlaceholder() );
return $user;
}
/**
* Purge user related caches, "touch" the user table to invalidate further caches
* @since 1.41
* @param UserIdentity $userIdentity
*/
public function invalidateCache( UserIdentity $userIdentity ) {
if ( !$userIdentity->isRegistered() ) {
return;
}
$wikiId = $userIdentity->getWikiId();
if ( $wikiId === UserIdentity::LOCAL ) {
$legacyUser = $this->newFromUserIdentity( $userIdentity );
// Update user_touched within User class to manage state of User::mTouched for CAS check
$legacyUser->invalidateCache();
} else {
// cross-wiki invalidation
$userId = $userIdentity->getId( $wikiId );
$dbw = $this->getUserTableConnection( ILoadBalancer::DB_PRIMARY, $wikiId );
$dbw->newUpdateQueryBuilder()
->update( 'user' )
->set( [ 'user_touched' => $dbw->timestamp() ] )
->where( [ 'user_id' => $userId ] )
->caller( __METHOD__ )->execute();
$dbw->onTransactionPreCommitOrIdle(
static function () use ( $wikiId, $userId ) {
User::purge( $wikiId, $userId );
},
__METHOD__
);
}
}
/**
* @param int $mode
* @param string|false $wikiId
* @return IDatabase
*/
private function getUserTableConnection( $mode, $wikiId ) {
if ( is_string( $wikiId ) && $this->loadBalancerFactory->getLocalDomainID() === $wikiId ) {
$wikiId = UserIdentity::LOCAL;
}
if ( $this->options->get( MainConfigNames::SharedDB ) &&
in_array( 'user', $this->options->get( MainConfigNames::SharedTables ) )
) {
// The main LB is aliased for the shared database in Setup.php
$lb = $this->loadBalancer;
} else {
$lb = $this->loadBalancerFactory->getMainLB( $wikiId );
}
return $lb->getConnection( $mode, [], $wikiId );
}
}