Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActiveUsersPager
0.00% covered (danger)
0.00%
0 / 169
0.00% covered (danger)
0.00%
0 / 6
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
42
 buildQueryInfo
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
20
 formatRow
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
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
22namespace MediaWiki\Pager;
23
24use MediaWiki\Block\HideUserUtils;
25use MediaWiki\Cache\LinkBatchFactory;
26use MediaWiki\Context\IContextSource;
27use MediaWiki\HookContainer\HookContainer;
28use MediaWiki\Html\FormOptions;
29use MediaWiki\Html\Html;
30use MediaWiki\Linker\Linker;
31use MediaWiki\MainConfigNames;
32use MediaWiki\Title\Title;
33use MediaWiki\User\UserGroupManager;
34use MediaWiki\User\UserIdentityLookup;
35use MediaWiki\User\UserIdentityValue;
36use Wikimedia\Rdbms\IConnectionProvider;
37
38/**
39 * This class is used to get a list of active users. The ones with specials
40 * rights (sysop, bureaucrat, developer) will have them displayed
41 * next to their names.
42 *
43 * @ingroup Pager
44 */
45class ActiveUsersPager extends UsersPager {
46    /**
47     * @var FormOptions
48     */
49    protected $opts;
50
51    /**
52     * @var string[]
53     */
54    protected $groups;
55
56    /**
57     * @var array
58     */
59    private $blockStatusByUid;
60
61    /** @var int */
62    private $RCMaxAge;
63
64    /** @var string[] */
65    private $excludegroups;
66
67    /**
68     * @param IContextSource $context
69     * @param HookContainer $hookContainer
70     * @param LinkBatchFactory $linkBatchFactory
71     * @param IConnectionProvider $dbProvider
72     * @param UserGroupManager $userGroupManager
73     * @param UserIdentityLookup $userIdentityLookup
74     * @param HideUserUtils $hideUserUtils
75     * @param FormOptions $opts
76     */
77    public function __construct(
78        IContextSource $context,
79        HookContainer $hookContainer,
80        LinkBatchFactory $linkBatchFactory,
81        IConnectionProvider $dbProvider,
82        UserGroupManager $userGroupManager,
83        UserIdentityLookup $userIdentityLookup,
84        HideUserUtils $hideUserUtils,
85        FormOptions $opts
86    ) {
87        parent::__construct(
88            $context,
89            $hookContainer,
90            $linkBatchFactory,
91            $dbProvider,
92            $userGroupManager,
93            $userIdentityLookup,
94            $hideUserUtils,
95            null,
96            null
97        );
98
99        $this->RCMaxAge = $this->getConfig()->get( MainConfigNames::ActiveUserDays );
100        $this->requestedUser = '';
101
102        $un = $opts->getValue( 'username' );
103        if ( $un != '' ) {
104            $username = Title::makeTitleSafe( NS_USER, $un );
105            if ( $username !== null ) {
106                $this->requestedUser = $username->getText();
107            }
108        }
109
110        $this->groups = $opts->getValue( 'groups' );
111        $this->excludegroups = $opts->getValue( 'excludegroups' );
112        // Backwards-compatibility with old URLs
113        if ( $opts->getValue( 'hidebots' ) ) {
114            $this->excludegroups[] = 'bot';
115        }
116        if ( $opts->getValue( 'hidesysops' ) ) {
117            $this->excludegroups[] = 'sysop';
118        }
119    }
120
121    public function getIndexField() {
122        return 'qcc_title';
123    }
124
125    public function getQueryInfo( $data = null ) {
126        $dbr = $this->getDatabase();
127
128        $activeUserSeconds = $this->getConfig()->get( MainConfigNames::ActiveUserDays ) * 86400;
129        $timestamp = $dbr->timestamp( (int)wfTimestamp( TS_UNIX ) - $activeUserSeconds );
130        $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
131
132        // Inner subselect to pull the active users out of querycachetwo
133        $tables = [ 'querycachetwo', 'user', 'actor' ];
134        $fields = [ 'qcc_title', 'user_id', 'actor_id' ];
135        $jconds = [
136            'user' => [ 'JOIN', 'user_name = qcc_title' ],
137            'actor' => [ 'JOIN', 'actor_user = user_id' ],
138        ];
139        $conds = [
140            'qcc_type' => 'activeusers',
141            'qcc_namespace' => NS_USER,
142        ];
143        $options = [];
144        if ( $data !== null ) {
145            $options['ORDER BY'] = 'qcc_title ' . $data['order'];
146            $options['LIMIT'] = $data['limit'];
147            $conds = array_merge( $conds, $data['conds'] );
148        }
149        if ( $this->requestedUser != '' ) {
150            $conds[] = $dbr->expr( 'qcc_title', '>=', $this->requestedUser );
151        }
152        if ( $this->groups !== [] ) {
153            $tables['ug1'] = 'user_groups';
154            $jconds['ug1'] = [ 'JOIN', 'ug1.ug_user = user_id' ];
155            $conds['ug1.ug_group'] = $this->groups;
156            $conds[] = 'ug1.ug_expiry IS NULL OR ug1.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() );
157        }
158        if ( $this->excludegroups !== [] ) {
159            $tables['ug2'] = 'user_groups';
160            $jconds['ug2'] = [ 'LEFT JOIN', [
161                'ug2.ug_user = user_id',
162                'ug2.ug_group' => $this->excludegroups,
163                'ug2.ug_expiry IS NULL OR ug2.ug_expiry >= ' . $dbr->addQuotes( $dbr->timestamp() ),
164            ] ];
165            $conds['ug2.ug_user'] = null;
166        }
167        if ( !$this->canSeeHideuser() ) {
168            $conds[] = $this->hideUserUtils->getExpression( $dbr );
169        }
170        $subquery = $dbr->buildSelectSubquery( $tables, $fields, $conds, $fname, $options, $jconds );
171
172        // Outer query to select the recent edit counts for the selected active users
173        $tables = [ 'qcc_users' => $subquery, 'recentchanges' ];
174        $jconds = [ 'recentchanges' => [ 'LEFT JOIN', [
175            'rc_actor = actor_id',
176            $dbr->expr( 'rc_type', '!=', RC_EXTERNAL ), // Don't count wikidata.
177            $dbr->expr( 'rc_type', '!=', RC_CATEGORIZE ), // Don't count categorization changes.
178            $dbr->expr( 'rc_log_type', '=', null )->or( 'rc_log_type', '!=', 'newusers' ),
179            $dbr->expr( 'rc_timestamp', '>=', $timestamp ),
180        ] ] ];
181        $conds = [];
182
183        return [
184            'tables' => $tables,
185            'fields' => [
186                'qcc_title',
187                'user_name' => 'qcc_title',
188                'user_id' => 'user_id',
189                'recentedits' => 'COUNT(DISTINCT rc_id)'
190            ],
191            'options' => [ 'GROUP BY' => [ 'qcc_title', 'user_id' ] ],
192            'conds' => $conds,
193            'join_conds' => $jconds,
194        ];
195    }
196
197    protected function buildQueryInfo( $offset, $limit, $order ) {
198        $fname = __METHOD__ . ' (' . $this->getSqlComment() . ')';
199
200        $sortColumns = array_merge( [ $this->mIndexField ], $this->mExtraSortFields );
201        if ( $order === self::QUERY_ASCENDING ) {
202            $dir = 'ASC';
203            $orderBy = $sortColumns;
204            $operator = $this->mIncludeOffset ? '>=' : '>';
205        } else {
206            $dir = 'DESC';
207            $orderBy = [];
208            foreach ( $sortColumns as $col ) {
209                $orderBy[] = $col . ' DESC';
210            }
211            $operator = $this->mIncludeOffset ? '<=' : '<';
212        }
213        $info = $this->getQueryInfo( [
214            'limit' => intval( $limit ),
215            'order' => $dir,
216            'conds' =>
217                $offset != '' ? [ $this->mIndexField . $operator . $this->getDatabase()->addQuotes( $offset ) ] : [],
218        ] );
219
220        $tables = $info['tables'];
221        $fields = $info['fields'];
222        $conds = $info['conds'];
223        $options = $info['options'];
224        $join_conds = $info['join_conds'];
225        $options['ORDER BY'] = $orderBy;
226        return [ $tables, $fields, $conds, $fname, $options, $join_conds ];
227    }
228
229    protected function doBatchLookups() {
230        parent::doBatchLookups();
231
232        $uids = [];
233        foreach ( $this->mResult as $row ) {
234            $uids[] = (int)$row->user_id;
235        }
236        // Fetch the block status of the user for showing "(blocked)" text and for
237        // striking out names of suppressed users when privileged user views the list.
238        // Although the first query already hits the block table for un-privileged, this
239        // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct.
240        $dbr = $this->getDatabase();
241        if ( $this->blockTargetReadStage === SCHEMA_COMPAT_READ_OLD ) {
242            $res = $dbr->newSelectQueryBuilder()
243                ->select( [
244                    'bt_user' => 'ipb_user',
245                    'deleted' => 'MAX(ipb_deleted)',
246                    'sitewide' => 'MAX(ipb_sitewide)'
247                ] )
248                ->from( 'ipblocks' )
249                ->where( [ 'ipb_user' => $uids ] )
250                ->groupBy( [ 'ipb_user' ] )
251                ->caller( __METHOD__ )->fetchResultSet();
252        } else {
253            $res = $dbr->newSelectQueryBuilder()
254                ->select( [
255                    'bt_user',
256                    'deleted' => 'MAX(bl_deleted)',
257                    'sitewide' => 'MAX(bl_sitewide)'
258                ] )
259                ->from( 'block_target' )
260                ->join( 'block', null, 'bl_target=bt_id' )
261                ->where( [ 'bt_user' => $uids ] )
262                ->groupBy( [ 'bt_user' ] )
263                ->caller( __METHOD__ )->fetchResultSet();
264        }
265        $this->blockStatusByUid = [];
266        foreach ( $res as $row ) {
267            $this->blockStatusByUid[$row->bt_user] = [
268                'deleted' => $row->deleted,
269                'sitewide' => $row->sitewide,
270            ];
271        }
272        $this->mResult->seek( 0 );
273    }
274
275    public function formatRow( $row ) {
276        $userName = $row->user_name;
277
278        $ulinks = Linker::userLink( $row->user_id, $userName );
279        $ulinks .= Linker::userToolLinks(
280            $row->user_id,
281            $userName,
282            // Should the contributions link be red if the user has no edits (using default)
283            false,
284            // Customisation flags (using default 0)
285            0,
286            // User edit count (using default)
287            null,
288            // do not wrap the message in parentheses (CSS will provide these)
289            false
290        );
291
292        $lang = $this->getLanguage();
293
294        $list = [];
295
296        $userIdentity = new UserIdentityValue( intval( $row->user_id ), $userName );
297        $ugms = $this->getGroupMemberships( $userIdentity );
298        foreach ( $ugms as $ugm ) {
299            $list[] = $this->buildGroupLink( $ugm, $userName );
300        }
301
302        $groups = $lang->commaList( $list );
303
304        $item = $lang->specialList( $ulinks, $groups );
305
306        // If there is a block, 'deleted' and 'sitewide' are both set on
307        // $this->blockStatusByUid[$row->user_id].
308        $blocked = '';
309        $isBlocked = isset( $this->blockStatusByUid[$row->user_id] );
310        if ( $isBlocked ) {
311            if ( $this->blockStatusByUid[$row->user_id]['deleted'] == 1 ) {
312                $item = "<span class=\"deleted\">$item</span>";
313            }
314            if ( $this->blockStatusByUid[$row->user_id]['sitewide'] == 1 ) {
315                $blocked = ' ' . $this->msg( 'listusers-blocked', $userName )->escaped();
316            }
317        }
318        $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits )
319            ->params( $userName )->numParams( $this->RCMaxAge )->escaped();
320
321        return Html::rawElement( 'li', [], "{$item} [{$count}]{$blocked}" );
322    }
323
324}
325
326/**
327 * Retain the old class name for backwards compatibility.
328 * @deprecated since 1.41
329 */
330class_alias( ActiveUsersPager::class, 'ActiveUsersPager' );