Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.00% covered (danger)
6.00%
9 / 150
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialCheckUserLog
6.00% covered (danger)
6.00%
9 / 150
16.67% covered (danger)
16.67%
1 / 6
318.84
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 execute
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
156
 addSubtitle
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 displaySearchForm
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
2
 verifyInitiator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\CheckUser\CheckUser;
4
5use HTMLForm;
6use MediaWiki\Cache\LinkBatchFactory;
7use MediaWiki\CheckUser\CheckUser\Pagers\CheckUserLogPager;
8use MediaWiki\CheckUser\Services\CheckUserLogService;
9use MediaWiki\CommentFormatter\CommentFormatter;
10use MediaWiki\CommentStore\CommentStore;
11use MediaWiki\Html\Html;
12use MediaWiki\Pager\ContribsPager;
13use MediaWiki\Permissions\PermissionManager;
14use MediaWiki\SpecialPage\SpecialPage;
15use MediaWiki\Title\Title;
16use MediaWiki\User\ActorStore;
17use MediaWiki\User\UserFactory;
18use UserBlockedError;
19use Wikimedia\Rdbms\IReadableDatabase;
20use Wikimedia\Rdbms\LBFactory;
21
22class SpecialCheckUserLog extends SpecialPage {
23    /**
24     * @var array an array of nullable string/integer options.
25     */
26    protected array $opts;
27
28    private IReadableDatabase $dbr;
29
30    private LinkBatchFactory $linkBatchFactory;
31    private PermissionManager $permissionManager;
32    private CommentStore $commentStore;
33    private CommentFormatter $commentFormatter;
34    private CheckUserLogService $checkUserLogService;
35    private UserFactory $userFactory;
36    private ActorStore $actorStore;
37
38    /**
39     * @param LinkBatchFactory $linkBatchFactory
40     * @param PermissionManager $permissionManager
41     * @param CommentStore $commentStore
42     * @param CommentFormatter $commentFormatter
43     * @param CheckUserLogService $checkUserLogService
44     * @param UserFactory $userFactory
45     * @param ActorStore $actorStore
46     * @param LBFactory $lbFactory
47     */
48    public function __construct(
49        LinkBatchFactory $linkBatchFactory,
50        PermissionManager $permissionManager,
51        CommentStore $commentStore,
52        CommentFormatter $commentFormatter,
53        CheckUserLogService $checkUserLogService,
54        UserFactory $userFactory,
55        ActorStore $actorStore,
56        LBFactory $lbFactory
57    ) {
58        parent::__construct( 'CheckUserLog', 'checkuser-log' );
59        $this->linkBatchFactory = $linkBatchFactory;
60        $this->permissionManager = $permissionManager;
61        $this->commentStore = $commentStore;
62        $this->commentFormatter = $commentFormatter;
63        $this->checkUserLogService = $checkUserLogService;
64        $this->userFactory = $userFactory;
65        $this->actorStore = $actorStore;
66        $this->dbr = $lbFactory->getReplicaDatabase();
67    }
68
69    /**
70     * @inheritDoc
71     */
72    public function execute( $par ) {
73        $this->setHeaders();
74        $this->addHelpLink( 'Extension:CheckUser' );
75        $this->checkPermissions();
76
77        // Blocked users are not allowed to run checkuser queries (bug T157883)
78        $block = $this->getUser()->getBlock();
79        if ( $block && $block->isSitewide() ) {
80            throw new UserBlockedError( $block );
81        }
82
83        $out = $this->getOutput();
84        $out->addModules( [ 'ext.checkUser' ] );
85        $out->addModuleStyles( [
86            'ext.checkUser.styles',
87            'mediawiki.interface.helpers.styles'
88        ] );
89        $request = $this->getRequest();
90
91        $this->opts = [];
92
93        // Normalise target parameter and ignore if not valid (T217713)
94        // It must be valid when making a link to Special:CheckUserLog/<user>.
95        // Do not normalize an empty target, as that means "everything" (T265606)
96        $this->opts['target'] = trim( $request->getVal( 'cuSearch', $par ?? '' ) );
97        if ( $this->opts['target'] !== '' ) {
98            $userTitle = Title::makeTitleSafe( NS_USER, $this->opts['target'] );
99            $this->opts['target'] = $userTitle ? $userTitle->getText() : '';
100        }
101
102        $this->opts['initiator'] = trim( $request->getVal( 'cuInitiator', '' ) );
103
104        $this->opts['reason'] = trim( $request->getVal( 'cuReasonSearch', '' ) );
105
106        // From SpecialContributions.php
107        $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) === 'prev';
108        # Offset overrides year/month selection
109        if ( !$skip ) {
110            $this->opts['year'] = $request->getIntOrNull( 'year' );
111            $this->opts['month'] = $request->getIntOrNull( 'month' );
112
113            $this->opts['start'] = $request->getVal( 'start' );
114            $this->opts['end'] = $request->getVal( 'end' );
115        }
116
117        $this->opts = ContribsPager::processDateFilter( $this->opts );
118
119        $this->addSubtitle();
120
121        $this->displaySearchForm();
122
123        $errorMessageKey = null;
124
125        if (
126            $this->opts['target'] !== '' &&
127            $this->checkUserLogService->verifyTarget( $this->opts['target'] ) === false
128        ) {
129            $errorMessageKey = 'checkuser-target-nonexistent';
130        }
131        if ( $this->opts['initiator'] !== '' && $this->verifyInitiator( $this->opts['initiator'] ) === false ) {
132            $errorMessageKey = 'checkuser-initiator-nonexistent';
133        }
134
135        if ( $errorMessageKey !== null ) {
136            // Invalid target was input so show an error message and stop from here
137            $out->addHTML(
138                Html::errorBox(
139                    $out->msg( $errorMessageKey )->parse()
140                )
141            );
142            return;
143        }
144
145        $pager = new CheckUserLogPager(
146            $this->getContext(),
147            $this->opts,
148            $this->linkBatchFactory,
149            $this->commentStore,
150            $this->commentFormatter,
151            $this->checkUserLogService,
152            $this->userFactory,
153            $this->actorStore
154        );
155
156        $out->addHTML(
157            $pager->getNavigationBar() .
158            $pager->getBody() .
159            $pager->getNavigationBar()
160        );
161    }
162
163    /**
164     * Add subtitle links to the page
165     */
166    private function addSubtitle(): void {
167        if ( $this->permissionManager->userHasRight( $this->getUser(), 'checkuser' ) ) {
168            $links = [
169                $this->getLinkRenderer()->makeKnownLink(
170                    SpecialPage::getTitleFor( 'CheckUser' ),
171                    $this->msg( 'checkuser-showmain' )->text()
172                ),
173                $this->getLinkRenderer()->makeKnownLink(
174                    SpecialPage::getTitleFor( 'Investigate' ),
175                    $this->msg( 'checkuser-show-investigate' )->text()
176                ),
177            ];
178
179            if ( $this->opts['target'] ) {
180                $links[] = $this->getLinkRenderer()->makeKnownLink(
181                    SpecialPage::getTitleFor( 'CheckUser', $this->opts['target'] ),
182                    $this->msg( 'checkuser-check-this-user' )->text()
183                );
184
185                $links[] = $this->getLinkRenderer()->makeKnownLink(
186                    SpecialPage::getTitleFor( 'Investigate' ),
187                    $this->msg( 'checkuser-investigate-this-user' )->text(),
188                    [],
189                    [ 'targets' => $this->opts['target'] ]
190                );
191            }
192
193            $this->getOutput()->addSubtitle( Html::rawElement(
194                    'span',
195                    [ "class" => "mw-checkuser-links-no-parentheses" ],
196                    Html::openElement( 'span' ) .
197                    implode(
198                        Html::closeElement( 'span' ) . Html::openElement( 'span' ),
199                        $links
200                    ) .
201                    Html::closeElement( 'span' )
202                )
203            );
204        }
205    }
206
207    /**
208     * Use an HTMLForm to create and output the search form used on this page.
209     */
210    protected function displaySearchForm() {
211        $fields = [
212            'target' => [
213                'type' => 'user',
214                // validation in execute() currently
215                'exists' => false,
216                'ipallowed' => true,
217                'name' => 'cuSearch',
218                'size' => 40,
219                'label-message' => 'checkuser-log-search-target',
220                'default' => $this->opts['target'],
221                'id' => 'mw-target-user-or-ip'
222            ],
223            'initiator' => [
224                'type' => 'user',
225                // validation in execute() currently
226                'exists' => false,
227                'ipallowed' => true,
228                'name' => 'cuInitiator',
229                'size' => 40,
230                'label-message' => 'checkuser-log-search-initiator',
231                'default' => $this->opts['initiator']
232            ],
233            'reason' => [
234                'type' => 'text',
235                'name' => 'cuReasonSearch',
236                'size' => 40,
237                'label-message' => 'checkuser-log-search-reason',
238                'default' => $this->opts['reason'],
239                'help-message' => 'checkuser-log-search-reason-help'
240            ],
241            'start' => [
242                'type' => 'date',
243                'default' => '',
244                'id' => 'mw-date-start',
245                'label' => $this->msg( 'date-range-from' )->text(),
246                'name' => 'start'
247            ],
248            'end' => [
249                'type' => 'date',
250                'default' => '',
251                'id' => 'mw-date-end',
252                'label' => $this->msg( 'date-range-to' )->text(),
253                'name' => 'end'
254            ]
255        ];
256
257        $form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
258        $form->setMethod( 'get' )
259            ->setWrapperLegendMsg( 'checkuser-search' )
260            ->setSubmitTextMsg( 'checkuser-search-submit' )
261            ->prepareForm()
262            ->displayForm( false );
263    }
264
265    /**
266     * Verify if the initiator is valid.
267     *
268     * This is defined by a user having a valid actor ID.
269     * Any user without an actor ID cannot be a valid initiator
270     * as making a check causes an actor ID to be created.
271     *
272     * @param string $initiator The name of the initiator that is to be verified
273     * @return bool|int
274     */
275    private function verifyInitiator( string $initiator ) {
276        return $this->actorStore->findActorIdByName( $initiator, $this->dbr ) ?? false;
277    }
278
279    /**
280     * @inheritDoc
281     */
282    protected function getGroupName() {
283        return 'changes';
284    }
285}