UsersPager.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  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. use MediaWiki\MediaWikiServices;
  26. /**
  27. * This class is used to get a list of user. The ones with specials
  28. * rights (sysop, bureaucrat, developer) will have them displayed
  29. * next to their names.
  30. *
  31. * @ingroup Pager
  32. */
  33. class UsersPager extends AlphabeticPager {
  34. /**
  35. * @var array[] A array with user ids as key and a array of groups as value
  36. */
  37. protected $userGroupCache;
  38. /** @var string */
  39. protected $requestedGroup;
  40. /** @var bool */
  41. protected $editsOnly;
  42. /** @var bool */
  43. protected $temporaryGroupsOnly;
  44. /** @var bool */
  45. protected $creationSort;
  46. /** @var bool|null */
  47. protected $including;
  48. /** @var string */
  49. protected $requestedUser;
  50. /**
  51. * @param IContextSource|null $context
  52. * @param array|null $par (Default null)
  53. * @param bool|null $including Whether this page is being transcluded in
  54. * another page
  55. */
  56. public function __construct( IContextSource $context = null, $par = null, $including = null ) {
  57. if ( $context ) {
  58. $this->setContext( $context );
  59. }
  60. $request = $this->getRequest();
  61. $par = $par ?? '';
  62. $parms = explode( '/', $par );
  63. $symsForAll = [ '*', 'user' ];
  64. if ( $parms[0] != '' &&
  65. ( in_array( $par, User::getAllGroups() ) || in_array( $par, $symsForAll ) )
  66. ) {
  67. $this->requestedGroup = $par;
  68. $un = $request->getText( 'username' );
  69. } elseif ( count( $parms ) == 2 ) {
  70. $this->requestedGroup = $parms[0];
  71. $un = $parms[1];
  72. } else {
  73. $this->requestedGroup = $request->getVal( 'group' );
  74. $un = ( $par != '' ) ? $par : $request->getText( 'username' );
  75. }
  76. if ( in_array( $this->requestedGroup, $symsForAll ) ) {
  77. $this->requestedGroup = '';
  78. }
  79. $this->editsOnly = $request->getBool( 'editsOnly' );
  80. $this->temporaryGroupsOnly = $request->getBool( 'temporaryGroupsOnly' );
  81. $this->creationSort = $request->getBool( 'creationSort' );
  82. $this->including = $including;
  83. $this->mDefaultDirection = $request->getBool( 'desc' )
  84. ? IndexPager::DIR_DESCENDING
  85. : IndexPager::DIR_ASCENDING;
  86. $this->requestedUser = '';
  87. if ( $un != '' ) {
  88. $username = Title::makeTitleSafe( NS_USER, $un );
  89. if ( !is_null( $username ) ) {
  90. $this->requestedUser = $username->getText();
  91. }
  92. }
  93. parent::__construct();
  94. }
  95. /**
  96. * @return string
  97. */
  98. function getIndexField() {
  99. return $this->creationSort ? 'user_id' : 'user_name';
  100. }
  101. /**
  102. * @return array
  103. */
  104. function getQueryInfo() {
  105. $dbr = wfGetDB( DB_REPLICA );
  106. $conds = [];
  107. // Don't show hidden names
  108. if ( !MediaWikiServices::getInstance()
  109. ->getPermissionManager()
  110. ->userHasRight( $this->getUser(), 'hideuser' )
  111. ) {
  112. $conds[] = 'ipb_deleted IS NULL OR ipb_deleted = 0';
  113. }
  114. $options = [];
  115. if ( $this->requestedGroup != '' || $this->temporaryGroupsOnly ) {
  116. $conds[] = 'ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ) .
  117. ( !$this->temporaryGroupsOnly ? ' OR ug_expiry IS NULL' : '' );
  118. }
  119. if ( $this->requestedGroup != '' ) {
  120. $conds['ug_group'] = $this->requestedGroup;
  121. }
  122. if ( $this->requestedUser != '' ) {
  123. # Sorted either by account creation or name
  124. if ( $this->creationSort ) {
  125. $conds[] = 'user_id >= ' . intval( User::idFromName( $this->requestedUser ) );
  126. } else {
  127. $conds[] = 'user_name >= ' . $dbr->addQuotes( $this->requestedUser );
  128. }
  129. }
  130. if ( $this->editsOnly ) {
  131. $conds[] = 'user_editcount > 0';
  132. }
  133. $options['GROUP BY'] = $this->creationSort ? 'user_id' : 'user_name';
  134. $query = [
  135. 'tables' => [ 'user', 'user_groups', 'ipblocks' ],
  136. 'fields' => [
  137. 'user_name' => $this->creationSort ? 'MAX(user_name)' : 'user_name',
  138. 'user_id' => $this->creationSort ? 'user_id' : 'MAX(user_id)',
  139. 'edits' => 'MAX(user_editcount)',
  140. 'creation' => 'MIN(user_registration)',
  141. 'ipb_deleted' => 'MAX(ipb_deleted)', // block/hide status
  142. 'ipb_sitewide' => 'MAX(ipb_sitewide)'
  143. ],
  144. 'options' => $options,
  145. 'join_conds' => [
  146. 'user_groups' => [ 'LEFT JOIN', 'user_id=ug_user' ],
  147. 'ipblocks' => [
  148. 'LEFT JOIN', [
  149. 'user_id=ipb_user',
  150. 'ipb_auto' => 0
  151. ]
  152. ],
  153. ],
  154. 'conds' => $conds
  155. ];
  156. Hooks::run( 'SpecialListusersQueryInfo', [ $this, &$query ] );
  157. return $query;
  158. }
  159. /**
  160. * @param stdClass $row
  161. * @return string
  162. */
  163. function formatRow( $row ) {
  164. if ( $row->user_id == 0 ) { # T18487
  165. return '';
  166. }
  167. $userName = $row->user_name;
  168. $ulinks = Linker::userLink( $row->user_id, $userName );
  169. $ulinks .= Linker::userToolLinksRedContribs(
  170. $row->user_id,
  171. $userName,
  172. (int)$row->edits,
  173. // don't render parentheses in HTML markup (CSS will provide)
  174. false
  175. );
  176. $lang = $this->getLanguage();
  177. $groups = '';
  178. $ugms = self::getGroupMemberships( intval( $row->user_id ), $this->userGroupCache );
  179. if ( !$this->including && count( $ugms ) > 0 ) {
  180. $list = [];
  181. foreach ( $ugms as $ugm ) {
  182. $list[] = $this->buildGroupLink( $ugm, $userName );
  183. }
  184. $groups = $lang->commaList( $list );
  185. }
  186. $item = $lang->specialList( $ulinks, $groups );
  187. if ( $row->ipb_deleted ) {
  188. $item = "<span class=\"deleted\">$item</span>";
  189. }
  190. $edits = '';
  191. if ( !$this->including && $this->getConfig()->get( 'Edititis' ) ) {
  192. $count = $this->msg( 'usereditcount' )->numParams( $row->edits )->escaped();
  193. $edits = $this->msg( 'word-separator' )->escaped() . $this->msg( 'brackets', $count )->escaped();
  194. }
  195. $created = '';
  196. # Some rows may be null
  197. if ( !$this->including && $row->creation ) {
  198. $user = $this->getUser();
  199. $d = $lang->userDate( $row->creation, $user );
  200. $t = $lang->userTime( $row->creation, $user );
  201. $created = $this->msg( 'usercreated', $d, $t, $row->user_name )->escaped();
  202. $created = ' ' . $this->msg( 'parentheses' )->rawParams( $created )->escaped();
  203. }
  204. $blocked = !is_null( $row->ipb_deleted ) && $row->ipb_sitewide === '1' ?
  205. ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() :
  206. '';
  207. Hooks::run( 'SpecialListusersFormatRow', [ &$item, $row ] );
  208. return Html::rawElement( 'li', [], "{$item}{$edits}{$created}{$blocked}" );
  209. }
  210. protected function doBatchLookups() {
  211. $batch = new LinkBatch();
  212. $userIds = [];
  213. # Give some pointers to make user links
  214. foreach ( $this->mResult as $row ) {
  215. $batch->add( NS_USER, $row->user_name );
  216. $batch->add( NS_USER_TALK, $row->user_name );
  217. $userIds[] = $row->user_id;
  218. }
  219. // Lookup groups for all the users
  220. $dbr = wfGetDB( DB_REPLICA );
  221. $groupRes = $dbr->select(
  222. 'user_groups',
  223. UserGroupMembership::selectFields(),
  224. [ 'ug_user' => $userIds ],
  225. __METHOD__
  226. );
  227. $cache = [];
  228. $groups = [];
  229. foreach ( $groupRes as $row ) {
  230. $ugm = UserGroupMembership::newFromRow( $row );
  231. if ( !$ugm->isExpired() ) {
  232. $cache[$row->ug_user][$row->ug_group] = $ugm;
  233. $groups[$row->ug_group] = true;
  234. }
  235. }
  236. // Give extensions a chance to add things like global user group data
  237. // into the cache array to ensure proper output later on
  238. Hooks::run( 'UsersPagerDoBatchLookups', [ $dbr, $userIds, &$cache, &$groups ] );
  239. $this->userGroupCache = $cache;
  240. // Add page of groups to link batch
  241. foreach ( $groups as $group => $unused ) {
  242. $groupPage = UserGroupMembership::getGroupPage( $group );
  243. if ( $groupPage ) {
  244. $batch->addObj( $groupPage );
  245. }
  246. }
  247. $batch->execute();
  248. $this->mResult->rewind();
  249. }
  250. /**
  251. * @return string
  252. */
  253. function getPageHeader() {
  254. $self = explode( '/', $this->getTitle()->getPrefixedDBkey(), 2 )[0];
  255. $groupOptions = [ $this->msg( 'group-all' )->text() => '' ];
  256. foreach ( $this->getAllGroups() as $group => $groupText ) {
  257. $groupOptions[ $groupText ] = $group;
  258. }
  259. $formDescriptor = [
  260. 'user' => [
  261. 'class' => HTMLUserTextField::class,
  262. 'label' => $this->msg( 'listusersfrom' )->text(),
  263. 'name' => 'username',
  264. 'default' => $this->requestedUser,
  265. ],
  266. 'dropdown' => [
  267. 'label' => $this->msg( 'group' )->text(),
  268. 'name' => 'group',
  269. 'default' => $this->requestedGroup,
  270. 'class' => HTMLSelectField::class,
  271. 'options' => $groupOptions,
  272. ],
  273. 'editsOnly' => [
  274. 'type' => 'check',
  275. 'label' => $this->msg( 'listusers-editsonly' )->text(),
  276. 'name' => 'editsOnly',
  277. 'id' => 'editsOnly',
  278. 'default' => $this->editsOnly
  279. ],
  280. 'temporaryGroupsOnly' => [
  281. 'type' => 'check',
  282. 'label' => $this->msg( 'listusers-temporarygroupsonly' )->text(),
  283. 'name' => 'temporaryGroupsOnly',
  284. 'id' => 'temporaryGroupsOnly',
  285. 'default' => $this->temporaryGroupsOnly
  286. ],
  287. 'creationSort' => [
  288. 'type' => 'check',
  289. 'label' => $this->msg( 'listusers-creationsort' )->text(),
  290. 'name' => 'creationSort',
  291. 'id' => 'creationSort',
  292. 'default' => $this->creationSort
  293. ],
  294. 'desc' => [
  295. 'type' => 'check',
  296. 'label' => $this->msg( 'listusers-desc' )->text(),
  297. 'name' => 'desc',
  298. 'id' => 'desc',
  299. 'default' => $this->mDefaultDirection
  300. ],
  301. 'limithiddenfield' => [
  302. 'class' => HTMLHiddenField::class,
  303. 'name' => 'limit',
  304. 'default' => $this->mLimit
  305. ]
  306. ];
  307. $beforeSubmitButtonHookOut = '';
  308. Hooks::run( 'SpecialListusersHeaderForm', [ $this, &$beforeSubmitButtonHookOut ] );
  309. if ( $beforeSubmitButtonHookOut !== '' ) {
  310. $formDescriptor[ 'beforeSubmitButtonHookOut' ] = [
  311. 'class' => HTMLInfoField::class,
  312. 'raw' => true,
  313. 'default' => $beforeSubmitButtonHookOut
  314. ];
  315. }
  316. $formDescriptor[ 'submit' ] = [
  317. 'class' => HTMLSubmitField::class,
  318. 'buttonlabel-message' => 'listusers-submit',
  319. ];
  320. $beforeClosingFieldsetHookOut = '';
  321. Hooks::run( 'SpecialListusersHeader', [ $this, &$beforeClosingFieldsetHookOut ] );
  322. if ( $beforeClosingFieldsetHookOut !== '' ) {
  323. $formDescriptor[ 'beforeClosingFieldsetHookOut' ] = [
  324. 'class' => HTMLInfoField::class,
  325. 'raw' => true,
  326. 'default' => $beforeClosingFieldsetHookOut
  327. ];
  328. }
  329. $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
  330. $htmlForm
  331. ->setMethod( 'get' )
  332. ->setAction( Title::newFromText( $self )->getLocalURL() )
  333. ->setId( 'mw-listusers-form' )
  334. ->setFormIdentifier( 'mw-listusers-form' )
  335. ->suppressDefaultSubmit()
  336. ->setWrapperLegendMsg( 'listusers' );
  337. return $htmlForm->prepareForm()->getHTML( true );
  338. }
  339. /**
  340. * Get a list of all explicit groups
  341. * @return array
  342. */
  343. function getAllGroups() {
  344. $result = [];
  345. foreach ( User::getAllGroups() as $group ) {
  346. $result[$group] = UserGroupMembership::getGroupName( $group );
  347. }
  348. asort( $result );
  349. return $result;
  350. }
  351. /**
  352. * Preserve group and username offset parameters when paging
  353. * @return array
  354. */
  355. function getDefaultQuery() {
  356. $query = parent::getDefaultQuery();
  357. if ( $this->requestedGroup != '' ) {
  358. $query['group'] = $this->requestedGroup;
  359. }
  360. if ( $this->requestedUser != '' ) {
  361. $query['username'] = $this->requestedUser;
  362. }
  363. Hooks::run( 'SpecialListusersDefaultQuery', [ $this, &$query ] );
  364. return $query;
  365. }
  366. /**
  367. * Get an associative array containing groups the specified user belongs to,
  368. * and the relevant UserGroupMembership objects
  369. *
  370. * @param int $uid User id
  371. * @param array[]|null $cache
  372. * @return UserGroupMembership[] (group name => UserGroupMembership object)
  373. */
  374. protected static function getGroupMemberships( $uid, $cache = null ) {
  375. if ( $cache === null ) {
  376. $user = User::newFromId( $uid );
  377. return $user->getGroupMemberships();
  378. } else {
  379. return $cache[$uid] ?? [];
  380. }
  381. }
  382. /**
  383. * Format a link to a group description page
  384. *
  385. * @param string|UserGroupMembership $group Group name or UserGroupMembership object
  386. * @param string $username
  387. * @return string
  388. */
  389. protected function buildGroupLink( $group, $username ) {
  390. return UserGroupMembership::getLink( $group, $this->getContext(), 'html', $username );
  391. }
  392. }