/**
* @const int Serialized record version.
*/
- const VERSION = 10;
+ const VERSION = 11;
/**
* Exclude user options that are set to their default value.
'mRegistration',
'mEditCount',
// user_groups table
- 'mGroups',
+ 'mGroupMemberships',
// user_properties table
'mOptionOverrides',
];
protected $mRegistration;
/** @var int */
protected $mEditCount;
- /** @var array */
- public $mGroups;
+ /**
+ * @var array No longer used since 1.29; use User::getGroups() instead
+ * @deprecated since 1.29
+ */
+ private $mGroups;
+ /** @var array Associative array of (group name => UserGroupMembership object) */
+ protected $mGroupMemberships;
/** @var array */
protected $mOptionOverrides;
// @}
/** @var array */
public $mOptions;
- /**
- * @var WebRequest
- */
+ /** @var WebRequest */
private $mRequest;
/** @var Block */
protected $queryFlagsUsed = self::READ_NORMAL;
/** @var string Indicates type of block (used for eventlogging)
- * Permitted values: 'cookie-block', 'proxy-block', 'openproxy-block', 'xff-block'
+ * Permitted values: 'cookie-block', 'proxy-block', 'openproxy-block', 'xff-block',
+ * 'config-block'
*/
public $blockTrigger = false;
return $cache->makeGlobalKey( 'user', 'id', wfWikiID(), $this->mId );
}
+ /**
+ * @param WANObjectCache $cache
+ * @return string[]
+ * @since 1.28
+ */
+ public function getMutableCacheKeys( WANObjectCache $cache ) {
+ $id = $this->getId();
+
+ return $id ? [ $this->getCacheKey( $cache ) ] : [];
+ }
+
/**
* Load user data from shared cache, given mId has already been set.
*
$this->mEmailToken = '';
$this->mEmailTokenExpires = null;
$this->mRegistration = wfTimestamp( TS_MW );
- $this->mGroups = [];
+ $this->mGroupMemberships = [];
Hooks::run( 'UserLoadDefaults', [ $this, $name ] );
}
if ( $s !== false ) {
// Initialise user table data
$this->loadFromRow( $s );
- $this->mGroups = null; // deferred
+ $this->mGroupMemberships = null; // deferred
$this->getEditCount(); // revalidation for nulls
return true;
} else {
* @param stdClass $row Row from the user table to load.
* @param array $data Further user data to load into the object
*
- * user_groups Array with groups out of the user_groups table
- * user_properties Array with properties out of the user_properties table
+ * user_groups Array of arrays or stdClass result rows out of the user_groups
+ * table. Previously you were supposed to pass an array of strings
+ * here, but we also need expiry info nowadays, so an array of
+ * strings is ignored.
+ * user_properties Array with properties out of the user_properties table
*/
protected function loadFromRow( $row, $data = null ) {
$all = true;
- $this->mGroups = null; // deferred
+ $this->mGroupMemberships = null; // deferred
if ( isset( $row->user_name ) ) {
$this->mName = $row->user_name;
if ( is_array( $data ) ) {
if ( isset( $data['user_groups'] ) && is_array( $data['user_groups'] ) ) {
- $this->mGroups = $data['user_groups'];
+ if ( !count( $data['user_groups'] ) ) {
+ $this->mGroupMemberships = [];
+ } else {
+ $firstGroup = reset( $data['user_groups'] );
+ if ( is_array( $firstGroup ) || is_object( $firstGroup ) ) {
+ $this->mGroupMemberships = [];
+ foreach ( $data['user_groups'] as $row ) {
+ $ugm = UserGroupMembership::newFromRow( (object)$row );
+ $this->mGroupMemberships[$ugm->getGroup()] = $ugm;
+ }
+ }
+ }
}
if ( isset( $data['user_properties'] ) && is_array( $data['user_properties'] ) ) {
$this->loadOptions( $data['user_properties'] );
* Load the groups from the database if they aren't already loaded.
*/
private function loadGroups() {
- if ( is_null( $this->mGroups ) ) {
+ if ( is_null( $this->mGroupMemberships ) ) {
$db = ( $this->queryFlagsUsed & self::READ_LATEST )
? wfGetDB( DB_MASTER )
: wfGetDB( DB_REPLICA );
- $res = $db->select( 'user_groups',
- [ 'ug_group' ],
- [ 'ug_user' => $this->mId ],
- __METHOD__ );
- $this->mGroups = [];
- foreach ( $res as $row ) {
- $this->mGroups[] = $row->ug_group;
- }
+ $this->mGroupMemberships = UserGroupMembership::getMembershipsForUser(
+ $this->mId, $db );
}
}
$this->mRights = null;
$this->mEffectiveGroups = null;
$this->mImplicitGroups = null;
- $this->mGroups = null;
+ $this->mGroupMemberships = null;
$this->mOptions = null;
$this->mOptionsLoaded = false;
$this->mEditCount = null;
* Check when actually saving should be done against master.
*/
private function getBlockedStatus( $bFromSlave = true ) {
- global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff;
+ global $wgProxyWhitelist, $wgUser, $wgApplyIpBlocksToXff, $wgSoftBlockRanges;
if ( -1 != $this->mBlockedby ) {
return;
}
}
+ if ( !$block instanceof Block
+ && $ip !== null
+ && $this->isAnon()
+ && IP::isInRanges( $ip, $wgSoftBlockRanges )
+ ) {
+ $block = new Block( [
+ 'address' => $ip,
+ 'byText' => 'MediaWiki default',
+ 'reason' => wfMessage( 'softblockrangesreason', $ip )->text(),
+ 'anonOnly' => true,
+ 'systemBlock' => 'wgSoftBlockRanges',
+ ] );
+ $this->blockTrigger = 'config-block';
+ }
+
if ( $block instanceof Block ) {
wfDebug( __METHOD__ . ": Found block.\n" );
$this->mBlock = $block;
$this->blockTrigger = false;
}
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
// Extensions
- Hooks::run( 'GetBlockedStatus', [ &$this ] );
+ Hooks::run( 'GetBlockedStatus', [ &$user ] );
}
/**
* @return bool True if a rate limiter was tripped
*/
public function pingLimiter( $action = 'edit', $incrBy = 1 ) {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
// Call the 'PingLimiter' hook
$result = false;
- if ( !Hooks::run( 'PingLimiter', [ &$this, $action, &$result, $incrBy ] ) ) {
+ if ( !Hooks::run( 'PingLimiter', [ &$user, $action, &$result, $incrBy ] ) ) {
return $result;
}
} elseif ( !$ip ) {
$ip = $this->getRequest()->getIP();
}
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
$blocked = false;
$block = null;
- Hooks::run( 'UserIsBlockedGlobally', [ &$this, $ip, &$blocked, &$block ] );
+ Hooks::run( 'UserIsBlockedGlobally', [ &$user, $ip, &$blocked, &$block ] );
if ( $blocked && $block === null ) {
// back-compat: UserIsBlockedGlobally didn't have $block param first
if ( $this->mLocked !== null ) {
return $this->mLocked;
}
- $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
+ $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
$this->mLocked = $authUser && $authUser->isLocked();
Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
return $this->mLocked;
}
$this->getBlockedStatus();
if ( !$this->mHideName ) {
- $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
+ $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$user ], null );
$this->mHideName = $authUser && $authUser->isHidden();
Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
}
* @return array
*/
public function getNewMessageLinks() {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
$talks = [];
- if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$this, &$talks ] ) ) {
+ if ( !Hooks::run( 'UserRetrieveNewTalks', [ &$user, &$talks ] ) ) {
return $talks;
} elseif ( !$this->getNewtalk() ) {
return [];
public function getGroups() {
$this->load();
$this->loadGroups();
- return $this->mGroups;
+ return array_keys( $this->mGroupMemberships );
+ }
+
+ /**
+ * Get the list of explicit group memberships this user has, stored as
+ * UserGroupMembership objects. Implicit groups are not included.
+ *
+ * @return array Associative array of (group name as string => UserGroupMembership object)
+ * @since 1.29
+ */
+ public function getGroupMemberships() {
+ $this->load();
+ $this->loadGroups();
+ return $this->mGroupMemberships;
}
/**
$this->getGroups(), // explicit groups
$this->getAutomaticGroups( $recache ) // implicit groups
) );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
// Hook for additional groups
- Hooks::run( 'UserEffectiveGroups', [ &$this, &$this->mEffectiveGroups ] );
+ Hooks::run( 'UserEffectiveGroups', [ &$user, &$this->mEffectiveGroups ] );
// Force reindexation of groups when a hook has unset one of them
$this->mEffectiveGroups = array_values( array_unique( $this->mEffectiveGroups ) );
}
}
/**
- * Add the user to the given group.
- * This takes immediate effect.
+ * Add the user to the given group. This takes immediate effect.
+ * If the user is already in the group, the expiry time will be updated to the new
+ * expiry time. (If $expiry is omitted or null, the membership will be altered to
+ * never expire.)
+ *
* @param string $group Name of the group to add
+ * @param string $expiry Optional expiry timestamp in any format acceptable to
+ * wfTimestamp(), or null if the group assignment should not expire
* @return bool
*/
- public function addGroup( $group ) {
+ public function addGroup( $group, $expiry = null ) {
$this->load();
+ $this->loadGroups();
+
+ if ( $expiry ) {
+ $expiry = wfTimestamp( TS_MW, $expiry );
+ }
- if ( !Hooks::run( 'UserAddGroup', [ $this, &$group ] ) ) {
+ if ( !Hooks::run( 'UserAddGroup', [ $this, &$group, &$expiry ] ) ) {
return false;
}
- $dbw = wfGetDB( DB_MASTER );
- if ( $this->getId() ) {
- $dbw->insert( 'user_groups',
- [
- 'ug_user' => $this->getId(),
- 'ug_group' => $group,
- ],
- __METHOD__,
- [ 'IGNORE' ] );
+ // create the new UserGroupMembership and put it in the DB
+ $ugm = new UserGroupMembership( $this->mId, $group, $expiry );
+ if ( !$ugm->insert( true ) ) {
+ return false;
}
- $this->loadGroups();
- $this->mGroups[] = $group;
- // In case loadGroups was not called before, we now have the right twice.
- // Get rid of the duplicate.
- $this->mGroups = array_unique( $this->mGroups );
+ $this->mGroupMemberships[$group] = $ugm;
// Refresh the groups caches, and clear the rights cache so it will be
// refreshed on the next call to $this->getRights().
*/
public function removeGroup( $group ) {
$this->load();
+
if ( !Hooks::run( 'UserRemoveGroup', [ $this, &$group ] ) ) {
return false;
}
- $dbw = wfGetDB( DB_MASTER );
- $dbw->delete( 'user_groups',
- [
- 'ug_user' => $this->getId(),
- 'ug_group' => $group,
- ], __METHOD__
- );
- // Remember that the user was in this group
- $dbw->insert( 'user_former_groups',
- [
- 'ufg_user' => $this->getId(),
- 'ufg_group' => $group,
- ],
- __METHOD__,
- [ 'IGNORE' ]
- );
+ $ugm = UserGroupMembership::getMembership( $this->mId, $group );
+ // delete the membership entry
+ if ( !$ugm || !$ugm->delete() ) {
+ return false;
+ }
$this->loadGroups();
- $this->mGroups = array_diff( $this->mGroups, [ $group ] );
+ unset( $this->mGroupMemberships[$group] );
// Refresh the groups caches, and clear the rights cache so it will be
// refreshed on the next call to $this->getRights().
// If we're working on user's talk page, we should update the talk page message indicator
if ( $title->getNamespace() == NS_USER_TALK && $title->getText() == $this->getName() ) {
- if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$this, $oldid ] ) ) {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
+ if ( !Hooks::run( 'UserClearNewTalkNotification', [ &$user, $oldid ] ) ) {
return;
}
* Log this user out.
*/
public function logout() {
- if ( Hooks::run( 'UserLogout', [ &$this ] ) ) {
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
+ if ( Hooks::run( 'UserLogout', [ &$user ] ) ) {
$this->doLogout();
}
}
return false;
}
$canSend = $this->isEmailConfirmed();
- Hooks::run( 'UserCanSendEmail', [ &$this, &$canSend ] );
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
+ Hooks::run( 'UserCanSendEmail', [ &$user, &$canSend ] );
return $canSend;
}
public function isEmailConfirmed() {
global $wgEmailAuthentication;
$this->load();
+ // Avoid PHP 7.1 warning of passing $this by reference
+ $user = $this;
$confirmed = true;
- if ( Hooks::run( 'EmailConfirmed', [ &$this, &$confirmed ] ) ) {
+ if ( Hooks::run( 'EmailConfirmed', [ &$user, &$confirmed ] ) ) {
if ( $this->isAnon() ) {
return false;
}
/**
* Get the localized descriptive name for a group, if it exists
+ * @deprecated since 1.29 Use UserGroupMembership::getGroupName instead
*
* @param string $group Internal group name
* @return string Localized descriptive group name
*/
public static function getGroupName( $group ) {
- $msg = wfMessage( "group-$group" );
- return $msg->isBlank() ? $group : $msg->text();
+ wfDeprecated( __METHOD__, '1.29' );
+ return UserGroupMembership::getGroupName( $group );
}
/**
* Get the localized descriptive name for a member of a group, if it exists
+ * @deprecated since 1.29 Use UserGroupMembership::getGroupMemberName instead
*
* @param string $group Internal group name
* @param string $username Username for gender (since 1.19)
* @return string Localized name for group member
*/
public static function getGroupMember( $group, $username = '#' ) {
- $msg = wfMessage( "group-$group-member", $username );
- return $msg->isBlank() ? $group : $msg->text();
+ wfDeprecated( __METHOD__, '1.29' );
+ return UserGroupMembership::getGroupMemberName( $group, $username );
}
/**
/**
* Get the title of a page describing a particular group
+ * @deprecated since 1.29 Use UserGroupMembership::getGroupPage instead
*
* @param string $group Internal group name
* @return Title|bool Title of the page if it exists, false otherwise
*/
public static function getGroupPage( $group ) {
- $msg = wfMessage( 'grouppage-' . $group )->inContentLanguage();
- if ( $msg->exists() ) {
- $title = Title::newFromText( $msg->text() );
- if ( is_object( $title ) ) {
- return $title;
- }
- }
- return false;
+ wfDeprecated( __METHOD__, '1.29' );
+ return UserGroupMembership::getGroupPage( $group );
}
/**
* Create a link to the group in HTML, if available;
* else return the group name.
+ * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
+ * make the link yourself if you need custom text
*
* @param string $group Internal name of the group
* @param string $text The text of the link
* @return string HTML link to the group
*/
public static function makeGroupLinkHTML( $group, $text = '' ) {
+ wfDeprecated( __METHOD__, '1.29' );
+
if ( $text == '' ) {
- $text = self::getGroupName( $group );
+ $text = UserGroupMembership::getGroupName( $group );
}
- $title = self::getGroupPage( $group );
+ $title = UserGroupMembership::getGroupPage( $group );
if ( $title ) {
return Linker::link( $title, htmlspecialchars( $text ) );
} else {
/**
* Create a link to the group in Wikitext, if available;
* else return the group name.
+ * @deprecated since 1.29 Use UserGroupMembership::getLink instead, or
+ * make the link yourself if you need custom text
*
* @param string $group Internal name of the group
* @param string $text The text of the link
* @return string Wikilink to the group
*/
public static function makeGroupLinkWiki( $group, $text = '' ) {
+ wfDeprecated( __METHOD__, '1.29' );
+
if ( $text == '' ) {
- $text = self::getGroupName( $group );
+ $text = UserGroupMembership::getGroupName( $group );
}
- $title = self::getGroupPage( $group );
+ $title = UserGroupMembership::getGroupPage( $group );
if ( $title ) {
$page = $title->getFullText();
return "[[$page|$text]]";
return $msg->isDisabled() ? $grant : $msg->text();
}
- /**
- * Make a new-style password hash
- *
- * @param string $password Plain-text password
- * @param bool|string $salt Optional salt, may be random or the user ID.
- * If unspecified or false, will generate one automatically
- * @return string Password hash
- * @deprecated since 1.24, use Password class
- */
- public static function crypt( $password, $salt = false ) {
- wfDeprecated( __METHOD__, '1.24' );
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
- $hash = $passwordFactory->newFromPlaintext( $password );
- return $hash->toString();
- }
-
- /**
- * Compare a password hash with a plain-text password. Requires the user
- * ID if there's a chance that the hash is an old-style hash.
- *
- * @param string $hash Password hash
- * @param string $password Plain-text password to compare
- * @param string|bool $userId User ID for old-style password salt
- *
- * @return bool
- * @deprecated since 1.24, use Password class
- */
- public static function comparePasswords( $hash, $password, $userId = false ) {
- wfDeprecated( __METHOD__, '1.24' );
-
- // Check for *really* old password hashes that don't even have a type
- // The old hash format was just an md5 hex hash, with no type information
- if ( preg_match( '/^[0-9a-f]{32}$/', $hash ) ) {
- global $wgPasswordSalt;
- if ( $wgPasswordSalt ) {
- $password = ":B:{$userId}:{$hash}";
- } else {
- $password = ":A:{$hash}";
- }
- }
-
- $passwordFactory = new PasswordFactory();
- $passwordFactory->init( RequestContext::getMain()->getConfig() );
- $hash = $passwordFactory->newFromCiphertext( $hash );
- return $hash->equals( $password );
- }
-
/**
* Add a newuser log entry for this user.
* Before 1.19 the return value was always true.
static function newFatalPermissionDeniedStatus( $permission ) {
global $wgLang;
- $groups = array_map(
- [ 'User', 'makeGroupLinkWiki' ],
- User::getGroupsWithPermission( $permission )
- );
+ $groups = [];
+ foreach ( User::getGroupsWithPermission( $permission ) as $group ) {
+ $groups[] = UserGroupMembership::getLink( $group, RequestContext::getMain(), 'wiki' );
+ }
if ( $groups ) {
return Status::newFatal( 'badaccess-groups', $wgLang->commaList( $groups ), count( $groups ) );