MediaWiki  master
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 ) {
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 ],
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 ( $linkTitle ) {
402  if ( $format === 'wiki' ) {
403  $linkPage = $linkTitle->getFullText();
404  $groupLink = "[[$linkPage|$groupName]]";
405  } else {
406  $groupLink = $linkRenderer->makeLink( $linkTitle, $groupName );
407  }
408  } else {
409  $groupLink = htmlspecialchars( $groupName );
410  }
411 
412  if ( $expiry ) {
413  // format the expiry to a nice string
414  $uiLanguage = $context->getLanguage();
415  $uiUser = $context->getUser();
416  $expiryDT = $uiLanguage->userTimeAndDate( $expiry, $uiUser );
417  $expiryD = $uiLanguage->userDate( $expiry, $uiUser );
418  $expiryT = $uiLanguage->userTime( $expiry, $uiUser );
419  if ( $format === 'html' ) {
420  $groupLink = Message::rawParam( $groupLink );
421  }
422  return $context->msg( 'group-membership-link-with-expiry' )
423  ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text();
424  }
425  return $groupLink;
426  }
427 
435  public static function getGroupName( $group ) {
436  $msg = wfMessage( "group-$group" );
437  return $msg->isBlank() ? $group : $msg->text();
438  }
439 
448  public static function getGroupMemberName( $group, $username ) {
449  $msg = wfMessage( "group-$group-member", $username );
450  return $msg->isBlank() ? $group : $msg->text();
451  }
452 
460  public static function getGroupPage( $group ) {
461  $msg = wfMessage( "grouppage-$group" )->inContentLanguage();
462  if ( $msg->exists() ) {
463  $title = Title::newFromText( $msg->text() );
464  if ( is_object( $title ) ) {
465  return $title;
466  }
467  }
468  return false;
469  }
470 }
static getMembershipsForUser( $userId, IDatabase $db=null)
Returns UserGroupMembership objects for all the groups a user currently belongs to.
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 rawParam( $raw)
Definition: Message.php:1027
isExpired()
Has the membership expired?
static selectFields()
Returns the list of user_groups fields that should be selected to create a new user group membership...
$context
Definition: load.php:45
affectedRows()
Get the number of rows affected by the last write query.
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
endAtomic( $fname=__METHOD__)
Ends an atomic section of SQL statements.
static getLink( $ugm, IContextSource $context, $format, $userName=null)
Gets a link for a user group, possibly including the expiry date if relevant.
const DB_MASTER
Definition: defines.php:26
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getScopedLockAndFlush( $lockKey, $fname, $timeout)
Acquire a named lock, flush any transaction, and return an RAII style unlocker object.
wfReadOnly()
Check whether the wiki is in read-only mode.
const LIST_AND
Definition: Defines.php:39
static getGroupMemberName( $group, $username)
Gets the localized name for a member of a group, if it exists.
msg( $key,... $params)
This is the method for getting translated interface messages.
insert( $allowUpdate=false, IDatabase $dbw=null)
Insert a user right membership into the database.
static getGroupName( $group)
Gets the localized friendly name for a group, if it exists.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
getDatabaseArray(IDatabase $db)
Get an array suitable for passing to $dbw->insert() or $dbw->update()
static getGroupPage( $group)
Gets the title of a page describing a particular user group.
const LIST_OR
Definition: Defines.php:42
string null $expiry
Timestamp of expiry in TS_MW format, or null if no expiry.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
startAtomic( $fname=__METHOD__, $cancelable=self::ATOMIC_NOT_CANCELABLE)
Begin an atomic section of SQL statements.
makeList( $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
insert( $table, $rows, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
__construct( $userId=0, $group=null, $expiry=null)
static singleton( $domain=false)
const DB_REPLICA
Definition: defines.php:25
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
int $userId
The ID of the user who belongs to the group.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
static purgeExpired()
Purge expired memberships from the user_groups table.
static newFromRow( $row)
Creates a new UserGroupMembership object from a database row.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319