]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/user/UserGroupMembership.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / includes / user / UserGroupMembership.php
1 <?php
2 /**
3  * Represents the membership of a user to a user group.
4  *
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.
9  *
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.
14  *
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
19  *
20  * @file
21  */
22
23 use Wikimedia\Rdbms\IDatabase;
24
25 /**
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.
29  *
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.
33  *
34  * @since 1.29
35  */
36 class UserGroupMembership {
37         /** @var int The ID of the user who belongs to the group */
38         private $userId;
39
40         /** @var string */
41         private $group;
42
43         /** @var string|null Timestamp of expiry in TS_MW format, or null if no expiry */
44         private $expiry;
45
46         /**
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
50          */
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;
55         }
56
57         /**
58          * @return int
59          */
60         public function getUserId() {
61                 return $this->userId;
62         }
63
64         /**
65          * @return string
66          */
67         public function getGroup() {
68                 return $this->group;
69         }
70
71         /**
72          * @return string|null Timestamp of expiry in TS_MW format, or null if no expiry
73          */
74         public function getExpiry() {
75                 return $this->expiry;
76         }
77
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 ?
82                         null :
83                         wfTimestamp( TS_MW, $row->ug_expiry );
84         }
85
86         /**
87          * Creates a new UserGroupMembership object from a database row.
88          *
89          * @param stdClass $row The row from the user_groups table
90          * @return UserGroupMembership
91          */
92         public static function newFromRow( $row ) {
93                 $ugm = new self;
94                 $ugm->initFromRow( $row );
95                 return $ugm;
96         }
97
98         /**
99          * Returns the list of user_groups fields that should be selected to create
100          * a new user group membership.
101          * @return array
102          */
103         public static function selectFields() {
104                 return [
105                         'ug_user',
106                         'ug_group',
107                         'ug_expiry',
108                 ];
109         }
110
111         /**
112          * Delete the row from the user_groups table.
113          *
114          * @throws MWException
115          * @param IDatabase|null $dbw Optional master database connection to use
116          * @return bool Whether or not anything was deleted
117          */
118         public function delete( IDatabase $dbw = null ) {
119                 if ( wfReadOnly() ) {
120                         return false;
121                 }
122
123                 if ( $dbw === null ) {
124                         $dbw = wfGetDB( DB_MASTER );
125                 }
126
127                 $dbw->delete(
128                         'user_groups',
129                         [ 'ug_user' => $this->userId, 'ug_group' => $this->group ],
130                         __METHOD__ );
131                 if ( !$dbw->affectedRows() ) {
132                         return false;
133                 }
134
135                 // Remember that the user was in this group
136                 $dbw->insert(
137                         'user_former_groups',
138                         [ 'ufg_user' => $this->userId, 'ufg_group' => $this->group ],
139                         __METHOD__,
140                         [ 'IGNORE' ] );
141
142                 return true;
143         }
144
145         /**
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.
149          *
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
154          */
155         public function insert( $allowUpdate = false, IDatabase $dbw = null ) {
156                 if ( $dbw === null ) {
157                         $dbw = wfGetDB( DB_MASTER );
158                 }
159
160                 // Purge old, expired memberships from the DB
161                 self::purgeExpired( $dbw );
162
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()?' );
171                 }
172
173                 $row = $this->getDatabaseArray( $dbw );
174                 $dbw->insert( 'user_groups', $row, __METHOD__, [ 'IGNORE' ] );
175                 $affected = $dbw->affectedRows();
176
177                 // Don't collide with expired user group memberships
178                 // Do this after trying to insert, in order to avoid locking
179                 if ( !$affected ) {
180                         $conds = [
181                                 'ug_user' => $row['ug_user'],
182                                 'ug_group' => $row['ug_group'],
183                         ];
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 ) );
191                                 } else {
192                                         $conds[] = 'ug_expiry IS NOT NULL';
193                                 }
194                         } else {
195                                 $conds[] = 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() );
196                         }
197
198                         $row = $dbw->selectRow( 'user_groups', $this::selectFields(), $conds, __METHOD__ );
199                         if ( $row ) {
200                                 $dbw->update(
201                                         'user_groups',
202                                         [ 'ug_expiry' => $this->expiry ? $dbw->timestamp( $this->expiry ) : null ],
203                                         [ 'ug_user' => $row->ug_user, 'ug_group' => $row->ug_group ],
204                                         __METHOD__ );
205                                 $affected = $dbw->affectedRows();
206                         }
207                 }
208
209                 return $affected > 0;
210         }
211
212         /**
213          * Get an array suitable for passing to $dbw->insert() or $dbw->update()
214          * @param IDatabase $db
215          * @return array
216          */
217         protected function getDatabaseArray( IDatabase $db ) {
218                 return [
219                         'ug_user' => $this->userId,
220                         'ug_group' => $this->group,
221                         'ug_expiry' => $this->expiry ? $db->timestamp( $this->expiry ) : null,
222                 ];
223         }
224
225         /**
226          * Has the membership expired?
227          * @return bool
228          */
229         public function isExpired() {
230                 if ( !$this->expiry ) {
231                         return false;
232                 } else {
233                         return wfTimestampNow() > $this->expiry;
234                 }
235         }
236
237         /**
238          * Purge expired memberships from the user_groups table
239          *
240          * @param IDatabase|null $dbw
241          */
242         public static function purgeExpired( IDatabase $dbw = null ) {
243                 if ( wfReadOnly() ) {
244                         return;
245                 }
246
247                 if ( $dbw === null ) {
248                         $dbw = wfGetDB( DB_MASTER );
249                 }
250
251                 DeferredUpdates::addUpdate( new AtomicSectionUpdate(
252                         $dbw,
253                         __METHOD__,
254                         function ( IDatabase $dbw, $fname ) {
255                                 $expiryCond = [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ];
256                                 $res = $dbw->select( 'user_groups', self::selectFields(), $expiryCond, $fname );
257
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 ];
262                                 }
263
264                                 // delete 'em all
265                                 $dbw->delete( 'user_groups', $expiryCond, $fname );
266
267                                 // and push the groups to user_former_groups
268                                 $dbw->insert( 'user_former_groups', $usersAndGroups, __METHOD__, [ 'IGNORE' ] );
269                         }
270                 ) );
271         }
272
273         /**
274          * Returns UserGroupMembership objects for all the groups a user currently
275          * belongs to.
276          *
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)
280          */
281         public static function getMembershipsForUser( $userId, IDatabase $db = null ) {
282                 if ( !$db ) {
283                         $db = wfGetDB( DB_REPLICA );
284                 }
285
286                 $res = $db->select( 'user_groups',
287                         self::selectFields(),
288                         [ 'ug_user' => $userId ],
289                         __METHOD__ );
290
291                 $ugms = [];
292                 foreach ( $res as $row ) {
293                         $ugm = self::newFromRow( $row );
294                         if ( !$ugm->isExpired() ) {
295                                 $ugms[$ugm->group] = $ugm;
296                         }
297                 }
298
299                 return $ugms;
300         }
301
302         /**
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
305          * expired).
306          *
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
311          */
312         public static function getMembership( $userId, $group, IDatabase $db = null ) {
313                 if ( !$db ) {
314                         $db = wfGetDB( DB_REPLICA );
315                 }
316
317                 $row = $db->selectRow( 'user_groups',
318                         self::selectFields(),
319                         [ 'ug_user' => $userId, 'ug_group' => $group ],
320                         __METHOD__ );
321                 if ( !$row ) {
322                         return false;
323                 }
324
325                 $ugm = self::newFromRow( $row );
326                 if ( !$ugm->isExpired() ) {
327                         return $ugm;
328                 } else {
329                         return false;
330                 }
331         }
332
333         /**
334          * Gets a link for a user group, possibly including the expiry date if relevant.
335          *
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.
344          * @return string
345          */
346         public static function getLink( $ugm, IContextSource $context, $format,
347                 $userName = null
348         ) {
349                 if ( $format !== 'wiki' && $format !== 'html' ) {
350                         throw new MWException( 'UserGroupMembership::getLink() $format parameter should be ' .
351                                 "'wiki' or 'html'" );
352                 }
353
354                 if ( $ugm instanceof UserGroupMembership ) {
355                         $expiry = $ugm->getExpiry();
356                         $group = $ugm->getGroup();
357                 } else {
358                         $expiry = null;
359                         $group = $ugm;
360                 }
361
362                 if ( $userName !== null ) {
363                         $groupName = self::getGroupMemberName( $group, $userName );
364                 } else {
365                         $groupName = self::getGroupName( $group );
366                 }
367
368                 // link to the group description page, if it exists
369                 $linkTitle = self::getGroupPage( $group );
370                 if ( $linkTitle ) {
371                         if ( $format === 'wiki' ) {
372                                 $linkPage = $linkTitle->getFullText();
373                                 $groupLink = "[[$linkPage|$groupName]]";
374                         } else {
375                                 $groupLink = Linker::link( $linkTitle, htmlspecialchars( $groupName ) );
376                         }
377                 } else {
378                         $groupLink = htmlspecialchars( $groupName );
379                 }
380
381                 if ( $expiry ) {
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 );
390                         }
391                         return $context->msg( 'group-membership-link-with-expiry' )
392                                 ->params( $groupLink, $expiryDT, $expiryD, $expiryT )->text();
393                 } else {
394                         return $groupLink;
395                 }
396         }
397
398         /**
399          * Gets the localized friendly name for a group, if it exists. For example,
400          * "Administrators" or "Bureaucrats"
401          *
402          * @param string $group Internal group name
403          * @return string Localized friendly group name
404          */
405         public static function getGroupName( $group ) {
406                 $msg = wfMessage( "group-$group" );
407                 return $msg->isBlank() ? $group : $msg->text();
408         }
409
410         /**
411          * Gets the localized name for a member of a group, if it exists. For example,
412          * "administrator" or "bureaucrat"
413          *
414          * @param string $group Internal group name
415          * @param string $username Username for gender
416          * @return string Localized name for group member
417          */
418         public static function getGroupMemberName( $group, $username ) {
419                 $msg = wfMessage( "group-$group-member", $username );
420                 return $msg->isBlank() ? $group : $msg->text();
421         }
422
423         /**
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.
426          *
427          * @param string $group Internal group name
428          * @return Title|bool Title of the page if it exists, false otherwise
429          */
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 ) ) {
435                                 return $title;
436                         }
437                 }
438                 return false;
439         }
440 }