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