wiki.techinc.nl/includes/user/UserGroupMembership.php
This, that and the other f8008a7cb8 Remove $wgDisableUserGroupExpiry feature flag
This feature flag was added to comply with WMF's schema change policy.
It is no longer needed now that the feature is enabled on WMF sites.
External MW installations will run update.php or manually do DB updates
as required before upgrading MW.

Bug: T162517
Change-Id: I5b532d79fd1e8b178490cf2617499ae62967de2c
2017-05-06 20:50:42 +10:00

440 lines
12 KiB
PHP

<?php
/**
* Represents the membership of a user to a user group.
*
* 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
*/
use Wikimedia\Rdbms\IDatabase;
/**
* Represents a "user group membership" -- a specific instance of a user belonging
* to a group. For example, the fact that user Mary belongs to the sysop group is a
* user group membership.
*
* The class encapsulates rows in the user_groups table. The logic is low-level and
* doesn't run any hooks. Often, you will want to call User::addGroup() or
* User::removeGroup() instead.
*
* @since 1.29
*/
class UserGroupMembership {
/** @var int The ID of the user who belongs to the group */
private $userId;
/** @var string */
private $group;
/** @var string|null Timestamp of expiry in TS_MW format, or null if no expiry */
private $expiry;
/**
* @param int $userId The ID of the user who belongs to the group
* @param string $group The internal group name
* @param string|null $expiry Timestamp of expiry in TS_MW format, or null if no expiry
*/
public function __construct( $userId = 0, $group = null, $expiry = null ) {
$this->userId = (int)$userId;
$this->group = $group; // TODO throw on invalid group?
$this->expiry = $expiry ?: null;
}
/**
* @return int
*/
public function getUserId() {
return $this->userId;
}
/**
* @return string
*/
public function getGroup() {
return $this->group;
}
/**
* @return string|null Timestamp of expiry in TS_MW format, or null if no expiry
*/
public function getExpiry() {
return $this->expiry;
}
protected function initFromRow( $row ) {
$this->userId = (int)$row->ug_user;
$this->group = $row->ug_group;
$this->expiry = $row->ug_expiry === null ?
null :
wfTimestamp( TS_MW, $row->ug_expiry );
}
/**
* Creates a new UserGroupMembership object from a database row.
*
* @param stdClass $row The row from the user_groups table
* @return UserGroupMembership
*/
public static function newFromRow( $row ) {
$ugm = new self;
$ugm->initFromRow( $row );
return $ugm;
}
/**
* Returns the list of user_groups fields that should be selected to create
* a new user group membership.
* @return array
*/
public static function selectFields() {
return [
'ug_user',
'ug_group',
'ug_expiry',
];
}
/**
* Delete the row from the user_groups table.
*
* @throws MWException
* @param IDatabase|null $dbw Optional master database connection to use
* @return bool Whether or not anything was deleted
*/
public function delete( IDatabase $dbw = null ) {
if ( wfReadOnly() ) {
return false;
}
if ( $dbw === null ) {
$dbw = wfGetDB( DB_MASTER );
}
$dbw->delete(
'user_groups',
[ 'ug_user' => $this->userId, 'ug_group' => $this->group ],
__METHOD__ );
if ( !$dbw->affectedRows() ) {
return false;
}
// Remember that the user was in this group
$dbw->insert(
'user_former_groups',
[ 'ufg_user' => $this->userId, 'ufg_group' => $this->group ],
__METHOD__,
[ 'IGNORE' ] );
return true;
}
/**
* Insert a user right membership into the database. When $allowUpdate is false,
* the function fails if there is a conflicting membership entry (same user and
* group) already in the table.
*
* @throws MWException
* @param bool $allowUpdate Whether to perform "upsert" instead of INSERT
* @param IDatabase|null $dbw If you have one available
* @return bool Whether or not anything was inserted
*/
public function insert( $allowUpdate = false, IDatabase $dbw = null ) {
if ( $dbw === null ) {
$dbw = wfGetDB( DB_MASTER );
}
// Purge old, expired memberships from the DB
self::purgeExpired( $dbw );
// Check that the values make sense
if ( $this->group === null ) {
throw new UnexpectedValueException(
'Don\'t try inserting an uninitialized UserGroupMembership object' );
} elseif ( $this->userId <= 0 ) {
throw new UnexpectedValueException(
'UserGroupMembership::insert() needs a positive user ID. ' .
'Did you forget to add your User object to the database before calling addGroup()?' );
}
$row = $this->getDatabaseArray( $dbw );
$dbw->insert( 'user_groups', $row, __METHOD__, [ 'IGNORE' ] );
$affected = $dbw->affectedRows();
// Don't collide with expired user group memberships
// Do this after trying to insert, in order to avoid locking
if ( !$affected ) {
$conds = [
'ug_user' => $row['ug_user'],
'ug_group' => $row['ug_group'],
];
// if we're unconditionally updating, check that the expiry is not already the
// same as what we are trying to update it to; otherwise, only update if
// the expiry date is in the past
if ( $allowUpdate ) {
if ( $this->expiry ) {
$conds[] = 'ug_expiry IS NULL OR ug_expiry != ' .
$dbw->addQuotes( $dbw->timestamp( $this->expiry ) );
} else {
$conds[] = 'ug_expiry IS NOT NULL';
}
} else {
$conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() );
}
$row = $dbw->selectRow( 'user_groups', $this::selectFields(), $conds, __METHOD__ );
if ( $row ) {
$dbw->update(
'user_groups',
[ 'ug_expiry' => $this->expiry ? $dbw->timestamp( $this->expiry ) : null ],
[ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
__METHOD__ );
$affected = $dbw->affectedRows();
}
}
return $affected > 0;
}
/**
* Get an array suitable for passing to $dbw->insert() or $dbw->update()
* @param IDatabase $db
* @return array
*/
protected function getDatabaseArray( IDatabase $db ) {
return [
'ug_user' => $this->userId,
'ug_group' => $this->group,
'ug_expiry' => $this->expiry ? $db->timestamp( $this->expiry ) : null,
];
}
/**
* Has the membership expired?
* @return bool
*/
public function isExpired() {
if ( !$this->expiry ) {
return false;
} else {
return wfTimestampNow() > $this->expiry;
}
}
/**
* Purge expired memberships from the user_groups table
*
* @param IDatabase|null $dbw
*/
public static function purgeExpired( IDatabase $dbw = null ) {
if ( wfReadOnly() ) {
return;
}
if ( $dbw === null ) {
$dbw = wfGetDB( DB_MASTER );
}
DeferredUpdates::addUpdate( new AtomicSectionUpdate(
$dbw,
__METHOD__,
function ( IDatabase $dbw, $fname ) {
$expiryCond = [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ];
$res = $dbw->select( 'user_groups', self::selectFields(), $expiryCond, $fname );
// save an array of users/groups to insert to user_former_groups
$usersAndGroups = [];
foreach ( $res as $row ) {
$usersAndGroups[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
}
// delete 'em all
$dbw->delete( 'user_groups', $expiryCond, $fname );
// and push the groups to user_former_groups
$dbw->insert( 'user_former_groups', $usersAndGroups, __METHOD__, [ 'IGNORE' ] );
}
) );
}
/**
* Returns UserGroupMembership objects for all the groups a user currently
* belongs to.
*
* @param int $userId ID of the user to search for
* @param IDatabase|null $db Optional database connection
* @return array Associative array of (group name => UserGroupMembership object)
*/
public static function getMembershipsForUser( $userId, IDatabase $db = null ) {
if ( !$db ) {
$db = wfGetDB( DB_REPLICA );
}
$res = $db->select( 'user_groups',
self::selectFields(),
[ 'ug_user' => $userId ],
__METHOD__ );
$ugms = [];
foreach ( $res as $row ) {
$ugm = self::newFromRow( $row );
if ( !$ugm->isExpired() ) {
$ugms[$ugm->group] = $ugm;
}
}
return $ugms;
}
/**
* Returns a UserGroupMembership object that pertains to the given user and group,
* or false if the user does not belong to that group (or the assignment has
* expired).
*
* @param int $userId ID of the user to search for
* @param string $group User group name
* @param IDatabase|null $db Optional database connection
* @return UserGroupMembership|false
*/
public static function getMembership( $userId, $group, IDatabase $db = null ) {
if ( !$db ) {
$db = wfGetDB( DB_REPLICA );
}
$row = $db->selectRow( 'user_groups',
self::selectFields(),
[ 'ug_user' => $userId, 'ug_group' => $group ],
__METHOD__ );
if ( !$row ) {
return false;
}
$ugm = self::newFromRow( $row );
if ( !$ugm->isExpired() ) {
return $ugm;
} else {
return false;
}
}
/**
* Gets a link for a user group, possibly including the expiry date if relevant.
*
* @param string|UserGroupMembership $ugm Either a group name as a string, or
* a UserGroupMembership object
* @param IContextSource $context
* @param string $format Either 'wiki' or 'html'
* @param string|null $userName If you want to use the group member message
* ("administrator"), pass the name of the user who belongs to the group; it
* is used for GENDER of the group member message. If you instead want the
* group name message ("Administrators"), omit this parameter.
* @return string
*/
public static function getLink( $ugm, IContextSource $context, $format,
$userName = null ) {
if ( $format !== 'wiki' && $format !== 'html' ) {
throw new MWException( 'UserGroupMembership::getLink() $format parameter should be ' .
"'wiki' or 'html'" );
}
if ( $ugm instanceof UserGroupMembership ) {
$expiry = $ugm->getExpiry();
$group = $ugm->getGroup();
} else {
$expiry = null;
$group = $ugm;
}
if ( $userName !== null ) {
$groupName = self::getGroupMemberName( $group, $userName );
} else {
$groupName = self::getGroupName( $group );
}
// link to the group description page, if it exists
$linkTitle = self::getGroupPage( $group );
if ( $linkTitle ) {
if ( $format === 'wiki' ) {
$linkPage = $linkTitle->getFullText();
$groupLink = "[[$linkPage|$groupName]]";
} else {
$groupLink = Linker::link( $linkTitle, htmlspecialchars( $groupName ) );
}
} else {
$groupLink = htmlspecialchars( $groupName );
}
if ( $expiry ) {
// format the expiry to a nice string
$uiLanguage = $context->getLanguage();
$uiUser = $context->getUser();
$expiryDT = $uiLanguage->userTimeAndDate( $expiry, $uiUser );
$expiryD = $uiLanguage->userDate( $expiry, $uiUser );
$expiryT = $uiLanguage->userTime( $expiry, $uiUser );
if ( $format === 'html' ) {
$groupLink = Message::rawParam( $groupLink );
}
return $context->msg( 'group-membership-link-with-expiry' )
->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text();
} else {
return $groupLink;
}
}
/**
* Gets the localized friendly name for a group, if it exists. For example,
* "Administrators" or "Bureaucrats"
*
* @param string $group Internal group name
* @return string Localized friendly group name
*/
public static function getGroupName( $group ) {
$msg = wfMessage( "group-$group" );
return $msg->isBlank() ? $group : $msg->text();
}
/**
* Gets the localized name for a member of a group, if it exists. For example,
* "administrator" or "bureaucrat"
*
* @param string $group Internal group name
* @param string $username Username for gender
* @return string Localized name for group member
*/
public static function getGroupMemberName( $group, $username ) {
$msg = wfMessage( "group-$group-member", $username );
return $msg->isBlank() ? $group : $msg->text();
}
/**
* Gets the title of a page describing a particular user group. When the name
* of the group appears in the UI, it can link to this page.
*
* @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;
}
}