1<?php 2/** 3 * Copyright © 2004 Brion Vibber, lcrocker, Tim Starling, 4 * Domas Mituzas, Antoine Musso, Jens Frank, Zhengzhu, 5 * 2006 Rob Church <robchur@gmail.com> 6 * 7 * This program is free software; you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation; either version 2 of the License, or 10 * (at your option) any later version. 11 * 12 * This program is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 * 17 * You should have received a copy of the GNU General Public License along 18 * with this program; if not, write to the Free Software Foundation, Inc., 19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 20 * http://www.gnu.org/copyleft/gpl.html 21 * 22 * @file 23 * @ingroup Pager 24 */ 25 26use MediaWiki\MediaWikiServices; 27 28/** 29 * This class is used to get a list of user. The ones with specials 30 * rights (sysop, bureaucrat, developer) will have them displayed 31 * next to their names. 32 * 33 * @ingroup Pager 34 */ 35class UsersPager extends AlphabeticPager { 36 37 /** 38 * @var array[] A array with user ids as key and a array of groups as value 39 */ 40 protected $userGroupCache; 41 42 /** @var string */ 43 public $requestedGroup; 44 45 /** @var bool */ 46 protected $editsOnly; 47 48 /** @var bool */ 49 protected $temporaryGroupsOnly; 50 51 /** @var bool */ 52 protected $creationSort; 53 54 /** @var bool|null */ 55 protected $including; 56 57 /** @var string */ 58 protected $requestedUser; 59 60 /** 61 * @param IContextSource|null $context 62 * @param array|null $par (Default null) 63 * @param bool|null $including Whether this page is being transcluded in 64 * another page 65 */ 66 public function __construct( IContextSource $context = null, $par = null, $including = null ) { 67 if ( $context ) { 68 $this->setContext( $context ); 69 } 70 71 $request = $this->getRequest(); 72 $par = $par ?? ''; 73 $parms = explode( '/', $par ); 74 $symsForAll = [ '*', 'user' ]; 75 76 if ( $parms[0] != '' && 77 ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) ) 78 ) { 79 $this->requestedGroup = $par; 80 $un = $request->getText( 'username' ); 81 } elseif ( count( $parms ) == 2 ) { 82 $this->requestedGroup = $parms[0]; 83 $un = $parms[1]; 84 } else { 85 $this->requestedGroup = $request->getVal( 'group' ); 86 $un = ( $par != '' ) ? $par : $request->getText( 'username' ); 87 } 88 89 if ( in_array( $this->requestedGroup, $symsForAll ) ) { 90 $this->requestedGroup = ''; 91 } 92 $this->editsOnly = $request->getBool( 'editsOnly' ); 93 $this->temporaryGroupsOnly = $request->getBool( 'temporaryGroupsOnly' ); 94 $this->creationSort = $request->getBool( 'creationSort' ); 95 $this->including = $including; 96 $this->mDefaultDirection = $request->getBool( 'desc' ) 97 ? IndexPager::DIR_DESCENDING 98 : IndexPager::DIR_ASCENDING; 99 100 $this->requestedUser = ''; 101 102 if ( $un != '' ) { 103 $username = Title::makeTitleSafe( NS_USER, $un ); 104 105 if ( $username !== null ) { 106 $this->requestedUser = $username->getText(); 107 } 108 } 109 110 parent::__construct(); 111 } 112 113 /** 114 * @return string 115 */ 116 public function getIndexField() { 117 return $this->creationSort ? 'user_id' : 'user_name'; 118 } 119 120 /** 121 * @return array 122 */ 123 public function getQueryInfo() { 124 $dbr = wfGetDB( DB_REPLICA ); 125 $conds = []; 126 127 // Don't show hidden names 128 if ( !MediaWikiServices::getInstance() 129 ->getPermissionManager() 130 ->userHasRight( $this->getUser(), 'hideuser' ) 131 ) { 132 $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0'; 133 } 134 135 $options = []; 136 137 if ( $this->requestedGroup != '' || $this->temporaryGroupsOnly ) { 138 $conds[] = 'ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) . 139 ( !$this->temporaryGroupsOnly ? ' OR ug_expiry IS NULL' : '' ); 140 } 141 142 if ( $this->requestedGroup != '' ) { 143 $conds['ug_group'] = $this->requestedGroup; 144 } 145 146 if ( $this->requestedUser != '' ) { 147 # Sorted either by account creation or name 148 if ( $this->creationSort ) { 149 $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) ); 150 } else { 151 $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser ); 152 } 153 } 154 155 if ( $this->editsOnly ) { 156 $conds[] = 'user_editcount > 0'; 157 } 158 159 $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name'; 160 161 $query = [ 162 'tables' => [ 'user', 'user_groups', 'ipblocks' ], 163 'fields' => [ 164 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name', 165 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)', 166 'edits' => 'MAX(user_editcount)', 167 'creation' => 'MIN(user_registration)', 168 'ipb_deleted' => 'MAX(ipb_deleted)', // block/hide status 169 'ipb_sitewide' => 'MAX(ipb_sitewide)' 170 ], 171 'options' => $options, 172 'join_conds' => [ 173 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ], 174 'ipblocks' => [ 175 'LEFT JOIN', [ 176 'user_id=ipb_user', 177 'ipb_auto' => 0 178 ] 179 ], 180 ], 181 'conds' => $conds 182 ]; 183 184 $this->getHookRunner()->onSpecialListusersQueryInfo( $this, $query ); 185 186 return $query; 187 } 188 189 /** 190 * @param stdClass $row 191 * @return string 192 */ 193 public function formatRow( $row ) { 194 if ( $row->user_id == 0 ) { # T18487 195 return ''; 196 } 197 198 $userName = $row->user_name; 199 200 $ulinks = Linker::userLink( $row->user_id, $userName ); 201 $ulinks .= Linker::userToolLinksRedContribs( 202 $row->user_id, 203 $userName, 204 (int)$row->edits, 205 // don't render parentheses in HTML markup (CSS will provide) 206 false 207 ); 208 209 $lang = $this->getLanguage(); 210 211 $groups = ''; 212 $ugms = self::getGroupMemberships( intval( $row->user_id ), $this->userGroupCache ); 213 214 if ( !$this->including && count( $ugms ) > 0 ) { 215 $list = []; 216 foreach ( $ugms as $ugm ) { 217 $list[] = $this->buildGroupLink( $ugm, $userName ); 218 } 219 $groups = $lang->commaList( $list ); 220 } 221 222 $item = $lang->specialList( $ulinks, $groups ); 223 224 if ( $row->ipb_deleted ) { 225 $item = "<span class=\"deleted\">$item</span>"; 226 } 227 228 $edits = ''; 229 if ( !$this->including && $this->getConfig()->get( 'Edititis' ) ) { 230 $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped(); 231 $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped(); 232 } 233 234 $created = ''; 235 # Some rows may be null 236 if ( !$this->including && $row->creation ) { 237 $user = $this->getUser(); 238 $d = $lang->userDate( $row->creation, $user ); 239 $t = $lang->userTime( $row->creation, $user ); 240 $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped(); 241 $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped(); 242 } 243 244 $blocked = $row->ipb_deleted !== null && $row->ipb_sitewide === '1' ? 245 ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : 246 ''; 247 248 $this->getHookRunner()->onSpecialListusersFormatRow( $item, $row ); 249 250 return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" ); 251 } 252 253 protected function doBatchLookups() { 254 $batch = new LinkBatch(); 255 $userIds = []; 256 # Give some pointers to make user links 257 foreach ( $this->mResult as $row ) { 258 $batch->add( NS_USER, $row->user_name ); 259 $batch->add( NS_USER_TALK, $row->user_name ); 260 $userIds[] = $row->user_id; 261 } 262 263 // Lookup groups for all the users 264 $dbr = wfGetDB( DB_REPLICA ); 265 $groupManager = MediaWikiServices::getInstance()->getUserGroupManager(); 266 $groupsQueryInfo = $groupManager->getQueryInfo(); 267 $groupRes = $dbr->select( 268 $groupsQueryInfo['tables'], 269 $groupsQueryInfo['fields'], 270 [ 'ug_user' => $userIds ], 271 __METHOD__, 272 $groupsQueryInfo['joins'] 273 ); 274 $cache = []; 275 $groups = []; 276 foreach ( $groupRes as $row ) { 277 $ugm = $groupManager->newGroupMembershipFromRow( $row ); 278 if ( !$ugm->isExpired() ) { 279 $cache[$row->ug_user][$row->ug_group] = $ugm; 280 $groups[$row->ug_group] = true; 281 } 282 } 283 284 // Give extensions a chance to add things like global user group data 285 // into the cache array to ensure proper output later on 286 $this->getHookRunner()->onUsersPagerDoBatchLookups( $dbr, $userIds, $cache, $groups ); 287 288 $this->userGroupCache = $cache; 289 290 // Add page of groups to link batch 291 foreach ( $groups as $group => $unused ) { 292 $groupPage = UserGroupMembership::getGroupPage( $group ); 293 if ( $groupPage ) { 294 $batch->addObj( $groupPage ); 295 } 296 } 297 298 $batch->execute(); 299 $this->mResult->rewind(); 300 } 301 302 /** 303 * @return string 304 */ 305 public function getPageHeader() { 306 $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0]; 307 308 $groupOptions = [ $this->msg( 'group-all' )->text() => '' ]; 309 foreach ( $this->getAllGroups() as $group => $groupText ) { 310 $groupOptions[ $groupText ] = $group; 311 } 312 313 $formDescriptor = [ 314 'user' => [ 315 'class' => HTMLUserTextField::class, 316 'label' => $this->msg( 'listusersfrom' )->text(), 317 'name' => 'username', 318 'default' => $this->requestedUser, 319 ], 320 'dropdown' => [ 321 'label' => $this->msg( 'group' )->text(), 322 'name' => 'group', 323 'default' => $this->requestedGroup, 324 'class' => HTMLSelectField::class, 325 'options' => $groupOptions, 326 ], 327 'editsOnly' => [ 328 'type' => 'check', 329 'label' => $this->msg( 'listusers-editsonly' )->text(), 330 'name' => 'editsOnly', 331 'id' => 'editsOnly', 332 'default' => $this->editsOnly 333 ], 334 'temporaryGroupsOnly' => [ 335 'type' => 'check', 336 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(), 337 'name' => 'temporaryGroupsOnly', 338 'id' => 'temporaryGroupsOnly', 339 'default' => $this->temporaryGroupsOnly 340 ], 341 'creationSort' => [ 342 'type' => 'check', 343 'label' => $this->msg( 'listusers-creationsort' )->text(), 344 'name' => 'creationSort', 345 'id' => 'creationSort', 346 'default' => $this->creationSort 347 ], 348 'desc' => [ 349 'type' => 'check', 350 'label' => $this->msg( 'listusers-desc' )->text(), 351 'name' => 'desc', 352 'id' => 'desc', 353 'default' => $this->mDefaultDirection 354 ], 355 'limithiddenfield' => [ 356 'class' => HTMLHiddenField::class, 357 'name' => 'limit', 358 'default' => $this->mLimit 359 ] 360 ]; 361 362 $beforeSubmitButtonHookOut = ''; 363 $this->getHookRunner()->onSpecialListusersHeaderForm( $this, $beforeSubmitButtonHookOut ); 364 365 if ( $beforeSubmitButtonHookOut !== '' ) { 366 $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [ 367 'class' => HTMLInfoField::class, 368 'raw' => true, 369 'default' => $beforeSubmitButtonHookOut 370 ]; 371 } 372 373 $formDescriptor[ 'submit' ] = [ 374 'class' => HTMLSubmitField::class, 375 'buttonlabel-message' => 'listusers-submit', 376 ]; 377 378 $beforeClosingFieldsetHookOut = ''; 379 $this->getHookRunner()->onSpecialListusersHeader( $this, $beforeClosingFieldsetHookOut ); 380 381 if ( $beforeClosingFieldsetHookOut !== '' ) { 382 $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [ 383 'class' => HTMLInfoField::class, 384 'raw' => true, 385 'default' => $beforeClosingFieldsetHookOut 386 ]; 387 } 388 389 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); 390 $htmlForm 391 ->setMethod( 'get' ) 392 ->setAction( Title::newFromText( $self )->getLocalURL() ) 393 ->setId( 'mw-listusers-form' ) 394 ->setFormIdentifier( 'mw-listusers-form' ) 395 ->suppressDefaultSubmit() 396 ->setWrapperLegendMsg( 'listusers' ); 397 return $htmlForm->prepareForm()->getHTML( true ); 398 } 399 400 /** 401 * Get a list of all explicit groups 402 * @return array 403 */ 404 private function getAllGroups() { 405 $result = []; 406 foreach ( User::getAllGroups() as $group ) { 407 $result[$group] = UserGroupMembership::getGroupName( $group ); 408 } 409 asort( $result ); 410 411 return $result; 412 } 413 414 /** 415 * Preserve group and username offset parameters when paging 416 * @return array 417 */ 418 public function getDefaultQuery() { 419 $query = parent::getDefaultQuery(); 420 if ( $this->requestedGroup != '' ) { 421 $query['group'] = $this->requestedGroup; 422 } 423 if ( $this->requestedUser != '' ) { 424 $query['username'] = $this->requestedUser; 425 } 426 $this->getHookRunner()->onSpecialListusersDefaultQuery( $this, $query ); 427 428 return $query; 429 } 430 431 /** 432 * Get an associative array containing groups the specified user belongs to, 433 * and the relevant UserGroupMembership objects 434 * 435 * @param int $uid User id 436 * @param array[]|null $cache 437 * @return UserGroupMembership[] (group name => UserGroupMembership object) 438 */ 439 protected static function getGroupMemberships( $uid, $cache = null ) { 440 if ( $cache === null ) { 441 $user = User::newFromId( $uid ); 442 return $user->getGroupMemberships(); 443 } else { 444 return $cache[$uid] ?? []; 445 } 446 } 447 448 /** 449 * Format a link to a group description page 450 * 451 * @param string|UserGroupMembership $group Group name or UserGroupMembership object 452 * @param string $username 453 * @return string 454 */ 455 protected function buildGroupLink( $group, $username ) { 456 return UserGroupMembership::getLink( $group, $this->getContext(), 'html', $username ); 457 } 458} 459