/**
* @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 */
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;
// User/IP blocking
$block = Block::newFromTarget( $this, $ip, !$bFromSlave );
- // If no block has been found, check for a cookie indicating that the user is blocked.
- $blockCookieVal = (int)$this->getRequest()->getCookie( 'BlockID' );
- if ( !$block instanceof Block && $blockCookieVal > 0 ) {
- // Load the Block from the ID in the cookie.
- $tmpBlock = Block::newFromID( $blockCookieVal );
- if ( $tmpBlock instanceof Block ) {
- // Check the validity of the block.
- $blockIsValid = $tmpBlock->getType() == Block::TYPE_USER
- && !$tmpBlock->isExpired()
- && $tmpBlock->isAutoblocking();
- $config = RequestContext::getMain()->getConfig();
- $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
- if ( $blockIsValid && $useBlockCookie ) {
- // Use the block.
- $block = $tmpBlock;
- $this->blockTrigger = 'cookie-block';
- } else {
- // If the block is not valid, clear the block cookie (but don't delete it,
- // because it needs to be cleared from LocalStorage as well and an empty string
- // value is checked for in the mediawiki.user.blockcookie module).
- $tmpBlock->setCookie( $this->getRequest()->response(), true );
- }
- }
+ // Cookie blocking
+ if ( !$block instanceof Block ) {
+ $block = $this->getBlockFromCookieValue( $this->getRequest()->getCookie( 'BlockID' ) );
}
// Proxy blocking
Hooks::run( 'GetBlockedStatus', [ &$user ] );
}
+ /**
+ * Try to load a Block from an ID given in a cookie value.
+ * @param string|null $blockCookieVal The cookie value to check.
+ * @return Block|bool The Block object, or false if none could be loaded.
+ */
+ protected function getBlockFromCookieValue( $blockCookieVal ) {
+ // Make sure there's something to check. The cookie value must start with a number.
+ if ( strlen( $blockCookieVal ) < 1 || !is_numeric( substr( $blockCookieVal, 0, 1 ) ) ) {
+ return false;
+ }
+ // Load the Block from the ID in the cookie.
+ $blockCookieId = Block::getIdFromCookieValue( $blockCookieVal );
+ if ( $blockCookieId !== null ) {
+ // An ID was found in the cookie.
+ $tmpBlock = Block::newFromID( $blockCookieId );
+ if ( $tmpBlock instanceof Block ) {
+ // Check the validity of the block.
+ $blockIsValid = $tmpBlock->getType() == Block::TYPE_USER
+ && !$tmpBlock->isExpired()
+ && $tmpBlock->isAutoblocking();
+ $config = RequestContext::getMain()->getConfig();
+ $useBlockCookie = ( $config->get( 'CookieSetOnAutoblock' ) === true );
+ if ( $blockIsValid && $useBlockCookie ) {
+ // Use the block.
+ $this->blockTrigger = 'cookie-block';
+ return $tmpBlock;
+ } else {
+ // If the block is not valid, clear the block cookie (but don't delete it,
+ // because it needs to be cleared from LocalStorage as well and an empty string
+ // value is checked for in the mediawiki.user.blockcookie module).
+ $tmpBlock->setCookie( $this->getRequest()->response(), true );
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Whether the given IP is in a DNS blacklist.
*
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;
}
/**
}
/**
- * 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().
/**
* 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 ) );