1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 * @ingroup Pager 20 */ 21 22use MediaWiki\Cache\LinkBatchFactory; 23use MediaWiki\HookContainer\HookContainer; 24use MediaWiki\User\UserGroupManager; 25use Wikimedia\Rdbms\ILoadBalancer; 26 27/** 28 * This class is used to get a list of active users. The ones with specials 29 * rights (sysop, bureaucrat, developer) will have them displayed 30 * next to their names. 31 * 32 * @ingroup Pager 33 */ 34class ActiveUsersPager extends UsersPager { 35 36 /** 37 * @var FormOptions 38 */ 39 protected $opts; 40 41 /** 42 * @var string[] 43 */ 44 protected $groups; 45 46 /** 47 * @var array 48 */ 49 private $blockStatusByUid; 50 51 /** @var int */ 52 private $RCMaxAge; 53 54 /** @var string[] */ 55 private $excludegroups; 56 57 /** 58 * @param IContextSource|null $context 59 * @param FormOptions $opts 60 * @param LinkBatchFactory|null $linkBatchFactory 61 * @param HookContainer $hookContainer 62 * @param ILoadBalancer $loadBalancer 63 * @param UserGroupManager $userGroupManager 64 */ 65 public function __construct( 66 ?IContextSource $context, 67 FormOptions $opts, 68 LinkBatchFactory $linkBatchFactory, 69 HookContainer $hookContainer, 70 ILoadBalancer $loadBalancer, 71 UserGroupManager $userGroupManager 72 ) { 73 parent::__construct( 74 $context, 75 null, 76 null, 77 $linkBatchFactory, 78 $hookContainer, 79 $loadBalancer, 80 $userGroupManager 81 ); 82 83 $this->RCMaxAge = $this->getConfig()->get( 'ActiveUserDays' ); 84 $this->requestedUser = ''; 85 86 $un = $opts->getValue( 'username' ); 87 if ( $un != '' ) { 88 $username = Title::makeTitleSafe( NS_USER, $un ); 89 if ( $username !== null ) { 90 $this->requestedUser = $username->getText(); 91 } 92 } 93 94 $this->groups = $opts->getValue( 'groups' ); 95 $this->excludegroups = $opts->getValue( 'excludegroups' ); 96 // Backwards-compatibility with old URLs 97 if ( $opts->getValue( 'hidebots' ) ) { 98 $this->excludegroups[] = 'bot'; 99 } 100 if ( $opts->getValue( 'hidesysops' ) ) { 101 $this->excludegroups[] = 'sysop'; 102 } 103 } 104 105 public function getIndexField() { 106 return 'qcc_title'; 107 } 108 109 public function getQueryInfo( $data = null ) { 110 $dbr = $this->getDatabase(); 111 112 $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400; 113 $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); 114 $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')'; 115 116 // Inner subselect to pull the active users out of querycachetwo 117 $tables = [ 'querycachetwo', 'user', 'actor' ]; 118 $fields = [ 'qcc_title', 'user_id', 'actor_id' ]; 119 $jconds = [ 120 'user' => [ 'JOIN', 'user_name = qcc_title' ], 121 'actor' => [ 'JOIN', 'actor_user = user_id' ], 122 ]; 123 $conds = [ 124 'qcc_type' => 'activeusers', 125 'qcc_namespace' => NS_USER, 126 ]; 127 $options = []; 128 if ( $data !== null ) { 129 $options['ORDER BY'] = 'qcc_title ' . $data['order']; 130 $options['LIMIT'] = $data['limit']; 131 $conds = array_merge( $conds, $data['conds'] ); 132 } 133 if ( $this->requestedUser != '' ) { 134 $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser ); 135 } 136 if ( $this->groups !== [] ) { 137 $tables['ug1'] = 'user_groups'; 138 $jconds['ug1'] = [ 'JOIN', 'ug1.ug_user = user_id' ]; 139 $conds['ug1.ug_group'] = $this->groups; 140 $conds[] = 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ); 141 } 142 if ( $this->excludegroups !== [] ) { 143 $tables['ug2'] = 'user_groups'; 144 $jconds['ug2'] = [ 'LEFT JOIN', [ 145 'ug2.ug_user = user_id', 146 'ug2.ug_group' => $this->excludegroups, 147 'ug2.ug_expiry IS NULL OR ug2.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ), 148 ] ]; 149 $conds['ug2.ug_user'] = null; 150 } 151 if ( !$this->canSeeHideuser() ) { 152 $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText( 153 'ipblocks', '1', [ 'ipb_user=user_id', 'ipb_deleted' => 1 ], __METHOD__ 154 ) . ')'; 155 } 156 $subquery = $dbr->buildSelectSubquery( $tables, $fields, $conds, $fname, $options, $jconds ); 157 158 // Outer query to select the recent edit counts for the selected active users 159 $tables = [ 'qcc_users' => $subquery, 'recentchanges' ]; 160 $jconds = [ 'recentchanges' => [ 'LEFT JOIN', [ 161 'rc_actor = actor_id', 162 'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata. 163 'rc_type != ' . $dbr->addQuotes( RC_CATEGORIZE ), // Don't count categorization changes. 164 'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ), 165 'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ), 166 ] ] ]; 167 $conds = []; 168 169 return [ 170 'tables' => $tables, 171 'fields' => [ 172 'qcc_title', 173 'user_name' => 'qcc_title', 174 'user_id' => 'user_id', 175 'recentedits' => 'COUNT(rc_id)' 176 ], 177 'options' => [ 'GROUP BY' => [ 'qcc_title', 'user_id' ] ], 178 'conds' => $conds, 179 'join_conds' => $jconds, 180 ]; 181 } 182 183 protected function buildQueryInfo( $offset, $limit, $order ) { 184 $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')'; 185 186 $sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields ); 187 if ( $order === self::QUERY_ASCENDING ) { 188 $dir = 'ASC'; 189 $orderBy = $sortColumns; 190 $operator = $this->mIncludeOffset ? '>=' : '>'; 191 } else { 192 $dir = 'DESC'; 193 $orderBy = []; 194 foreach ( $sortColumns as $col ) { 195 $orderBy[] = $col . ' DESC'; 196 } 197 $operator = $this->mIncludeOffset ? '<=' : '<'; 198 } 199 $info = $this->getQueryInfo( [ 200 'limit' => intval( $limit ), 201 'order' => $dir, 202 'conds' => 203 $offset != '' ? [ $this->mIndexField . $operator . $this->getDatabase()->addQuotes( $offset ) ] : [], 204 ] ); 205 206 $tables = $info['tables']; 207 $fields = $info['fields']; 208 $conds = $info['conds']; 209 $options = $info['options']; 210 $join_conds = $info['join_conds']; 211 $options['ORDER BY'] = $orderBy; 212 return [ $tables, $fields, $conds, $fname, $options, $join_conds ]; 213 } 214 215 protected function doBatchLookups() { 216 parent::doBatchLookups(); 217 218 $uids = []; 219 foreach ( $this->mResult as $row ) { 220 $uids[] = $row->user_id; 221 } 222 // Fetch the block status of the user for showing "(blocked)" text and for 223 // striking out names of suppressed users when privileged user views the list. 224 // Although the first query already hits the block table for un-privileged, this 225 // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct. 226 $dbr = $this->getDatabase(); 227 $res = $dbr->select( 'ipblocks', 228 [ 'ipb_user', 'deleted' => 'MAX(ipb_deleted)', 'sitewide' => 'MAX(ipb_sitewide)' ], 229 [ 'ipb_user' => $uids ], 230 __METHOD__, 231 [ 'GROUP BY' => [ 'ipb_user' ] ] 232 ); 233 $this->blockStatusByUid = []; 234 foreach ( $res as $row ) { 235 $this->blockStatusByUid[$row->ipb_user] = [ 236 'deleted' => $row->deleted, 237 'sitewide' => $row->sitewide, 238 ]; 239 } 240 $this->mResult->seek( 0 ); 241 } 242 243 public function formatRow( $row ) { 244 $userName = $row->user_name; 245 246 $ulinks = Linker::userLink( $row->user_id, $userName ); 247 $ulinks .= Linker::userToolLinks( 248 $row->user_id, 249 $userName, 250 // Should the contributions link be red if the user has no edits (using default) 251 false, 252 // Customisation flags (using default 0) 253 0, 254 // User edit count (using default) 255 null, 256 // do not wrap the message in parentheses (CSS will provide these) 257 false 258 ); 259 260 $lang = $this->getLanguage(); 261 262 $list = []; 263 264 $ugms = self::getGroupMemberships( intval( $row->user_id ), $this->userGroupCache ); 265 foreach ( $ugms as $ugm ) { 266 $list[] = $this->buildGroupLink( $ugm, $userName ); 267 } 268 269 $groups = $lang->commaList( $list ); 270 271 $item = $lang->specialList( $ulinks, $groups ); 272 273 // If there is a block, 'deleted' and 'sitewide' are both set on 274 // $this->blockStatusByUid[$row->user_id]. 275 $blocked = ''; 276 $isBlocked = isset( $this->blockStatusByUid[$row->user_id] ); 277 if ( $isBlocked ) { 278 if ( $this->blockStatusByUid[$row->user_id]['deleted'] == 1 ) { 279 $item = "<span class=\"deleted\">$item</span>"; 280 } 281 if ( $this->blockStatusByUid[$row->user_id]['sitewide'] == 1 ) { 282 $blocked = ' ' . $this->msg( 'listusers-blocked', $userName )->escaped(); 283 } 284 } 285 $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits ) 286 ->params( $userName )->numParams( $this->RCMaxAge )->escaped(); 287 288 return Html::rawElement( 'li', [], "{$item} [{$count}]{$blocked}" ); 289 } 290 291} 292