MediaWiki REL1_34
UserGroupMembership.php
Go to the documentation of this file.
1<?php
25
39 private $userId;
40
42 private $group;
43
45 private $expiry;
46
52 public function __construct( $userId = 0, $group = null, $expiry = null ) {
53 $this->userId = (int)$userId;
54 $this->group = $group; // TODO throw on invalid group?
55 $this->expiry = $expiry ?: null;
56 }
57
61 public function getUserId() {
62 return $this->userId;
63 }
64
68 public function getGroup() {
69 return $this->group;
70 }
71
75 public function getExpiry() {
76 return $this->expiry;
77 }
78
79 protected function initFromRow( $row ) {
80 $this->userId = (int)$row->ug_user;
81 $this->group = $row->ug_group;
82 $this->expiry = $row->ug_expiry === null ?
83 null :
84 wfTimestamp( TS_MW, $row->ug_expiry );
85 }
86
93 public static function newFromRow( $row ) {
94 $ugm = new self;
95 $ugm->initFromRow( $row );
96 return $ugm;
97 }
98
104 public static function selectFields() {
105 return [
106 'ug_user',
107 'ug_group',
108 'ug_expiry',
109 ];
110 }
111
119 public function delete( IDatabase $dbw = null ) {
120 if ( wfReadOnly() ) {
121 return false;
122 }
123
124 if ( $dbw === null ) {
125 $dbw = wfGetDB( DB_MASTER );
126 }
127
128 $dbw->delete(
129 'user_groups',
130 [ 'ug_user' => $this->userId, 'ug_group' => $this->group ],
131 __METHOD__ );
132 if ( !$dbw->affectedRows() ) {
133 return false;
134 }
135
136 // Remember that the user was in this group
137 $dbw->insert(
138 'user_former_groups',
139 [ 'ufg_user' => $this->userId, 'ufg_group' => $this->group ],
140 __METHOD__,
141 [ 'IGNORE' ] );
142
143 return true;
144 }
145
156 public function insert( $allowUpdate = false, IDatabase $dbw = null ) {
157 if ( $this->group === null ) {
158 throw new UnexpectedValueException(
159 'Cannot insert an uninitialized UserGroupMembership instance'
160 );
161 } elseif ( $this->userId <= 0 ) {
162 throw new UnexpectedValueException(
163 'UserGroupMembership::insert() needs a positive user ID. ' .
164 'Perhaps addGroup() was called before the user was added to the database.'
165 );
166 }
167
168 $dbw = $dbw ?: wfGetDB( DB_MASTER );
169 $row = $this->getDatabaseArray( $dbw );
170
171 $dbw->startAtomic( __METHOD__ );
172 $dbw->insert( 'user_groups', $row, __METHOD__, [ 'IGNORE' ] );
173 $affected = $dbw->affectedRows();
174 if ( !$affected ) {
175 // Conflicting row already exists; it should be overriden if it is either expired
176 // or if $allowUpdate is true and the current row is different than the loaded row.
177 $conds = [ 'ug_user' => $row['ug_user'], 'ug_group' => $row['ug_group'] ];
178 if ( $allowUpdate ) {
179 // Update the current row if its expiry does not match that of the loaded row
180 $conds[] = $this->expiry
181 ? 'ug_expiry IS NULL OR ug_expiry != ' .
182 $dbw->addQuotes( $dbw->timestamp( $this->expiry ) )
183 : 'ug_expiry IS NOT NULL';
184 } else {
185 // Update the current row if it is expired
186 $conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() );
187 }
188 $dbw->update(
189 'user_groups',
190 [ 'ug_expiry' => $this->expiry ? $dbw->timestamp( $this->expiry ) : null ],
191 $conds,
192 __METHOD__
193 );
194 $affected = $dbw->affectedRows();
195 }
196 $dbw->endAtomic( __METHOD__ );
197
198 // Purge old, expired memberships from the DB
199 $fname = __METHOD__;
200 DeferredUpdates::addCallableUpdate( function () use ( $dbw, $fname ) {
201 $hasExpiredRow = $dbw->selectField(
202 'user_groups',
203 '1',
204 [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
205 $fname
206 );
207 if ( $hasExpiredRow ) {
208 JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
209 }
210 } );
211
212 return $affected > 0;
213 }
214
220 protected function getDatabaseArray( IDatabase $db ) {
221 return [
222 'ug_user' => $this->userId,
223 'ug_group' => $this->group,
224 'ug_expiry' => $this->expiry ? $db->timestamp( $this->expiry ) : null,
225 ];
226 }
227
232 public function isExpired() {
233 if ( !$this->expiry ) {
234 return false;
235 }
236 return wfTimestampNow() > $this->expiry;
237 }
238
245 public static function purgeExpired() {
246 $services = MediaWikiServices::getInstance();
247 if ( $services->getReadOnlyMode()->isReadOnly() ) {
248 return false;
249 }
250
251 $lbFactory = $services->getDBLoadBalancerFactory();
252 $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
253 $dbw = $services->getDBLoadBalancer()->getConnectionRef( DB_MASTER );
254
255 $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
256 $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
257 if ( !$scopedLock ) {
258 return false; // already running
259 }
260
261 $now = time();
262 $purgedRows = 0;
263 do {
264 $dbw->startAtomic( __METHOD__ );
265
266 $res = $dbw->select(
267 'user_groups',
268 self::selectFields(),
269 [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp( $now ) ) ],
270 __METHOD__,
271 [ 'FOR UPDATE', 'LIMIT' => 100 ]
272 );
273
274 if ( $res->numRows() > 0 ) {
275 $insertData = []; // array of users/groups to insert to user_former_groups
276 $deleteCond = []; // array for deleting the rows that are to be moved around
277 foreach ( $res as $row ) {
278 $insertData[] = [ 'ufg_user' => $row->ug_user, 'ufg_group' => $row->ug_group ];
279 $deleteCond[] = $dbw->makeList(
280 [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
281 $dbw::LIST_AND
282 );
283 }
284 // Delete the rows we're about to move
285 $dbw->delete(
286 'user_groups',
287 $dbw->makeList( $deleteCond, $dbw::LIST_OR ),
288 __METHOD__
289 );
290 // Push the groups to user_former_groups
291 $dbw->insert( 'user_former_groups', $insertData, __METHOD__, [ 'IGNORE' ] );
292 // Count how many rows were purged
293 $purgedRows += $res->numRows();
294 }
295
296 $dbw->endAtomic( __METHOD__ );
297
298 $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
299 } while ( $res->numRows() > 0 );
300 return $purgedRows;
301 }
302
311 public static function getMembershipsForUser( $userId, IDatabase $db = null ) {
312 if ( !$db ) {
313 $db = wfGetDB( DB_REPLICA );
314 }
315
316 $res = $db->select( 'user_groups',
317 self::selectFields(),
318 [ 'ug_user' => $userId ],
319 __METHOD__ );
320
321 $ugms = [];
322 foreach ( $res as $row ) {
323 $ugm = self::newFromRow( $row );
324 if ( !$ugm->isExpired() ) {
325 $ugms[$ugm->group] = $ugm;
326 }
327 }
328 ksort( $ugms );
329
330 return $ugms;
331 }
332
343 public static function getMembership( $userId, $group, IDatabase $db = null ) {
344 if ( !$db ) {
345 $db = wfGetDB( DB_REPLICA );
346 }
347
348 $row = $db->selectRow( 'user_groups',
349 self::selectFields(),
350 [ 'ug_user' => $userId, 'ug_group' => $group ],
351 __METHOD__ );
352 if ( !$row ) {
353 return false;
354 }
355
356 $ugm = self::newFromRow( $row );
357 if ( !$ugm->isExpired() ) {
358 return $ugm;
359 }
360 return false;
361 }
362
376 public static function getLink( $ugm, IContextSource $context, $format,
377 $userName = null
378 ) {
379 if ( $format !== 'wiki' && $format !== 'html' ) {
380 throw new MWException( 'UserGroupMembership::getLink() $format parameter should be ' .
381 "'wiki' or 'html'" );
382 }
383
384 if ( $ugm instanceof UserGroupMembership ) {
385 $expiry = $ugm->getExpiry();
386 $group = $ugm->getGroup();
387 } else {
388 $expiry = null;
389 $group = $ugm;
390 }
391
392 if ( $userName !== null ) {
393 $groupName = self::getGroupMemberName( $group, $userName );
394 } else {
395 $groupName = self::getGroupName( $group );
396 }
397
398 // link to the group description page, if it exists
399 $linkTitle = self::getGroupPage( $group );
400 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
401 if ( $format === 'wiki' ) {
402 if ( $linkTitle ) {
403 $linkPage = $linkTitle->getFullText();
404 $groupLink = "[[$linkPage|$groupName]]";
405 } else {
406 $groupLink = $groupName;
407 }
408 } else {
409 if ( $linkTitle ) {
410 $groupLink = $linkRenderer->makeLink( $linkTitle, $groupName );
411 } else {
412 $groupLink = htmlspecialchars( $groupName );
413 }
414 }
415
416 if ( $expiry ) {
417 // format the expiry to a nice string
418 $uiLanguage = $context->getLanguage();
419 $uiUser = $context->getUser();
420 $expiryDT = $uiLanguage->userTimeAndDate( $expiry, $uiUser );
421 $expiryD = $uiLanguage->userDate( $expiry, $uiUser );
422 $expiryT = $uiLanguage->userTime( $expiry, $uiUser );
423
424 if ( $format === 'wiki' ) {
425 return $context->msg( 'group-membership-link-with-expiry' )
426 ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text();
427 } else {
428 $groupLink = Message::rawParam( $groupLink );
429 return $context->msg( 'group-membership-link-with-expiry' )
430 ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->escaped();
431 }
432 }
433 return $groupLink;
434 }
435
443 public static function getGroupName( $group ) {
444 $msg = wfMessage( "group-$group" );
445 return $msg->isBlank() ? $group : $msg->text();
446 }
447
456 public static function getGroupMemberName( $group, $username ) {
457 $msg = wfMessage( "group-$group-member", $username );
458 return $msg->isBlank() ? $group : $msg->text();
459 }
460
468 public static function getGroupPage( $group ) {
469 $msg = wfMessage( "grouppage-$group" )->inContentLanguage();
470 if ( $msg->exists() ) {
471 $title = Title::newFromText( $msg->text() );
472 if ( is_object( $title ) ) {
473 return $title;
474 }
475 }
476 return false;
477 }
478}
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static rawParam( $raw)
Definition Message.php:1027
Represents a "user group membership" – a specific instance of a user belonging to a group.
static newFromRow( $row)
Creates a new UserGroupMembership object from a database row.
static getMembershipsForUser( $userId, IDatabase $db=null)
Returns UserGroupMembership objects for all the groups a user currently belongs to.
static getGroupPage( $group)
Gets the title of a page describing a particular user group.
getDatabaseArray(IDatabase $db)
Get an array suitable for passing to $dbw->insert() or $dbw->update()
static getMembership( $userId, $group, IDatabase $db=null)
Returns a UserGroupMembership object that pertains to the given user and group, or false if the user ...
static selectFields()
Returns the list of user_groups fields that should be selected to create a new user group membership.
static getLink( $ugm, IContextSource $context, $format, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
insert( $allowUpdate=false, IDatabase $dbw=null)
Insert a user right membership into the database.
int $userId
The ID of the user who belongs to the group.
static getGroupName( $group)
Gets the localized friendly name for a group, if it exists.
static purgeExpired()
Purge expired memberships from the user_groups table.
__construct( $userId=0, $group=null, $expiry=null)
isExpired()
Has the membership expired?
static getGroupMemberName( $group, $username)
Gets the localized name for a member of a group, if it exists.
string null $expiry
Timestamp of expiry in TS_MW format, or null if no expiry.
Interface for objects which can provide a MediaWiki context on request.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
affectedRows()
Get the number of rows affected by the last write query.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
$context
Definition load.php:45
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26