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
416 lines
11 KiB
PHP
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 );
|
|
}
|
|
}
|