Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.47% covered (warning)
74.47%
175 / 235
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterViewList
74.47% covered (warning)
74.47%
175 / 235
25.00% covered (danger)
25.00%
1 / 4
59.79
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 show
74.44% covered (warning)
74.44%
67 / 90
0.00% covered (danger)
0.00%
0 / 1
31.83
 showList
79.13% covered (warning)
79.13%
91 / 115
0.00% covered (danger)
0.00%
0 / 1
10.91
 showStatus
40.91% covered (danger)
40.91%
9 / 22
0.00% covered (danger)
0.00%
0 / 1
4.86
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\View;
4
5use MediaWiki\Cache\LinkBatchFactory;
6use MediaWiki\Context\IContextSource;
7use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
8use MediaWiki\Extension\AbuseFilter\CentralDBManager;
9use MediaWiki\Extension\AbuseFilter\Filter\Flags;
10use MediaWiki\Extension\AbuseFilter\FilterLookup;
11use MediaWiki\Extension\AbuseFilter\FilterProfiler;
12use MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterPager;
13use MediaWiki\Extension\AbuseFilter\Pager\GlobalAbuseFilterPager;
14use MediaWiki\Extension\AbuseFilter\SpecsFormatter;
15use MediaWiki\Html\Html;
16use MediaWiki\HTMLForm\HTMLForm;
17use MediaWiki\Linker\LinkRenderer;
18use MediaWiki\Parser\ParserOptions;
19use OOUI;
20use StringUtils;
21use Wikimedia\Rdbms\IConnectionProvider;
22
23/**
24 * The default view used in Special:AbuseFilter
25 */
26class AbuseFilterViewList extends AbuseFilterView {
27
28    private LinkBatchFactory $linkBatchFactory;
29    private IConnectionProvider $dbProvider;
30    private FilterProfiler $filterProfiler;
31    private SpecsFormatter $specsFormatter;
32    private CentralDBManager $centralDBManager;
33    private FilterLookup $filterLookup;
34
35    public function __construct(
36        LinkBatchFactory $linkBatchFactory,
37        IConnectionProvider $dbProvider,
38        AbuseFilterPermissionManager $afPermManager,
39        FilterProfiler $filterProfiler,
40        SpecsFormatter $specsFormatter,
41        CentralDBManager $centralDBManager,
42        FilterLookup $filterLookup,
43        IContextSource $context,
44        LinkRenderer $linkRenderer,
45        string $basePageName,
46        array $params
47    ) {
48        parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params );
49        $this->linkBatchFactory = $linkBatchFactory;
50        $this->dbProvider = $dbProvider;
51        $this->filterProfiler = $filterProfiler;
52        $this->specsFormatter = $specsFormatter;
53        $this->specsFormatter->setMessageLocalizer( $context );
54        $this->centralDBManager = $centralDBManager;
55        $this->filterLookup = $filterLookup;
56    }
57
58    /**
59     * Shows the page
60     */
61    public function show() {
62        $out = $this->getOutput();
63        $request = $this->getRequest();
64        $config = $this->getConfig();
65        $performer = $this->getAuthority();
66
67        $out->addWikiMsg( 'abusefilter-intro' );
68        $this->showStatus();
69
70        // New filter button
71        if ( $this->afPermManager->canEdit( $performer ) ) {
72            $out->enableOOUI();
73            $buttons = new OOUI\HorizontalLayout( [
74                'items' => [
75                    new OOUI\ButtonWidget( [
76                        'label' => $this->msg( 'abusefilter-new' )->text(),
77                        'href' => $this->getTitle( 'new' )->getFullURL(),
78                        'flags' => [ 'primary', 'progressive' ],
79                    ] ),
80                    new OOUI\ButtonWidget( [
81                        'label' => $this->msg( 'abusefilter-import-button' )->text(),
82                        'href' => $this->getTitle( 'import' )->getFullURL(),
83                        'flags' => [ 'primary', 'progressive' ],
84                    ] )
85                ]
86            ] );
87            $out->addHTML( $buttons );
88        }
89
90        $conds = [];
91        $deleted = $request->getVal( 'deletedfilters' );
92        $furtherOptions = $request->getArray( 'furtheroptions', [] );
93        '@phan-var array $furtherOptions';
94        // Backward compatibility with old links
95        if ( $request->getBool( 'hidedisabled' ) ) {
96            $furtherOptions[] = 'hidedisabled';
97        }
98        if ( $request->getBool( 'hideprivate' ) ) {
99            $furtherOptions[] = 'hideprivate';
100        }
101        $defaultscope = 'all';
102        if ( $config->get( 'AbuseFilterCentralDB' ) !== null
103                && !$config->get( 'AbuseFilterIsCentral' ) ) {
104            // Show on remote wikis as default only local filters
105            $defaultscope = 'local';
106        }
107        $scope = $request->getVal( 'rulescope', $defaultscope );
108
109        $searchEnabled = $this->afPermManager->canViewPrivateFilters( $performer ) && !(
110            $config->get( 'AbuseFilterCentralDB' ) !== null &&
111            !$config->get( 'AbuseFilterIsCentral' ) &&
112            $scope === 'global' );
113
114        if ( $searchEnabled ) {
115            $querypattern = $request->getVal( 'querypattern', '' );
116            $searchmode = $request->getVal( 'searchoption', null );
117            if ( $querypattern === '' ) {
118                // Not specified or empty, that would error out
119                $querypattern = $searchmode = null;
120            }
121        } else {
122            $querypattern = null;
123            $searchmode = null;
124        }
125
126        if ( $deleted === 'show' ) {
127            // Nothing
128        } elseif ( $deleted === 'only' ) {
129            $conds['af_deleted'] = 1;
130        } else {
131            // hide, or anything else.
132            $conds['af_deleted'] = 0;
133            $deleted = 'hide';
134        }
135        if ( in_array( 'hidedisabled', $furtherOptions ) ) {
136            $conds['af_deleted'] = 0;
137            $conds['af_enabled'] = 1;
138        }
139        if ( in_array( 'hideprivate', $furtherOptions ) ) {
140            $conds['af_hidden'] = Flags::FILTER_PUBLIC;
141        }
142
143        if ( $scope === 'local' ) {
144            $conds['af_global'] = 0;
145        } elseif ( $scope === 'global' ) {
146            $conds['af_global'] = 1;
147        }
148
149        if ( $searchmode !== null ) {
150            // Check the search pattern. Filtering the results is done in AbuseFilterPager
151            $error = null;
152            if ( !in_array( $searchmode, [ 'LIKE', 'RLIKE', 'IRLIKE' ] ) ) {
153                $error = 'abusefilter-list-invalid-searchmode';
154            } elseif ( $searchmode !== 'LIKE' && !StringUtils::isValidPCRERegex( "/$querypattern/" ) ) {
155                // @phan-suppress-previous-line SecurityCheck-ReDoS Yes, I know...
156                $error = 'abusefilter-list-regexerror';
157            }
158
159            if ( $error !== null ) {
160                $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
161                $out->addHTML(
162                    Html::rawElement(
163                        'p',
164                        [],
165                        Html::errorBox( $this->msg( $error )->escaped() )
166                    )
167                );
168
169                // Reset the conditions in case of error
170                $conds = [ 'af_deleted' => 0 ];
171                $searchmode = $querypattern = null;
172            }
173
174            // Viewers with the right to view private filters have access to the search
175            // function, which can query against protected filters and potentially expose PII.
176            // Remove protected filters from the query if the user doesn't have the right to search
177            // against them. This allows protected filters to be visible in the general list of
178            // filters at all other times.
179            // Filters with protected variables that have additional restrictions cannot be excluded using SQL
180            // but will be excluded in the AbuseFilterPager.
181            if ( !$this->afPermManager->canViewProtectedVariables( $performer, [] )->isGood() ) {
182                $dbr = $this->dbProvider->getReplicaDatabase();
183                $conds[] = $dbr->bitAnd( 'af_hidden', Flags::FILTER_USES_PROTECTED_VARS ) . ' = 0';
184            }
185        }
186
187        $this->showList(
188            [
189                'deleted' => $deleted,
190                'furtherOptions' => $furtherOptions,
191                'querypattern' => $querypattern,
192                'searchmode' => $searchmode,
193                'scope' => $scope,
194            ],
195            $conds
196        );
197    }
198
199    /**
200     * @param array $optarray
201     * @param array $conds
202     */
203    private function showList( array $optarray, array $conds = [ 'af_deleted' => 0 ] ) {
204        $performer = $this->getAuthority();
205        $config = $this->getConfig();
206        $centralDB = $config->get( 'AbuseFilterCentralDB' );
207        $dbIsCentral = $config->get( 'AbuseFilterIsCentral' );
208        $this->getOutput()->addHTML(
209            Html::rawElement( 'h2', [], $this->msg( 'abusefilter-list' )->parse() )
210        );
211
212        $deleted = $optarray['deleted'];
213        $furtherOptions = $optarray['furtherOptions'];
214        $scope = $optarray['scope'];
215        $querypattern = $optarray['querypattern'];
216        $searchmode = $optarray['searchmode'];
217
218        if ( $centralDB !== null && !$dbIsCentral && $scope === 'global' ) {
219            // TODO: remove the circular dependency
220            $pager = new GlobalAbuseFilterPager(
221                $this,
222                $this->linkRenderer,
223                $this->afPermManager,
224                $this->specsFormatter,
225                $this->centralDBManager,
226                $this->filterLookup,
227                $conds
228            );
229        } else {
230            $pager = new AbuseFilterPager(
231                $this,
232                $this->linkRenderer,
233                $this->linkBatchFactory,
234                $this->afPermManager,
235                $this->specsFormatter,
236                $this->filterLookup,
237                $conds,
238                $querypattern,
239                $searchmode
240            );
241        }
242
243        // Options form
244        $formDescriptor = [];
245
246        if ( $centralDB !== null ) {
247            $optionsMsg = [
248                'abusefilter-list-options-scope-local' => 'local',
249                'abusefilter-list-options-scope-global' => 'global',
250            ];
251            if ( $dbIsCentral ) {
252                // For central wiki: add third scope option
253                $optionsMsg['abusefilter-list-options-scope-all'] = 'all';
254            }
255            $formDescriptor['rulescope'] = [
256                'name' => 'rulescope',
257                'type' => 'radio',
258                'flatlist' => true,
259                'label-message' => 'abusefilter-list-options-scope',
260                'options-messages' => $optionsMsg,
261                'default' => $scope,
262            ];
263        }
264
265        $formDescriptor['deletedfilters'] = [
266            'name' => 'deletedfilters',
267            'type' => 'radio',
268            'flatlist' => true,
269            'label-message' => 'abusefilter-list-options-deleted',
270            'options-messages' => [
271                'abusefilter-list-options-deleted-show' => 'show',
272                'abusefilter-list-options-deleted-hide' => 'hide',
273                'abusefilter-list-options-deleted-only' => 'only',
274            ],
275            'default' => $deleted,
276        ];
277
278        $formDescriptor['furtheroptions'] = [
279            'name' => 'furtheroptions',
280            'type' => 'multiselect',
281            'label-message' => 'abusefilter-list-options-further-options',
282            'flatlist' => true,
283            'options' => [
284                $this->msg( 'abusefilter-list-options-hideprivate' )->parse() => 'hideprivate',
285                $this->msg( 'abusefilter-list-options-hidedisabled' )->parse() => 'hidedisabled',
286            ],
287            'default' => $furtherOptions
288        ];
289
290        if ( $this->afPermManager->canViewPrivateFilters( $performer ) ) {
291            $globalEnabled = $centralDB !== null && !$dbIsCentral;
292            $formDescriptor['querypattern'] = [
293                'name' => 'querypattern',
294                'type' => 'text',
295                'hide-if' => $globalEnabled ? [ '===', 'rulescope', 'global' ] : [],
296                'label-message' => 'abusefilter-list-options-searchfield',
297                'placeholder' => $this->msg( 'abusefilter-list-options-searchpattern' )->text(),
298                'default' => $querypattern
299            ];
300
301            $formDescriptor['searchoption'] = [
302                'name' => 'searchoption',
303                'type' => 'radio',
304                'flatlist' => true,
305                'label-message' => 'abusefilter-list-options-searchoptions',
306                'hide-if' => $globalEnabled ?
307                    [ 'OR', [ '===', 'querypattern', '' ], $formDescriptor['querypattern']['hide-if'] ] :
308                    [ '===', 'querypattern', '' ],
309                'options-messages' => [
310                    'abusefilter-list-options-search-like' => 'LIKE',
311                    'abusefilter-list-options-search-rlike' => 'RLIKE',
312                    'abusefilter-list-options-search-irlike' => 'IRLIKE',
313                ],
314                'default' => $searchmode
315            ];
316        }
317
318        $formDescriptor['limit'] = [
319            'name' => 'limit',
320            'type' => 'select',
321            'label-message' => 'abusefilter-list-limit',
322            'options' => $pager->getLimitSelectList(),
323            'default' => $pager->getLimit(),
324        ];
325
326        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
327            ->setTitle( $this->getTitle() )
328            ->setCollapsibleOptions( true )
329            ->setWrapperLegendMsg( 'abusefilter-list-options' )
330            ->setSubmitTextMsg( 'abusefilter-list-options-submit' )
331            ->setMethod( 'get' )
332            ->prepareForm()
333            ->displayForm( false );
334
335        $this->getOutput()->addParserOutputContent(
336            $pager->getFullOutput(),
337            ParserOptions::newFromContext( $this->getContext() )
338        );
339    }
340
341    /**
342     * Generates a summary of filter activity using the internal statistics.
343     */
344    public function showStatus() {
345        $totalCount = 0;
346        $matchCount = 0;
347        $overflowCount = 0;
348        foreach ( $this->getConfig()->get( 'AbuseFilterValidGroups' ) as $group ) {
349            $profile = $this->filterProfiler->getGroupProfile( $group );
350            $totalCount += $profile[ 'total' ];
351            $overflowCount += $profile[ 'overflow' ];
352            $matchCount += $profile[ 'matches' ];
353        }
354
355        if ( $totalCount > 0 ) {
356            $overflowPercent = round( 100 * $overflowCount / $totalCount, 2 );
357            $matchPercent = round( 100 * $matchCount / $totalCount, 2 );
358
359            $status = $this->msg( 'abusefilter-status' )
360                ->numParams(
361                    $totalCount,
362                    $overflowCount,
363                    $overflowPercent,
364                    $this->getConfig()->get( 'AbuseFilterConditionLimit' ),
365                    $matchCount,
366                    $matchPercent
367                )->parse();
368
369            $status = Html::rawElement( 'p', [ 'class' => 'mw-abusefilter-status' ], $status );
370            $this->getOutput()->addHTML( $status );
371        }
372    }
373}