3 * Represents the membership of a user to a user group.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 use Wikimedia\Rdbms\IDatabase
;
26 * Represents a "user group membership" -- a specific instance of a user belonging
27 * to a group. For example, the fact that user Mary belongs to the sysop group is a
28 * user group membership.
30 * The class encapsulates rows in the user_groups table. The logic is low-level and
31 * doesn't run any hooks. Often, you will want to call User::addGroup() or
32 * User::removeGroup() instead.
36 class UserGroupMembership
{
37 /** @var int The ID of the user who belongs to the group */
43 /** @var string|null Timestamp of expiry in TS_MW format, or null if no expiry */
47 * @param int $userId The ID of the user who belongs to the group
48 * @param string $group The internal group name
49 * @param string|null $expiry Timestamp of expiry in TS_MW format, or null if no expiry
51 public function __construct( $userId = 0, $group = null, $expiry = null ) {
52 $this->userId
= (int)$userId;
53 $this->group
= $group; // TODO throw on invalid group?
54 $this->expiry
= $expiry ?
: null;
60 public function getUserId() {
67 public function getGroup() {
72 * @return string|null Timestamp of expiry in TS_MW format, or null if no expiry
74 public function getExpiry() {
78 protected function initFromRow( $row ) {
79 $this->userId
= (int)$row->ug_user
;
80 $this->group
= $row->ug_group
;
81 $this->expiry
= $row->ug_expiry
=== null ?
83 wfTimestamp( TS_MW
, $row->ug_expiry
);
87 * Creates a new UserGroupMembership object from a database row.
89 * @param stdClass $row The row from the user_groups table
90 * @return UserGroupMembership
92 public static function newFromRow( $row ) {
94 $ugm->initFromRow( $row );
99 * Returns the list of user_groups fields that should be selected to create
100 * a new user group membership.
103 public static function selectFields() {
112 * Delete the row from the user_groups table.
114 * @throws MWException
115 * @param IDatabase|null $dbw Optional master database connection to use
116 * @return bool Whether or not anything was deleted
118 public function delete( IDatabase
$dbw = null ) {
119 if ( wfReadOnly() ) {
123 if ( $dbw === null ) {
124 $dbw = wfGetDB( DB_MASTER
);
129 [ 'ug_user' => $this->userId
, 'ug_group' => $this->group
],
131 if ( !$dbw->affectedRows() ) {
135 // Remember that the user was in this group
137 'user_former_groups',
138 [ 'ufg_user' => $this->userId
, 'ufg_group' => $this->group
],
146 * Insert a user right membership into the database. When $allowUpdate is false,
147 * the function fails if there is a conflicting membership entry (same user and
148 * group) already in the table.
150 * @throws MWException
151 * @param bool $allowUpdate Whether to perform "upsert" instead of INSERT
152 * @param IDatabase|null $dbw If you have one available
153 * @return bool Whether or not anything was inserted
155 public function insert( $allowUpdate = false, IDatabase
$dbw = null ) {
156 if ( $dbw === null ) {
157 $dbw = wfGetDB( DB_MASTER
);
160 // Purge old, expired memberships from the DB
161 self
::purgeExpired( $dbw );
163 // Check that the values make sense
164 if ( $this->group
=== null ) {
165 throw new UnexpectedValueException(
166 'Don\'t try inserting an uninitialized UserGroupMembership object' );
167 } elseif ( $this->userId
<= 0 ) {
168 throw new UnexpectedValueException(
169 'UserGroupMembership::insert() needs a positive user ID. ' .
170 'Did you forget to add your User object to the database before calling addGroup()?' );
173 $row = $this->getDatabaseArray( $dbw );
174 $dbw->insert( 'user_groups', $row, __METHOD__
, [ 'IGNORE' ] );
175 $affected = $dbw->affectedRows();
177 // Don't collide with expired user group memberships
178 // Do this after trying to insert, in order to avoid locking
181 'ug_user' => $row['ug_user'],
182 'ug_group' => $row['ug_group'],
184 // if we're unconditionally updating, check that the expiry is not already the
185 // same as what we are trying to update it to; otherwise, only update if
186 // the expiry date is in the past
187 if ( $allowUpdate ) {
188 if ( $this->expiry
) {
189 $conds[] = 'ug_expiry IS NULL OR ug_expiry != ' .
190 $dbw->addQuotes( $dbw->timestamp( $this->expiry
) );
192 $conds[] = 'ug_expiry IS NOT NULL';
195 $conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() );
198 $row = $dbw->selectRow( 'user_groups', $this::selectFields(), $conds, __METHOD__
);
202 [ 'ug_expiry' => $this->expiry ?
$dbw->timestamp( $this->expiry
) : null ],
203 [ 'ug_user' => $row->ug_user
, 'ug_group' => $row->ug_group
],
205 $affected = $dbw->affectedRows();
209 return $affected > 0;
213 * Get an array suitable for passing to $dbw->insert() or $dbw->update()
214 * @param IDatabase $db
217 protected function getDatabaseArray( IDatabase
$db ) {
219 'ug_user' => $this->userId
,
220 'ug_group' => $this->group
,
221 'ug_expiry' => $this->expiry ?
$db->timestamp( $this->expiry
) : null,
226 * Has the membership expired?
229 public function isExpired() {
230 if ( !$this->expiry
) {
233 return wfTimestampNow() > $this->expiry
;
238 * Purge expired memberships from the user_groups table
240 * @param IDatabase|null $dbw
242 public static function purgeExpired( IDatabase
$dbw = null ) {
243 if ( wfReadOnly() ) {
247 if ( $dbw === null ) {
248 $dbw = wfGetDB( DB_MASTER
);
251 DeferredUpdates
::addUpdate( new AtomicSectionUpdate(
254 function ( IDatabase
$dbw, $fname ) {
255 $expiryCond = [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ];
256 $res = $dbw->select( 'user_groups', self
::selectFields(), $expiryCond, $fname );
258 // save an array of users/groups to insert to user_former_groups
259 $usersAndGroups = [];
260 foreach ( $res as $row ) {
261 $usersAndGroups[] = [ 'ufg_user' => $row->ug_user
, 'ufg_group' => $row->ug_group
];
265 $dbw->delete( 'user_groups', $expiryCond, $fname );
267 // and push the groups to user_former_groups
268 $dbw->insert( 'user_former_groups', $usersAndGroups, __METHOD__
, [ 'IGNORE' ] );
274 * Returns UserGroupMembership objects for all the groups a user currently
277 * @param int $userId ID of the user to search for
278 * @param IDatabase|null $db Optional database connection
279 * @return array Associative array of (group name => UserGroupMembership object)
281 public static function getMembershipsForUser( $userId, IDatabase
$db = null ) {
283 $db = wfGetDB( DB_REPLICA
);
286 $res = $db->select( 'user_groups',
287 self
::selectFields(),
288 [ 'ug_user' => $userId ],
292 foreach ( $res as $row ) {
293 $ugm = self
::newFromRow( $row );
294 if ( !$ugm->isExpired() ) {
295 $ugms[$ugm->group
] = $ugm;
303 * Returns a UserGroupMembership object that pertains to the given user and group,
304 * or false if the user does not belong to that group (or the assignment has
307 * @param int $userId ID of the user to search for
308 * @param string $group User group name
309 * @param IDatabase|null $db Optional database connection
310 * @return UserGroupMembership|false
312 public static function getMembership( $userId, $group, IDatabase
$db = null ) {
314 $db = wfGetDB( DB_REPLICA
);
317 $row = $db->selectRow( 'user_groups',
318 self
::selectFields(),
319 [ 'ug_user' => $userId, 'ug_group' => $group ],
325 $ugm = self
::newFromRow( $row );
326 if ( !$ugm->isExpired() ) {
334 * Gets a link for a user group, possibly including the expiry date if relevant.
336 * @param string|UserGroupMembership $ugm Either a group name as a string, or
337 * a UserGroupMembership object
338 * @param IContextSource $context
339 * @param string $format Either 'wiki' or 'html'
340 * @param string|null $userName If you want to use the group member message
341 * ("administrator"), pass the name of the user who belongs to the group; it
342 * is used for GENDER of the group member message. If you instead want the
343 * group name message ("Administrators"), omit this parameter.
346 public static function getLink( $ugm, IContextSource
$context, $format,
349 if ( $format !== 'wiki' && $format !== 'html' ) {
350 throw new MWException( 'UserGroupMembership::getLink() $format parameter should be ' .
351 "'wiki' or 'html'" );
354 if ( $ugm instanceof UserGroupMembership
) {
355 $expiry = $ugm->getExpiry();
356 $group = $ugm->getGroup();
362 if ( $userName !== null ) {
363 $groupName = self
::getGroupMemberName( $group, $userName );
365 $groupName = self
::getGroupName( $group );
368 // link to the group description page, if it exists
369 $linkTitle = self
::getGroupPage( $group );
371 if ( $format === 'wiki' ) {
372 $linkPage = $linkTitle->getFullText();
373 $groupLink = "[[$linkPage|$groupName]]";
375 $groupLink = Linker
::link( $linkTitle, htmlspecialchars( $groupName ) );
378 $groupLink = htmlspecialchars( $groupName );
382 // format the expiry to a nice string
383 $uiLanguage = $context->getLanguage();
384 $uiUser = $context->getUser();
385 $expiryDT = $uiLanguage->userTimeAndDate( $expiry, $uiUser );
386 $expiryD = $uiLanguage->userDate( $expiry, $uiUser );
387 $expiryT = $uiLanguage->userTime( $expiry, $uiUser );
388 if ( $format === 'html' ) {
389 $groupLink = Message
::rawParam( $groupLink );
391 return $context->msg( 'group-membership-link-with-expiry' )
392 ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text();
399 * Gets the localized friendly name for a group, if it exists. For example,
400 * "Administrators" or "Bureaucrats"
402 * @param string $group Internal group name
403 * @return string Localized friendly group name
405 public static function getGroupName( $group ) {
406 $msg = wfMessage( "group-$group" );
407 return $msg->isBlank() ?
$group : $msg->text();
411 * Gets the localized name for a member of a group, if it exists. For example,
412 * "administrator" or "bureaucrat"
414 * @param string $group Internal group name
415 * @param string $username Username for gender
416 * @return string Localized name for group member
418 public static function getGroupMemberName( $group, $username ) {
419 $msg = wfMessage( "group-$group-member", $username );
420 return $msg->isBlank() ?
$group : $msg->text();
424 * Gets the title of a page describing a particular user group. When the name
425 * of the group appears in the UI, it can link to this page.
427 * @param string $group Internal group name
428 * @return Title|bool Title of the page if it exists, false otherwise
430 public static function getGroupPage( $group ) {
431 $msg = wfMessage( "grouppage-$group" )->inContentLanguage();
432 if ( $msg->exists() ) {
433 $title = Title
::newFromText( $msg->text() );
434 if ( is_object( $title ) ) {