Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.71% covered (warning)
57.71%
116 / 201
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterViewExamine
57.71% covered (warning)
57.71%
116 / 201
28.57% covered (danger)
28.57%
2 / 7
115.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 show
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 showSearch
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
2
 showResults
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
30
 showExaminerForRC
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 showExaminerForLogEntry
87.30% covered (warning)
87.30%
55 / 63
0.00% covered (danger)
0.00%
0 / 1
13.35
 showExaminer
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\View;
4
5use LogicException;
6use MediaWiki\Context\IContextSource;
7use MediaWiki\Extension\AbuseFilter\AbuseFilterChangesList;
8use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
9use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory;
10use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
11use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory;
12use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter;
13use MediaWiki\Extension\AbuseFilter\FilterLookup;
14use MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterExaminePager;
15use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
16use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
17use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
18use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
19use MediaWiki\Extension\AbuseFilter\Variables\VariablesFormatter;
20use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
21use MediaWiki\Html\Html;
22use MediaWiki\HTMLForm\HTMLForm;
23use MediaWiki\Linker\LinkRenderer;
24use MediaWiki\RecentChanges\ChangesList;
25use MediaWiki\RecentChanges\RecentChange;
26use MediaWiki\Revision\RevisionRecord;
27use MediaWiki\Title\Title;
28use OOUI;
29use Wikimedia\Rdbms\LBFactory;
30
31class AbuseFilterViewExamine extends AbuseFilterView {
32    /**
33     * @var string The rules of the filter we're examining
34     */
35    private $testFilter;
36    /**
37     * @var LBFactory
38     */
39    private $lbFactory;
40    /**
41     * @var FilterLookup
42     */
43    private $filterLookup;
44    /**
45     * @var EditBoxBuilderFactory
46     */
47    private $boxBuilderFactory;
48    /**
49     * @var VariablesBlobStore
50     */
51    private $varBlobStore;
52    /**
53     * @var VariablesFormatter
54     */
55    private $variablesFormatter;
56    /**
57     * @var VariablesManager
58     */
59    private $varManager;
60    /**
61     * @var VariableGeneratorFactory
62     */
63    private $varGeneratorFactory;
64
65    private AbuseLoggerFactory $abuseLoggerFactory;
66
67    /**
68     * @param LBFactory $lbFactory
69     * @param AbuseFilterPermissionManager $afPermManager
70     * @param FilterLookup $filterLookup
71     * @param EditBoxBuilderFactory $boxBuilderFactory
72     * @param VariablesBlobStore $varBlobStore
73     * @param VariablesFormatter $variablesFormatter
74     * @param VariablesManager $varManager
75     * @param VariableGeneratorFactory $varGeneratorFactory
76     * @param AbuseLoggerFactory $abuseLoggerFactory
77     * @param IContextSource $context
78     * @param LinkRenderer $linkRenderer
79     * @param string $basePageName
80     * @param array $params
81     */
82    public function __construct(
83        LBFactory $lbFactory,
84        AbuseFilterPermissionManager $afPermManager,
85        FilterLookup $filterLookup,
86        EditBoxBuilderFactory $boxBuilderFactory,
87        VariablesBlobStore $varBlobStore,
88        VariablesFormatter $variablesFormatter,
89        VariablesManager $varManager,
90        VariableGeneratorFactory $varGeneratorFactory,
91        AbuseLoggerFactory $abuseLoggerFactory,
92        IContextSource $context,
93        LinkRenderer $linkRenderer,
94        string $basePageName,
95        array $params
96    ) {
97        parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params );
98        $this->lbFactory = $lbFactory;
99        $this->filterLookup = $filterLookup;
100        $this->boxBuilderFactory = $boxBuilderFactory;
101        $this->varBlobStore = $varBlobStore;
102        $this->variablesFormatter = $variablesFormatter;
103        $this->variablesFormatter->setMessageLocalizer( $context );
104        $this->varManager = $varManager;
105        $this->varGeneratorFactory = $varGeneratorFactory;
106        $this->abuseLoggerFactory = $abuseLoggerFactory;
107    }
108
109    /**
110     * Shows the page
111     */
112    public function show() {
113        $out = $this->getOutput();
114        $out->setPageTitleMsg( $this->msg( 'abusefilter-examine' ) );
115        $out->addHelpLink( 'Extension:AbuseFilter/Rules format' );
116        if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) {
117            $out->addWikiMsg( 'abusefilter-examine-intro' );
118        } else {
119            $out->addWikiMsg( 'abusefilter-examine-intro-examine-only' );
120        }
121
122        $this->testFilter = $this->getRequest()->getText( 'testfilter' );
123
124        // Check if we've got a subpage
125        if ( count( $this->mParams ) > 1 && is_numeric( $this->mParams[1] ) ) {
126            $this->showExaminerForRC( $this->mParams[1] );
127        } elseif ( count( $this->mParams ) > 2
128            && $this->mParams[1] === 'log'
129            && is_numeric( $this->mParams[2] )
130        ) {
131            $this->showExaminerForLogEntry( $this->mParams[2] );
132        } else {
133            $this->showSearch();
134        }
135    }
136
137    /**
138     * Shows the search form
139     */
140    public function showSearch() {
141        $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
142        $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
143        $max = wfTimestampNow();
144        $formDescriptor = [
145            'SearchUser' => [
146                'label-message' => 'abusefilter-test-user',
147                'type' => 'user',
148                'ipallowed' => true,
149            ],
150            'SearchPeriodStart' => [
151                'label-message' => 'abusefilter-test-period-start',
152                'type' => 'datetime',
153                'min' => $min,
154                'max' => $max,
155            ],
156            'SearchPeriodEnd' => [
157                'label-message' => 'abusefilter-test-period-end',
158                'type' => 'datetime',
159                'min' => $min,
160                'max' => $max,
161            ],
162        ];
163        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
164            ->addHiddenField( 'testfilter', $this->testFilter )
165            ->setWrapperLegendMsg( 'abusefilter-examine-legend' )
166            ->setSubmitTextMsg( 'abusefilter-examine-submit' )
167            ->setSubmitCallback( [ $this, 'showResults' ] )
168            ->showAlways();
169    }
170
171    /**
172     * Show search results, called as submit callback by HTMLForm
173     * @param array $formData
174     * @param HTMLForm $form
175     * @return bool
176     */
177    public function showResults( array $formData, HTMLForm $form ): bool {
178        $changesList = new AbuseFilterChangesList( $this->getContext(), $this->testFilter );
179
180        $dbr = $this->lbFactory->getReplicaDatabase();
181        $conds = $this->buildVisibilityConditions( $dbr, $this->getAuthority() );
182        $conds[] = $this->buildTestConditions( $dbr );
183
184        // Normalise username
185        $userTitle = Title::newFromText( $formData['SearchUser'], NS_USER );
186        $userName = $userTitle ? $userTitle->getText() : '';
187
188        if ( $userName !== '' ) {
189            $rcQuery = RecentChange::getQueryInfo();
190            $conds[$rcQuery['fields']['rc_user_text']] = $userName;
191        }
192
193        $startTS = strtotime( $formData['SearchPeriodStart'] );
194        if ( $startTS ) {
195            $conds[] = $dbr->expr( 'rc_timestamp', '>=', $dbr->timestamp( $startTS ) );
196        }
197        $endTS = strtotime( $formData['SearchPeriodEnd'] );
198        if ( $endTS ) {
199            $conds[] = $dbr->expr( 'rc_timestamp', '<=', $dbr->timestamp( $endTS ) );
200        }
201        $pager = new AbuseFilterExaminePager(
202            $changesList,
203            $this->linkRenderer,
204            $dbr,
205            $this->getTitle( 'examine' ),
206            $conds
207        );
208
209        $output = $changesList->beginRecentChangesList()
210            . $pager->getNavigationBar()
211            . $pager->getBody()
212            . $pager->getNavigationBar()
213            . $changesList->endRecentChangesList();
214
215        $form->addPostHtml( $output );
216        return true;
217    }
218
219    /**
220     * @param int $rcid
221     */
222    public function showExaminerForRC( $rcid ) {
223        // Get data
224        $rc = RecentChange::newFromId( $rcid );
225        $out = $this->getOutput();
226        if ( !$rc ) {
227            $out->addWikiMsg( 'abusefilter-examine-notfound' );
228            return;
229        }
230
231        if ( !ChangesList::userCan( $rc, RevisionRecord::SUPPRESSED_ALL ) ) {
232            $out->addWikiMsg( 'abusefilter-log-details-hidden-implicit' );
233            return;
234        }
235
236        $varGenerator = $this->varGeneratorFactory->newRCGenerator( $rc, $this->getUser() );
237        $vars = $varGenerator->getVars();
238        if ( !$vars ) {
239            $out->addWikiMsg( 'abusefilter-examine-incompatible' );
240            return;
241        }
242
243        $out->addJsConfigVars( [
244            'abuseFilterExamine' => [ 'type' => 'rc', 'id' => $rcid ]
245        ] );
246
247        $this->showExaminer( $vars );
248    }
249
250    /**
251     * @param int $logid
252     */
253    public function showExaminerForLogEntry( $logid ) {
254        // Get data
255        $dbr = $this->lbFactory->getReplicaDatabase();
256        $performer = $this->getAuthority();
257        $out = $this->getOutput();
258
259        $row = $dbr->newSelectQueryBuilder()
260            ->select( [
261                'afl_deleted',
262                'afl_ip',
263                'afl_var_dump',
264                'afl_rev_id',
265                'afl_filter_id',
266                'afl_global'
267            ] )
268            ->from( 'abuse_filter_log' )
269            ->where( [ 'afl_id' => $logid ] )
270            ->caller( __METHOD__ )
271            ->fetchRow();
272
273        if ( !$row ) {
274            $out->addWikiMsg( 'abusefilter-examine-notfound' );
275            return;
276        }
277
278        try {
279            $filter = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global );
280        } catch ( CentralDBNotAvailableException $_ ) {
281            // Conservatively assume that it's hidden and protected, like in AbuseLogPager::doFormatRow
282            $filter = MutableFilter::newDefault();
283            $filter->setProtected( true );
284            $filter->setHidden( true );
285        }
286        if ( !$this->afPermManager->canSeeLogDetailsForFilter( $performer, $filter ) ) {
287            $out->addWikiMsg( 'abusefilter-log-cannot-see-details' );
288            return;
289        }
290
291        $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager );
292        if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) {
293            if ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN ) {
294                $msg = 'abusefilter-log-details-hidden';
295            } elseif ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN_IMPLICIT ) {
296                $msg = 'abusefilter-log-details-hidden-implicit';
297            } else {
298                throw new LogicException( "Unexpected visibility $visibility" );
299            }
300            $out->addWikiMsg( $msg );
301            return;
302        }
303
304        $vars = $this->varBlobStore->loadVarDump( $row );
305        $varsArray = $this->varManager->dumpAllVars( $vars, true );
306
307        // Check that the user can see the protected variables that are being examined if the filter is protected.
308        $userAuthority = $this->getAuthority();
309        if (
310            $filter->isProtected() &&
311            !$this->afPermManager->canViewProtectedVariables( $userAuthority, array_keys( $varsArray ) )->isGood()
312        ) {
313            $out->addWikiMsg( 'abusefilter-examine-protected-vars-permission' );
314            return;
315        }
316
317        // AbuseFilter logs created before T390086 may have protected variables present in the variable dump
318        // when the filter itself isn't protected. This is because a different filter matched against the
319        // a protected variable which caused the value to be added to the var dump for the public filter
320        // match.
321        // We shouldn't block access to the details of an otherwise public filter hit so
322        // instead only check for access to the protected variables and redact them if the user
323        // shouldn't see them.
324        $protectedVariableValuesShown = [];
325        foreach ( $this->afPermManager->getProtectedVariables() as $protectedVariable ) {
326            if ( isset( $varsArray[$protectedVariable] ) ) {
327                // Try each variable at a time, as the user may be able to see some but not all of the
328                // protected variables. We only want to redact what is necessary to redact.
329                $canViewProtectedVariable = $this->afPermManager
330                    ->canViewProtectedVariables( $userAuthority, [ $protectedVariable ] )->isGood();
331                if ( !$canViewProtectedVariable ) {
332                    $varsArray[$protectedVariable] = '';
333                } else {
334                    $protectedVariableValuesShown[] = $protectedVariable;
335                }
336            }
337        }
338        $vars = VariableHolder::newFromArray( $varsArray );
339
340        if ( $filter->isProtected() ) {
341            $logger = $this->abuseLoggerFactory->getProtectedVarsAccessLogger();
342            $logger->logViewProtectedVariableValue(
343                $userAuthority->getUser(),
344                $varsArray['user_name'] ?? $varsArray['accountname'],
345                $protectedVariableValuesShown
346            );
347        }
348
349        $out->addJsConfigVars( [
350            'abuseFilterExamine' => [ 'type' => 'log', 'id' => $logid ]
351        ] );
352        $this->showExaminer( $vars );
353    }
354
355    /**
356     * @param VariableHolder $vars
357     */
358    public function showExaminer( VariableHolder $vars ) {
359        $output = $this->getOutput();
360        $output->enableOOUI();
361
362        $html = '';
363
364        $output->addModules( 'ext.abuseFilter.examine' );
365        $output->addJsConfigVars( [
366            'wgAbuseFilterVariables' => $this->varManager->dumpAllVars( $vars, true ),
367        ] );
368
369        // Add test bit
370        if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) {
371            $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder(
372                $this,
373                $this->getAuthority(),
374                $output
375            );
376
377            $tester = Html::rawElement( 'h2', [], $this->msg( 'abusefilter-examine-test' )->parse() );
378            $tester .= $boxBuilder->buildEditBox( $this->testFilter, false, false, false );
379            $tester .= $this->buildFilterLoader();
380            $html .= Html::rawElement( 'div', [ 'id' => 'mw-abusefilter-examine-editor' ], $tester );
381            $html .= Html::rawElement( 'p',
382                [],
383                new OOUI\ButtonInputWidget(
384                    [
385                        'label' => $this->msg( 'abusefilter-examine-test-button' )->text(),
386                        'id' => 'mw-abusefilter-examine-test',
387                        'flags' => [ 'primary', 'progressive' ]
388                    ]
389                ) .
390                Html::element( 'div',
391                    [
392                        'id' => 'mw-abusefilter-syntaxresult',
393                        'style' => 'display: none;'
394                    ]
395                )
396            );
397        }
398
399        // Variable dump
400        $html .= Html::rawElement(
401            'h2',
402            [],
403            $this->msg( 'abusefilter-examine-vars' )->parse()
404        );
405        $html .= $this->variablesFormatter->buildVarDumpTable( $vars );
406
407        $output->addHTML( $html );
408    }
409
410}