Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 332
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMultiLock
0.00% covered (danger)
0.00%
0 / 332
0.00% covered (danger)
0.00%
0 / 16
3422
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
240
 getGlobalUsers
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 searchForUsers
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 showStatusForm
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
12
 showTableHeader
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 showUserTable
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 getUserTableRow
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
6
 setStatus
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
110
 showStatusError
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 showError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showUsernameForm
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 showLogExtract
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\CentralAuth\Special;
4
5use LogEventsList;
6use MediaWiki\Extension\CentralAuth\CentralAuthDatabaseManager;
7use MediaWiki\Extension\CentralAuth\CentralAuthUIService;
8use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
9use MediaWiki\Html\Html;
10use MediaWiki\SpecialPage\SpecialPage;
11use Wikimedia\Rdbms\IExpression;
12use Wikimedia\Rdbms\LikeValue;
13use Xml;
14
15/**
16 * Special page to allow locking and hiding multiple users
17 * at one time. Lots of code derived from Special:CentralAuth.
18 *
19 * @file
20 * @ingroup Extensions
21 */
22
23class SpecialMultiLock extends SpecialPage {
24    /** @var bool */
25    private $mCanSuppress;
26    /** @var string[] */
27    private $mGlobalUsers;
28    /** @var string[]|string|null */
29    private $mUserNames;
30    /** @var string */
31    private $mPrefixSearch;
32    /** @var bool */
33    private $mPosted;
34    /** @var string */
35    private $mMethod;
36    /** @var string */
37    private $mActionLock;
38    /** @var int */
39    private $mActionHide;
40    /** @var string */
41    private $mReason;
42    /** @var string[] */
43    private $mActionUserNames;
44    /** @var CentralAuthDatabaseManager */
45    private $databaseManager;
46    /** @var CentralAuthUIService */
47    private $uiService;
48
49    /**
50     * @param CentralAuthDatabaseManager $databaseManager
51     * @param CentralAuthUIService $uiService
52     */
53    public function __construct(
54        CentralAuthDatabaseManager $databaseManager,
55        CentralAuthUIService $uiService
56    ) {
57        parent::__construct( 'MultiLock', 'centralauth-lock' );
58        $this->databaseManager = $databaseManager;
59        $this->uiService = $uiService;
60    }
61
62    public function doesWrites() {
63        return true;
64    }
65
66    /** @inheritDoc */
67    public function execute( $subpage ) {
68        $this->setHeaders();
69        $this->checkPermissions();
70
71        $this->mCanSuppress = $this->getContext()->getAuthority()->isAllowed( 'centralauth-suppress' );
72        $this->getOutput()->addModules( 'ext.centralauth' );
73        $this->getOutput()->addModuleStyles( 'ext.centralauth.noflash' );
74        $this->mMethod = $this->getRequest()->getVal( 'wpMethod', '' );
75        $this->mActionLock = $this->getRequest()->getVal( 'wpActionLock', 'nochange' );
76        $this->mActionHide = $this->getRequest()->getInt( 'wpActionHide', -1 );
77        $this->mUserNames = $this->getRequest()->getVal( 'wpTarget', '' );
78        $this->mPrefixSearch = $this->getRequest()->getVal( 'wpSearchTarget', '' );
79        $this->mActionUserNames = $this->getRequest()->getArray( 'wpActionTarget' );
80        $this->mPosted = $this->getRequest()->wasPosted();
81
82        $this->mReason = $this->getRequest()->getText( 'wpReasonList' );
83        $reasonDetail = $this->getRequest()->getText( 'wpReason' );
84
85        if ( $this->mReason == 'other' ) {
86            $this->mReason = $reasonDetail;
87        } elseif ( $reasonDetail ) {
88            $this->mReason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() .
89                $reasonDetail;
90        }
91
92        if ( $this->mUserNames !== '' ) {
93            $this->mUserNames = explode( "\n", $this->mUserNames );
94        } else {
95            $this->mUserNames = [];
96        }
97
98        if ( $this->mPrefixSearch !== '' ) {
99            $this->mPrefixSearch = $this->getLanguage()->ucfirst( trim( $this->mPrefixSearch ) );
100        }
101
102        if ( $this->mMethod === '' ) {
103            $this->getOutput()->addWikiMsg( 'centralauth-admin-multi-intro' );
104            $this->showUsernameForm();
105            return;
106        } elseif ( $this->mPosted && $this->mMethod == 'search' && count( $this->mUserNames ) > 0 ) {
107            $this->showUserTable();
108        } elseif ( $this->mPosted && $this->mMethod == 'search' && $this->mPrefixSearch !== '' ) {
109            $this->searchForUsers();
110            $this->showUserTable();
111        } elseif ( $this->mPosted && $this->mMethod == 'set-status' &&
112            is_array( $this->mActionUserNames )
113        ) {
114            $this->mGlobalUsers = array_unique(
115                $this->getGlobalUsers( $this->mActionUserNames, true ), SORT_REGULAR
116            );
117            $this->setStatus();
118            $this->showUserTable();
119        } else {
120            $this->showError( 'centralauth-admin-multi-username' );
121        }
122
123        $this->showUsernameForm();
124
125        $this->showLogExtract();
126    }
127
128    /**
129     * Get the CentralAuthUsers from lines of text
130     *
131     * @param string[] $usernames
132     * @param bool $fromPrimaryDb
133     * @return (CentralAuthUser|string|false)[] User object, a HTML error string, or false.
134     */
135    private function getGlobalUsers( $usernames, $fromPrimaryDb = false ) {
136        $ret = [];
137        foreach ( $usernames as $username ) {
138            // T270954
139            $username = str_replace( '_', ' ', $username );
140            $username = trim( $username );
141            if ( $username === '' ) {
142                $ret[] = false;
143                continue;
144            }
145            $username = $this->getLanguage()->ucfirst( $username );
146
147            $globalUser = $fromPrimaryDb
148                ? CentralAuthUser::getPrimaryInstanceByName( $username )
149                : CentralAuthUser::getInstanceByName( $username );
150            if ( !$globalUser->exists()
151                || ( !$this->mCanSuppress &&
152                    ( $globalUser->isSuppressed() || $globalUser->isHidden() ) )
153            ) {
154                $ret[] = $this->msg( 'centralauth-admin-nonexistent', $username )->parse();
155            } else {
156                $ret[] = $globalUser;
157            }
158        }
159        return $ret;
160    }
161
162    /**
163     * Search the CentralAuth db for all usernames prefixed with mPrefixSearch
164     */
165    private function searchForUsers() {
166        $dbr = $this->databaseManager->getCentralReplicaDB();
167
168        $where = [
169            $dbr->expr( 'gu_name', IExpression::LIKE,
170                new LikeValue( $this->mPrefixSearch, $dbr->anyString() ) )
171        ];
172        if ( !$this->mCanSuppress ) {
173            $where['gu_hidden_level'] = CentralAuthUser::HIDDEN_LEVEL_NONE;
174        }
175
176        $result = $dbr->newSelectQueryBuilder()
177            ->select( 'gu_name' )
178            ->from( 'globaluser' )
179            ->where( $where )
180            ->limit( 100 )
181            ->caller( __METHOD__ )
182            ->fetchFieldValues();
183
184        foreach ( $result as $name ) {
185            $this->mUserNames[] = $name;
186        }
187    }
188
189    /**
190     * Show the Lock and/or Hide form, appropriate for this admin user's rights.
191     * The <form> and <fieldset> were started in showTableHeader()
192     */
193    private function showStatusForm() {
194        $form = '';
195        $radioLocked =
196            Xml::radioLabel(
197                $this->msg( 'centralauth-admin-action-lock-nochange' )->text(),
198                'wpActionLock',
199                'nochange',
200                'mw-centralauth-status-locked-no',
201                true ) .
202            '<br />' .
203            Xml::radioLabel(
204                $this->msg( 'centralauth-admin-action-lock-unlock' )->text(),
205                'wpActionLock',
206                'unlock',
207                'centralauth-admin-action-lock-unlock',
208                false ) .
209            '<br />' .
210            Xml::radioLabel(
211                $this->msg( 'centralauth-admin-action-lock-lock' )->text(),
212                'wpActionLock',
213                'lock',
214                'centralauth-admin-action-lock-lock',
215                false );
216
217        $radioHidden =
218            Xml::radioLabel(
219                $this->msg( 'centralauth-admin-action-hide-nochange' )->text(),
220                'wpActionHide',
221                '-1',
222                'mw-centralauth-status-hidden-nochange',
223                true ) .
224            '<br />';
225        if ( $this->mCanSuppress ) {
226            $radioHidden .= Xml::radioLabel(
227                $this->msg( 'centralauth-admin-action-hide-none' )->text(),
228                'wpActionHide',
229                (string)CentralAuthUser::HIDDEN_LEVEL_NONE,
230                'mw-centralauth-status-hidden-no',
231                false ) .
232            '<br />' .
233            Xml::radioLabel(
234                $this->msg( 'centralauth-admin-action-hide-lists' )->text(),
235                'wpActionHide',
236                (string)CentralAuthUser::HIDDEN_LEVEL_LISTS,
237                'mw-centralauth-status-hidden-list',
238                false ) .
239            '<br />' .
240            Xml::radioLabel(
241                $this->msg( 'centralauth-admin-action-hide-oversight' )->text(),
242                'wpActionHide',
243                (string)CentralAuthUser::HIDDEN_LEVEL_SUPPRESSED,
244                'mw-centralauth-status-hidden-oversight',
245                false
246            );
247        }
248
249        $reasonList = Xml::listDropdown(
250            'wpReasonList',
251            $this->msg( 'centralauth-admin-status-reasons' )->inContentLanguage()->text(),
252            $this->msg( 'centralauth-admin-reason-other-select' )->inContentLanguage()->text()
253        );
254        $reasonField = Xml::input( 'wpReason', 45, false );
255        $botField = Xml::check( 'markasbot' ) .
256            $this->msg( 'centralauth-admin-multi-botcheck' )->parse();
257
258        $form .= Xml::buildForm(
259            [
260                'centralauth-admin-status-locked' => $radioLocked,
261                'centralauth-admin-status-hidden' => $radioHidden,
262                'centralauth-admin-reason' => $reasonList,
263                'centralauth-admin-reason-other' => $reasonField,
264                'centralauth-admin-multi-bot' => $botField
265            ],
266            'centralauth-admin-status-submit'
267        );
268
269        $searchlist = $this->mUserNames;
270        if ( is_array( $this->mUserNames ) ) {
271            $searchlist = implode( "\n", $this->mUserNames );
272        }
273        $form .= Html::hidden( 'wpTarget', $searchlist );
274
275        $form .= '</fieldset></form>';
276
277        $this->getOutput()->addHTML( $form );
278    }
279
280    /**
281     * Start admin <form>, and start the table listing usernames to take action on
282     */
283    private function showTableHeader() {
284        $out = $this->getOutput();
285
286        $header = Xml::openElement(
287            'form',
288            [
289                'method' => 'POST',
290                'action' => $this->getPageTitle()->getFullUrl()
291            ]
292        );
293
294        $header .= Xml::fieldset( $this->msg( 'centralauth-admin-status' )->text() );
295        $header .= Html::hidden( 'wpMethod', 'set-status' );
296        $header .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
297        $header .= $this->msg( 'centralauth-admin-status-intro' )->parseAsBlock();
298
299        $header .= Xml::openElement(
300            'table',
301            [ 'class' => 'wikitable sortable mw-centralauth-wikislist' ]
302        );
303
304        $header .= '<thead><tr>' .
305                '<th></th>' .
306                '<th>' .
307                $out->getContext()->msg( 'centralauth-admin-username' )->escaped() .
308                '</th>' .
309                '<th>' .
310                $out->getContext()->msg( 'centralauth-admin-info-registered' )->escaped() .
311                '</th>' .
312                '<th>' .
313                $out->getContext()->msg( 'centralauth-admin-info-locked' )->escaped() .
314                '</th>' .
315                '<th>' .
316                $out->getContext()->msg( 'centralauth-admin-info-hidden' )->escaped() .
317                '</th>' .
318                '<th>' .
319                $out->getContext()->msg( 'centralauth-admin-info-editcount' )->escaped() .
320                '</th>' .
321                '<th>' .
322                $out->getContext()->msg( 'centralauth-admin-info-attached' )->escaped() .
323                '</th>' .
324                '<th>' .
325                $out->getContext()->msg( 'centralauth-multilock-homewiki' )->escaped() .
326                '</th>' .
327            '</tr></thead>' .
328            '<tbody>';
329
330        $out->addHTML( $header );
331        $out->addModuleStyles( 'jquery.tablesorter.styles' );
332        $out->addModules( 'jquery.tablesorter' );
333    }
334
335    /**
336     * Build the table of users to lock and/or hide
337     */
338    private function showUserTable() {
339        $this->mGlobalUsers = array_unique(
340            $this->getGlobalUsers( $this->mUserNames ), SORT_REGULAR
341        );
342
343        $out = $this->getOutput();
344
345        if ( count( $this->mGlobalUsers ) < 1 ) {
346            $this->showError( 'centralauth-admin-multi-notfound' );
347            return;
348        }
349
350        $this->showTableHeader();
351
352        foreach ( $this->mGlobalUsers as $globalUser ) {
353            $rowtext = Xml::openElement( 'tr' );
354
355            if ( $globalUser === false ) {
356                continue;
357            } elseif ( $globalUser instanceof CentralAuthUser ) {
358                $rowtext .= $this->getUserTableRow( $globalUser );
359            } else {
360                $rowtext .= Html::rawElement(
361                    'td',
362                    [ 'colspan' => 8 ],
363                    $globalUser
364                );
365            }
366
367            $rowtext .= Xml::closeElement( 'tr' );
368            $out->addHTML( $rowtext );
369        }
370
371        $out->addHTML( '</tbody></table>' );
372        $this->showStatusForm();
373    }
374
375    /**
376     * @param CentralAuthUser $globalUser
377     * @return string
378     */
379    private function getUserTableRow( CentralAuthUser $globalUser ) {
380        $rowHtml = '';
381
382        $guName = $globalUser->getName();
383        $guLink = $this->getLinkRenderer()->makeLink(
384            SpecialPage::getTitleFor( 'CentralAuth', $guName ),
385            // Names are known to exist, so this is not really needed
386            $guName
387        );
388        // formatHiddenLevel html escapes its output
389        $guHidden = $this->uiService->formatHiddenLevel( $this->getContext(), $globalUser->getHiddenLevelInt() );
390        $accountAge = time() - (int)wfTimestamp( TS_UNIX, $globalUser->getRegistration() );
391        $guRegister = $this->uiService->prettyTimespan( $this->getContext(), $accountAge );
392        $guLocked = $this->msg( 'centralauth-admin-status-locked-no' )->text();
393        if ( $globalUser->isLocked() ) {
394            $guLocked = $this->msg( 'centralauth-admin-status-locked-yes' )->text();
395        }
396        $guEditCount = $this->getLanguage()->formatNum( $globalUser->getGlobalEditCount() );
397        $guAttachedLocalAccounts = $this->getLanguage()
398            ->formatNum( count( $globalUser->listAttached() ) );
399        $guHomeWiki = $globalUser->getHomeWiki() ?? '';
400
401        $rowHtml .= Html::rawElement( 'td', [],
402            Html::input(
403                'wpActionTarget[' . $guName . ']',
404                $guName,
405                'checkbox',
406                [ 'checked' => 'checked' ]
407            )
408        );
409        $rowHtml .= Html::rawElement( 'td', [], $guLink );
410        $rowHtml .= Html::element( 'td', [ 'data-sort-value' => $accountAge ], $guRegister );
411        $rowHtml .= Html::element( 'td', [], $guLocked );
412        $rowHtml .= Html::rawElement( 'td', [], $guHidden );
413        $rowHtml .= Html::element( 'td', [], $guEditCount );
414        $rowHtml .= Html::element( 'td', [], $guAttachedLocalAccounts );
415        $rowHtml .= Html::element( 'td', [], $guHomeWiki );
416
417        return $rowHtml;
418    }
419
420    /**
421     * Lock and/or hide global users and log the activity (if any)
422     */
423    private function setStatus() {
424        if ( !$this->getUser()->matchEditToken( $this->getRequest()->getVal( 'wpEditToken' ) ) ) {
425            $this->showError( 'centralauth-token-mismatch' );
426            return;
427        }
428
429        if ( $this->mActionHide !== -1 && !$this->mCanSuppress ) {
430            $this->showError( 'centralauth-admin-not-authorized' );
431            return;
432        }
433
434        $setLocked = null;
435        $setHidden = null;
436
437        if ( $this->mActionLock != 'nochange' ) {
438            $setLocked = ( $this->mActionLock == 'lock' );
439        }
440
441        if ( $this->mActionHide !== -1 ) {
442            $setHidden = $this->mActionHide;
443        }
444
445        foreach ( $this->mGlobalUsers as $globalUser ) {
446            if ( !$globalUser instanceof CentralAuthUser ) {
447                // Somehow the user submitted a bad user name
448                $this->showStatusError( $globalUser );
449                continue;
450            }
451
452            $status = $globalUser->adminLockHide(
453                $setLocked,
454                $setHidden,
455                $this->mReason,
456                $this->getContext(),
457                $this->getRequest()->getCheck( 'markasbot' )
458            );
459
460            if ( !$status->isGood() ) {
461                $this->showStatusError( $status->getWikiText() );
462            } elseif ( $status->successCount > 0 ) {
463                $this->showSuccess( 'centralauth-admin-setstatus-success', $globalUser->getName() );
464            }
465        }
466    }
467
468    /**
469     * @param string $wikitext
470     */
471    private function showStatusError( $wikitext ) {
472        $out = $this->getOutput();
473        $out->addHTML(
474            Html::errorBox(
475                $out->parseAsInterface( $wikitext )
476            )
477        );
478    }
479
480    /**
481     * @param string $key
482     * @param mixed ...$params
483     */
484    private function showError( $key, ...$params ) {
485        $this->getOutput()->addHTML( Html::errorBox( $this->msg( $key, ...$params )->parse() ) );
486    }
487
488    /**
489     * @param string $key
490     * @param mixed ...$params
491     */
492    private function showSuccess( $key, ...$params ) {
493        $this->getOutput()->addHTML( Html::successBox( $this->msg( $key, ...$params )->parse() ) );
494    }
495
496    private function showUsernameForm() {
497        if ( is_array( $this->mUserNames ) ) {
498            $this->mUserNames = implode( "\n", $this->mUserNames );
499        }
500
501        $form = Xml::tags( 'form',
502            [
503                'method' => 'post',
504                'action' => $this->getPageTitle()->getLocalUrl()
505            ],
506            Xml::tags( 'fieldset', [],
507                Xml::element( 'legend', [], $this->msg( 'centralauth-admin-manage' )->text() ) .
508                Html::hidden( 'wpMethod', 'search' ) .
509                Xml::element( 'p', [],
510                    $this->msg( 'centralauth-admin-multi-username' )->text()
511                ) .
512                Xml::textarea( 'wpTarget',
513                    ( $this->mPrefixSearch ? '' : $this->mUserNames ), 25, 20 ) .
514                Xml::element( 'p', [],
515                    $this->msg( 'centralauth-admin-multi-searchprefix' )->text()
516                ) .
517                Html::input( 'wpSearchTarget', $this->mPrefixSearch ) .
518                Xml::tags( 'p', [],
519                    Xml::submitButton( $this->msg( 'centralauth-admin-lookup-ro' )->text() )
520                )
521            )
522        );
523        $this->getOutput()->addHTML( $form );
524    }
525
526    /**
527     * Show the last 50 log entries
528     */
529    private function showLogExtract() {
530        $text = '';
531        $numRows = LogEventsList::showLogExtract(
532            $text,
533            [ 'globalauth', 'suppress' ],
534            '',
535            '',
536            [
537                'conds' => [
538                    // T59253
539                    'log_action' => 'setstatus'
540                ],
541                'showIfEmpty' => true
542            ] );
543        if ( $numRows ) {
544            $this->getOutput()->addHTML(
545                Xml::fieldset( $this->msg( 'centralauth-admin-logsnippet' )->text(), $text )
546            );
547        }
548    }
549
550    /** @inheritDoc */
551    protected function getGroupName() {
552        return 'users';
553    }
554}