123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471 |
- <?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;
- use MediaWiki\MediaWikiServices;
- /**
- * 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|null $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 UnexpectedValueException
- * @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 ( $this->group === null ) {
- throw new UnexpectedValueException(
- 'Cannot insert an uninitialized UserGroupMembership instance'
- );
- } elseif ( $this->userId <= 0 ) {
- throw new UnexpectedValueException(
- 'UserGroupMembership::insert() needs a positive user ID. ' .
- 'Perhaps addGroup() was called before the user was added to the database.'
- );
- }
- $dbw = $dbw ?: wfGetDB( DB_MASTER );
- $row = $this->getDatabaseArray( $dbw );
- $dbw->startAtomic( __METHOD__ );
- $dbw->insert( 'user_groups', $row, __METHOD__, [ 'IGNORE' ] );
- $affected = $dbw->affectedRows();
- if ( !$affected ) {
- // Conflicting row already exists; it should be overriden if it is either expired
- // or if $allowUpdate is true and the current row is different than the loaded row.
- $conds = [ 'ug_user' => $row['ug_user'], 'ug_group' => $row['ug_group'] ];
- if ( $allowUpdate ) {
- // Update the current row if its expiry does not match that of the loaded row
- $conds[] = $this->expiry
- ? 'ug_expiry IS NULL OR ug_expiry != ' .
- $dbw->addQuotes( $dbw->timestamp( $this->expiry ) )
- : 'ug_expiry IS NOT NULL';
- } else {
- // Update the current row if it is expired
- $conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() );
- }
- $dbw->update(
- 'user_groups',
- [ 'ug_expiry' => $this->expiry ? $dbw->timestamp( $this->expiry ) : null ],
- $conds,
- __METHOD__
- );
- $affected = $dbw->affectedRows();
- }
- $dbw->endAtomic( __METHOD__ );
- // Purge old, expired memberships from the DB
- $fname = __METHOD__;
- DeferredUpdates::addCallableUpdate( function () use ( $dbw, $fname ) {
- $hasExpiredRow = $dbw->selectField(
- 'user_groups',
- '1',
- [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
- $fname
- );
- if ( $hasExpiredRow ) {
- JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
- }
- } );
- 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;
- }
- return wfTimestampNow() > $this->expiry;
- }
- /**
- * Purge expired memberships from the user_groups table
- *
- * @return int|bool false if purging wasn't attempted (e.g. because of
- * readonly), the number of rows purged (might be 0) otherwise
- */
- public static function purgeExpired() {
- $services = MediaWikiServices::getInstance();
- if ( $services->getReadOnlyMode()->isReadOnly() ) {
- return false;
- }
- $lbFactory = $services->getDBLoadBalancerFactory();
- $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER );
- $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
- $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
- if ( !$scopedLock ) {
- return false; // already running
- }
- $now = time();
- $purgedRows = 0;
- do {
- $dbw->startAtomic( __METHOD__ );
- $res = $dbw->select(
- 'user_groups',
- self::selectFields(),
- [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
- __METHOD__,
- [ 'FOR UPDATE', 'LIMIT' => 100 ]
- );
- if ( $res->numRows() > 0 ) {
- $insertData = []; // array of users/groups to insert to user_former_groups
- $deleteCond = []; // array for deleting the rows that are to be moved around
- foreach ( $res as $row ) {
- $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
- $deleteCond[] = $dbw->makeList(
- [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
- $dbw::LIST_AND
- );
- }
- // Delete the rows we're about to move
- $dbw->delete(
- 'user_groups',
- $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
- __METHOD__
- );
- // Push the groups to user_former_groups
- $dbw->insert( 'user_former_groups', $insertData, __METHOD__, [ 'IGNORE' ] );
- // Count how many rows were purged
- $purgedRows += $res->numRows();
- }
- $dbw->endAtomic( __METHOD__ );
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
- } while ( $res->numRows() > 0 );
- return $purgedRows;
- }
- /**
- * 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 UserGroupMembership[] 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;
- }
- }
- ksort( $ugms );
- 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;
- }
- 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 );
- $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
- if ( $linkTitle ) {
- if ( $format === 'wiki' ) {
- $linkPage = $linkTitle->getFullText();
- $groupLink = "[[$linkPage|$groupName]]";
- } else {
- $groupLink = $linkRenderer->makeLink( $linkTitle, $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();
- }
- 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;
- }
- }
|