Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.17% covered (danger)
5.17%
9 / 174
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterViewExamine
5.17% covered (danger)
5.17%
9 / 174
14.29% covered (danger)
14.29%
1 / 7
696.53
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
 show
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 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 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 showExaminerForLogEntry
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
56
 showExaminer
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\View;
4
5use ChangesList;
6use HTMLForm;
7use IContextSource;
8use LogicException;
9use MediaWiki\Extension\AbuseFilter\AbuseFilterChangesList;
10use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
11use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
12use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory;
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\Linker\LinkRenderer;
22use MediaWiki\Revision\RevisionRecord;
23use MediaWiki\Title\Title;
24use OOUI;
25use RecentChange;
26use Wikimedia\Rdbms\LBFactory;
27use Xml;
28
29class AbuseFilterViewExamine extends AbuseFilterView {
30    /**
31     * @var string The rules of the filter we're examining
32     */
33    private $testFilter;
34    /**
35     * @var LBFactory
36     */
37    private $lbFactory;
38    /**
39     * @var FilterLookup
40     */
41    private $filterLookup;
42    /**
43     * @var EditBoxBuilderFactory
44     */
45    private $boxBuilderFactory;
46    /**
47     * @var VariablesBlobStore
48     */
49    private $varBlobStore;
50    /**
51     * @var VariablesFormatter
52     */
53    private $variablesFormatter;
54    /**
55     * @var VariablesManager
56     */
57    private $varManager;
58    /**
59     * @var VariableGeneratorFactory
60     */
61    private $varGeneratorFactory;
62
63    /**
64     * @param LBFactory $lbFactory
65     * @param AbuseFilterPermissionManager $afPermManager
66     * @param FilterLookup $filterLookup
67     * @param EditBoxBuilderFactory $boxBuilderFactory
68     * @param VariablesBlobStore $varBlobStore
69     * @param VariablesFormatter $variablesFormatter
70     * @param VariablesManager $varManager
71     * @param VariableGeneratorFactory $varGeneratorFactory
72     * @param IContextSource $context
73     * @param LinkRenderer $linkRenderer
74     * @param string $basePageName
75     * @param array $params
76     */
77    public function __construct(
78        LBFactory $lbFactory,
79        AbuseFilterPermissionManager $afPermManager,
80        FilterLookup $filterLookup,
81        EditBoxBuilderFactory $boxBuilderFactory,
82        VariablesBlobStore $varBlobStore,
83        VariablesFormatter $variablesFormatter,
84        VariablesManager $varManager,
85        VariableGeneratorFactory $varGeneratorFactory,
86        IContextSource $context,
87        LinkRenderer $linkRenderer,
88        string $basePageName,
89        array $params
90    ) {
91        parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params );
92        $this->lbFactory = $lbFactory;
93        $this->filterLookup = $filterLookup;
94        $this->boxBuilderFactory = $boxBuilderFactory;
95        $this->varBlobStore = $varBlobStore;
96        $this->variablesFormatter = $variablesFormatter;
97        $this->variablesFormatter->setMessageLocalizer( $context );
98        $this->varManager = $varManager;
99        $this->varGeneratorFactory = $varGeneratorFactory;
100    }
101
102    /**
103     * Shows the page
104     */
105    public function show() {
106        $out = $this->getOutput();
107        $out->setPageTitleMsg( $this->msg( 'abusefilter-examine' ) );
108        $out->addHelpLink( 'Extension:AbuseFilter/Rules format' );
109        if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) {
110            $out->addWikiMsg( 'abusefilter-examine-intro' );
111        } else {
112            $out->addWikiMsg( 'abusefilter-examine-intro-examine-only' );
113        }
114
115        $this->testFilter = $this->getRequest()->getText( 'testfilter' );
116
117        // Check if we've got a subpage
118        if ( count( $this->mParams ) > 1 && is_numeric( $this->mParams[1] ) ) {
119            $this->showExaminerForRC( $this->mParams[1] );
120        } elseif ( count( $this->mParams ) > 2
121            && $this->mParams[1] === 'log'
122            && is_numeric( $this->mParams[2] )
123        ) {
124            $this->showExaminerForLogEntry( $this->mParams[2] );
125        } else {
126            $this->showSearch();
127        }
128    }
129
130    /**
131     * Shows the search form
132     */
133    public function showSearch() {
134        $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' );
135        $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge );
136        $max = wfTimestampNow();
137        $formDescriptor = [
138            'SearchUser' => [
139                'label-message' => 'abusefilter-test-user',
140                'type' => 'user',
141                'ipallowed' => true,
142            ],
143            'SearchPeriodStart' => [
144                'label-message' => 'abusefilter-test-period-start',
145                'type' => 'datetime',
146                'min' => $min,
147                'max' => $max,
148            ],
149            'SearchPeriodEnd' => [
150                'label-message' => 'abusefilter-test-period-end',
151                'type' => 'datetime',
152                'min' => $min,
153                'max' => $max,
154            ],
155        ];
156        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
157            ->addHiddenField( 'testfilter', $this->testFilter )
158            ->setWrapperLegendMsg( 'abusefilter-examine-legend' )
159            ->setSubmitTextMsg( 'abusefilter-examine-submit' )
160            ->setSubmitCallback( [ $this, 'showResults' ] )
161            ->showAlways();
162    }
163
164    /**
165     * Show search results, called as submit callback by HTMLForm
166     * @param array $formData
167     * @param HTMLForm $form
168     * @return bool
169     */
170    public function showResults( array $formData, HTMLForm $form ): bool {
171        $changesList = new AbuseFilterChangesList( $this->getContext(), $this->testFilter );
172
173        $dbr = $this->lbFactory->getReplicaDatabase();
174        $conds = $this->buildVisibilityConditions( $dbr, $this->getAuthority() );
175        $conds[] = $this->buildTestConditions( $dbr );
176
177        // Normalise username
178        $userTitle = Title::newFromText( $formData['SearchUser'], NS_USER );
179        $userName = $userTitle ? $userTitle->getText() : '';
180
181        if ( $userName !== '' ) {
182            $rcQuery = RecentChange::getQueryInfo();
183            $conds[$rcQuery['fields']['rc_user_text']] = $userName;
184        }
185
186        $startTS = strtotime( $formData['SearchPeriodStart'] );
187        if ( $startTS ) {
188            $conds[] = 'rc_timestamp>=' . $dbr->addQuotes( $dbr->timestamp( $startTS ) );
189        }
190        $endTS = strtotime( $formData['SearchPeriodEnd'] );
191        if ( $endTS ) {
192            $conds[] = 'rc_timestamp<=' . $dbr->addQuotes( $dbr->timestamp( $endTS ) );
193        }
194        $pager = new AbuseFilterExaminePager(
195            $changesList,
196            $this->linkRenderer,
197            $dbr,
198            $this->getTitle( 'examine' ),
199            $conds
200        );
201
202        $output = $changesList->beginRecentChangesList()
203            . $pager->getNavigationBar()
204            . $pager->getBody()
205            . $pager->getNavigationBar()
206            . $changesList->endRecentChangesList();
207
208        $form->addPostHtml( $output );
209        return true;
210    }
211
212    /**
213     * @param int $rcid
214     */
215    public function showExaminerForRC( $rcid ) {
216        // Get data
217        $rc = RecentChange::newFromId( $rcid );
218        $out = $this->getOutput();
219        if ( !$rc ) {
220            $out->addWikiMsg( 'abusefilter-examine-notfound' );
221            return;
222        }
223
224        if ( !ChangesList::userCan( $rc, RevisionRecord::SUPPRESSED_ALL ) ) {
225            $out->addWikiMsg( 'abusefilter-log-details-hidden-implicit' );
226            return;
227        }
228
229        $varGenerator = $this->varGeneratorFactory->newRCGenerator( $rc, $this->getUser() );
230        $vars = $varGenerator->getVars() ?: new VariableHolder();
231        $out->addJsConfigVars( [
232            'wgAbuseFilterVariables' => $this->varManager->dumpAllVars( $vars, true ),
233            'abuseFilterExamine' => [ 'type' => 'rc', 'id' => $rcid ]
234        ] );
235
236        $this->showExaminer( $vars );
237    }
238
239    /**
240     * @param int $logid
241     */
242    public function showExaminerForLogEntry( $logid ) {
243        // Get data
244        $dbr = $this->lbFactory->getReplicaDatabase();
245        $performer = $this->getAuthority();
246        $out = $this->getOutput();
247
248        $row = $dbr->selectRow(
249            'abuse_filter_log',
250            [
251                'afl_deleted',
252                'afl_var_dump',
253                'afl_rev_id',
254                'afl_filter_id',
255                'afl_global'
256            ],
257            [ 'afl_id' => $logid ],
258            __METHOD__
259        );
260
261        if ( !$row ) {
262            $out->addWikiMsg( 'abusefilter-examine-notfound' );
263            return;
264        }
265
266        try {
267            $isHidden = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global )->isHidden();
268        } catch ( CentralDBNotAvailableException $_ ) {
269            // Conservatively assume that it's hidden, like in SpecialAbuseLog
270            $isHidden = true;
271        }
272        if ( !$this->afPermManager->canSeeLogDetailsForFilter( $performer, $isHidden ) ) {
273            $out->addWikiMsg( 'abusefilter-log-cannot-see-details' );
274            return;
275        }
276
277        $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager );
278        if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) {
279            if ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN ) {
280                $msg = 'abusefilter-log-details-hidden';
281            } elseif ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN_IMPLICIT ) {
282                $msg = 'abusefilter-log-details-hidden-implicit';
283            } else {
284                throw new LogicException( "Unexpected visibility $visibility" );
285            }
286            $out->addWikiMsg( $msg );
287            return;
288        }
289
290        $vars = $this->varBlobStore->loadVarDump( $row->afl_var_dump );
291        $out->addJsConfigVars( [
292            'wgAbuseFilterVariables' => $this->varManager->dumpAllVars( $vars, true ),
293            'abuseFilterExamine' => [ 'type' => 'log', 'id' => $logid ]
294        ] );
295        $this->showExaminer( $vars );
296    }
297
298    /**
299     * @param VariableHolder|null $vars
300     */
301    public function showExaminer( ?VariableHolder $vars ) {
302        $output = $this->getOutput();
303        $output->enableOOUI();
304
305        if ( !$vars ) {
306            $output->addWikiMsg( 'abusefilter-examine-incompatible' );
307            return;
308        }
309
310        $html = '';
311
312        $output->addModules( 'ext.abuseFilter.examine' );
313
314        // Add test bit
315        if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) {
316            $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder(
317                $this,
318                $this->getAuthority(),
319                $output
320            );
321
322            $tester = Xml::tags( 'h2', null, $this->msg( 'abusefilter-examine-test' )->parse() );
323            $tester .= $boxBuilder->buildEditBox( $this->testFilter, false, false, false );
324            $tester .= $this->buildFilterLoader();
325            $html .= Xml::tags( 'div', [ 'id' => 'mw-abusefilter-examine-editor' ], $tester );
326            $html .= Xml::tags( 'p',
327                null,
328                new OOUI\ButtonInputWidget(
329                    [
330                        'label' => $this->msg( 'abusefilter-examine-test-button' )->text(),
331                        'id' => 'mw-abusefilter-examine-test',
332                        'flags' => [ 'primary', 'progressive' ]
333                    ]
334                ) .
335                Xml::element( 'div',
336                    [
337                        'id' => 'mw-abusefilter-syntaxresult',
338                        'style' => 'display: none;'
339                    ], '&#160;'
340                )
341            );
342        }
343
344        // Variable dump
345        $html .= Xml::tags(
346            'h2',
347            null,
348            $this->msg( 'abusefilter-examine-vars' )->parse()
349        );
350        $html .= $this->variablesFormatter->buildVarDumpTable( $vars );
351
352        $output->addHTML( $html );
353    }
354
355}