Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.47% |
175 / 235 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewList | |
74.47% |
175 / 235 |
|
25.00% |
1 / 4 |
59.79 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
show | |
74.44% |
67 / 90 |
|
0.00% |
0 / 1 |
31.83 | |||
showList | |
79.13% |
91 / 115 |
|
0.00% |
0 / 1 |
10.91 | |||
showStatus | |
40.91% |
9 / 22 |
|
0.00% |
0 / 1 |
4.86 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\View; |
4 | |
5 | use MediaWiki\Cache\LinkBatchFactory; |
6 | use MediaWiki\Context\IContextSource; |
7 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
8 | use MediaWiki\Extension\AbuseFilter\CentralDBManager; |
9 | use MediaWiki\Extension\AbuseFilter\Filter\Flags; |
10 | use MediaWiki\Extension\AbuseFilter\FilterLookup; |
11 | use MediaWiki\Extension\AbuseFilter\FilterProfiler; |
12 | use MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterPager; |
13 | use MediaWiki\Extension\AbuseFilter\Pager\GlobalAbuseFilterPager; |
14 | use MediaWiki\Extension\AbuseFilter\SpecsFormatter; |
15 | use MediaWiki\Html\Html; |
16 | use MediaWiki\HTMLForm\HTMLForm; |
17 | use MediaWiki\Linker\LinkRenderer; |
18 | use MediaWiki\Parser\ParserOptions; |
19 | use OOUI; |
20 | use StringUtils; |
21 | use Wikimedia\Rdbms\IConnectionProvider; |
22 | |
23 | /** |
24 | * The default view used in Special:AbuseFilter |
25 | */ |
26 | class 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 | } |