Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.07% |
754 / 876 |
|
37.50% |
6 / 16 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewEdit | |
86.07% |
754 / 876 |
|
37.50% |
6 / 16 |
218.06 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
show | |
93.18% |
41 / 44 |
|
0.00% |
0 / 1 |
18.10 | |||
attemptSave | |
96.55% |
28 / 29 |
|
0.00% |
0 / 1 |
6 | |||
showUnrecoverableError | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
buildFilterEditor | |
84.96% |
226 / 266 |
|
0.00% |
0 / 1 |
59.84 | |||
buildConsequenceEditor | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
buildConsequenceSelector | |
92.82% |
323 / 348 |
|
0.00% |
0 / 1 |
28.29 | |||
getExistingSelector | |
87.23% |
41 / 47 |
|
0.00% |
0 / 1 |
8.13 | |||
normalizeBlocks | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
6.17 | |||
getAbsoluteBlockDuration | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
loadFilterData | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
loadFromDatabase | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
loadRequest | |
74.07% |
20 / 27 |
|
0.00% |
0 / 1 |
5.44 | |||
loadImportRequest | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
3.21 | |||
loadActions | |
16.67% |
7 / 42 |
|
0.00% |
0 / 1 |
145.21 | |||
exposeMessages | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\View; |
4 | |
5 | use HtmlArmor; |
6 | use LogicException; |
7 | use MediaWiki\Context\IContextSource; |
8 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
9 | use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry; |
10 | use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory; |
11 | use MediaWiki\Extension\AbuseFilter\Filter\Filter; |
12 | use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException; |
13 | use MediaWiki\Extension\AbuseFilter\Filter\FilterVersionNotFoundException; |
14 | use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter; |
15 | use MediaWiki\Extension\AbuseFilter\FilterImporter; |
16 | use MediaWiki\Extension\AbuseFilter\FilterLookup; |
17 | use MediaWiki\Extension\AbuseFilter\FilterProfiler; |
18 | use MediaWiki\Extension\AbuseFilter\FilterStore; |
19 | use MediaWiki\Extension\AbuseFilter\InvalidImportDataException; |
20 | use MediaWiki\Extension\AbuseFilter\SpecsFormatter; |
21 | use MediaWiki\Html\Html; |
22 | use MediaWiki\Linker\Linker; |
23 | use MediaWiki\Linker\LinkRenderer; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\SpecialPage\SpecialPage; |
26 | use MediaWiki\Xml\Xml; |
27 | use OOUI; |
28 | use StatusValue; |
29 | use UnexpectedValueException; |
30 | use Wikimedia\Rdbms\IDBAccessObject; |
31 | use Wikimedia\Rdbms\IExpression; |
32 | use Wikimedia\Rdbms\LBFactory; |
33 | use Wikimedia\Rdbms\LikeValue; |
34 | use Wikimedia\Rdbms\SelectQueryBuilder; |
35 | |
36 | class AbuseFilterViewEdit extends AbuseFilterView { |
37 | /** |
38 | * @var int|null The history ID of the current filter |
39 | */ |
40 | private $historyID; |
41 | /** @var int|string */ |
42 | private $filter; |
43 | |
44 | /** @var LBFactory */ |
45 | private $lbFactory; |
46 | |
47 | /** @var FilterProfiler */ |
48 | private $filterProfiler; |
49 | |
50 | /** @var FilterLookup */ |
51 | private $filterLookup; |
52 | |
53 | /** @var FilterImporter */ |
54 | private $filterImporter; |
55 | |
56 | /** @var FilterStore */ |
57 | private $filterStore; |
58 | |
59 | /** @var EditBoxBuilderFactory */ |
60 | private $boxBuilderFactory; |
61 | |
62 | /** @var ConsequencesRegistry */ |
63 | private $consequencesRegistry; |
64 | |
65 | /** @var SpecsFormatter */ |
66 | private $specsFormatter; |
67 | |
68 | public function __construct( |
69 | LBFactory $lbFactory, |
70 | AbuseFilterPermissionManager $afPermManager, |
71 | FilterProfiler $filterProfiler, |
72 | FilterLookup $filterLookup, |
73 | FilterImporter $filterImporter, |
74 | FilterStore $filterStore, |
75 | EditBoxBuilderFactory $boxBuilderFactory, |
76 | ConsequencesRegistry $consequencesRegistry, |
77 | SpecsFormatter $specsFormatter, |
78 | IContextSource $context, |
79 | LinkRenderer $linkRenderer, |
80 | string $basePageName, |
81 | array $params |
82 | ) { |
83 | parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params ); |
84 | $this->lbFactory = $lbFactory; |
85 | $this->filterProfiler = $filterProfiler; |
86 | $this->filterLookup = $filterLookup; |
87 | $this->filterImporter = $filterImporter; |
88 | $this->filterStore = $filterStore; |
89 | $this->boxBuilderFactory = $boxBuilderFactory; |
90 | $this->consequencesRegistry = $consequencesRegistry; |
91 | $this->specsFormatter = $specsFormatter; |
92 | $this->specsFormatter->setMessageLocalizer( $this->getContext() ); |
93 | $this->filter = $this->mParams['filter']; |
94 | $this->historyID = $this->mParams['history'] ?? null; |
95 | } |
96 | |
97 | /** |
98 | * Shows the page |
99 | */ |
100 | public function show() { |
101 | $out = $this->getOutput(); |
102 | $out->enableOOUI(); |
103 | $request = $this->getRequest(); |
104 | $out->setPageTitleMsg( $this->msg( 'abusefilter-edit' ) ); |
105 | $out->addHelpLink( 'Extension:AbuseFilter/Rules format' ); |
106 | |
107 | if ( !is_numeric( $this->filter ) && $this->filter !== null ) { |
108 | $this->showUnrecoverableError( 'abusefilter-edit-badfilter' ); |
109 | return; |
110 | } |
111 | $filter = $this->filter ? (int)$this->filter : null; |
112 | $history_id = $this->historyID; |
113 | if ( $this->historyID ) { |
114 | $dbr = $this->lbFactory->getReplicaDatabase(); |
115 | $lastID = (int)$dbr->newSelectQueryBuilder() |
116 | ->select( 'afh_id' ) |
117 | ->from( 'abuse_filter_history' ) |
118 | ->where( [ |
119 | 'afh_filter' => $filter, |
120 | ] ) |
121 | ->orderBy( 'afh_id', SelectQueryBuilder::SORT_DESC ) |
122 | ->caller( __METHOD__ ) |
123 | ->fetchField(); |
124 | // change $history_id to null if it's current version id |
125 | if ( $lastID === $this->historyID ) { |
126 | $history_id = null; |
127 | } |
128 | } |
129 | |
130 | // Add the default warning and disallow messages in a JS variable |
131 | $this->exposeMessages(); |
132 | |
133 | $canEdit = $this->afPermManager->canEdit( $this->getAuthority() ); |
134 | |
135 | if ( $filter === null && !$canEdit ) { |
136 | // Special case: Special:AbuseFilter/new is certainly not usable if the user cannot edit |
137 | $this->showUnrecoverableError( 'abusefilter-edit-notallowed' ); |
138 | return; |
139 | } |
140 | |
141 | $isImport = $request->wasPosted() && $request->getRawVal( 'wpImportText' ) !== null; |
142 | |
143 | if ( !$isImport && $request->wasPosted() && $canEdit ) { |
144 | $this->attemptSave( $filter, $history_id ); |
145 | return; |
146 | } |
147 | |
148 | if ( $isImport ) { |
149 | $filterObj = $this->loadImportRequest(); |
150 | if ( $filterObj === null ) { |
151 | $this->showUnrecoverableError( 'abusefilter-import-invalid-data' ); |
152 | return; |
153 | } |
154 | } else { |
155 | // The request wasn't posted (i.e. just viewing the filter) or the user cannot edit |
156 | try { |
157 | $filterObj = $this->loadFromDatabase( $filter, $history_id ); |
158 | } catch ( FilterNotFoundException $_ ) { |
159 | $filterObj = null; |
160 | } |
161 | if ( $filterObj === null || ( $history_id && (int)$filterObj->getID() !== $filter ) ) { |
162 | $this->showUnrecoverableError( 'abusefilter-edit-badfilter' ); |
163 | return; |
164 | } |
165 | } |
166 | |
167 | $this->buildFilterEditor( null, $filterObj, $filter, $history_id ); |
168 | } |
169 | |
170 | /** |
171 | * @param int|null $filter The filter ID or null for a new filter |
172 | * @param int|null $history_id The history ID of the filter, if applicable. Otherwise null |
173 | */ |
174 | private function attemptSave( ?int $filter, $history_id ): void { |
175 | $out = $this->getOutput(); |
176 | $request = $this->getRequest(); |
177 | $authority = $this->getAuthority(); |
178 | |
179 | [ $newFilter, $origFilter ] = $this->loadRequest( $filter ); |
180 | |
181 | $tokenFilter = $filter === null ? 'new' : (string)$filter; |
182 | $editToken = $request->getVal( 'wpEditToken' ); |
183 | $tokenMatches = $this->getCsrfTokenSet()->matchToken( $editToken, [ 'abusefilter', $tokenFilter ] ); |
184 | |
185 | if ( !$tokenMatches ) { |
186 | // Token invalid or expired while the page was open, warn to retry |
187 | $status = StatusValue::newGood(); |
188 | $status->warning( 'abusefilter-edit-token-not-match' ); |
189 | $this->buildFilterEditor( $status, $newFilter, $filter, $history_id ); |
190 | return; |
191 | } |
192 | |
193 | $status = $this->filterStore->saveFilter( $authority, $filter, $newFilter, $origFilter ); |
194 | |
195 | if ( !$status->isGood() ) { |
196 | if ( $status->isOK() ) { |
197 | // Fixable error, show the editing interface |
198 | $this->buildFilterEditor( $status, $newFilter, $filter, $history_id ); |
199 | } else { |
200 | $this->showUnrecoverableError( $status->getMessages()[0] ); |
201 | } |
202 | } elseif ( $status->getValue() === false ) { |
203 | // No change |
204 | $out->redirect( $this->getTitle()->getLocalURL() ); |
205 | } else { |
206 | // Everything went fine! |
207 | [ $new_id, $history_id ] = $status->getValue(); |
208 | $out->redirect( |
209 | $this->getTitle()->getLocalURL( |
210 | [ |
211 | 'result' => 'success', |
212 | 'changedfilter' => $new_id, |
213 | 'changeid' => $history_id, |
214 | ] |
215 | ) |
216 | ); |
217 | } |
218 | } |
219 | |
220 | /** |
221 | * @param string|\Wikimedia\Message\MessageSpecifier $msg |
222 | */ |
223 | private function showUnrecoverableError( $msg ): void { |
224 | $out = $this->getOutput(); |
225 | |
226 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
227 | $out->addHTML( Html::errorBox( $this->msg( $msg )->parseAsBlock() ) ); |
228 | $href = $this->getTitle()->getFullURL(); |
229 | $btn = new OOUI\ButtonWidget( [ |
230 | 'label' => $this->msg( 'abusefilter-return' )->text(), |
231 | 'href' => $href |
232 | ] ); |
233 | $out->addHTML( $btn ); |
234 | } |
235 | |
236 | /** |
237 | * Builds the full form for edit filters, adding it to the OutputPage. This method can be called in 5 different |
238 | * situations, for a total of 5 different data sources for $filterObj and $actions: |
239 | * 1 - View the result of importing a filter |
240 | * 2 - Create a new filter |
241 | * 3 - Load the current version of an existing filter |
242 | * 4 - Load an old version of an existing filter |
243 | * 5 - Show the user input again if saving fails after one of the steps above |
244 | * |
245 | * @param StatusValue|null $status A status for showing warnings or errors above the filter box |
246 | * @param Filter $filterObj |
247 | * @param int|null $filter The filter ID, or null for a new filter |
248 | * @param int|null $history_id The history ID of the filter, if applicable. Otherwise null |
249 | */ |
250 | private function buildFilterEditor( |
251 | ?StatusValue $status, |
252 | Filter $filterObj, |
253 | ?int $filter, |
254 | $history_id |
255 | ) { |
256 | $out = $this->getOutput(); |
257 | $out->addJsConfigVars( 'isFilterEditor', true ); |
258 | $lang = $this->getLanguage(); |
259 | $authority = $this->getAuthority(); |
260 | $actions = $filterObj->getActions(); |
261 | |
262 | $isCreatingNewFilter = $filter === null; |
263 | $out->addSubtitle( $this->msg( |
264 | $isCreatingNewFilter ? 'abusefilter-edit-subtitle-new' : 'abusefilter-edit-subtitle', |
265 | $isCreatingNewFilter ? $filter : $this->getLanguage()->formatNum( $filter ), |
266 | $history_id |
267 | )->parse() ); |
268 | |
269 | // Grab the current hidden flag from the DB, in case we're editing an older, public revision of a filter that is |
270 | // currently hidden, so that we can also hide that public revision. |
271 | if ( |
272 | ( $filterObj->isHidden() || ( |
273 | $filter !== null && $this->filterLookup->getFilter( $filter, false )->isHidden() ) |
274 | ) && !$this->afPermManager->canViewPrivateFilters( $authority ) |
275 | ) { |
276 | $out->addHTML( $this->msg( 'abusefilter-edit-denied' )->escaped() ); |
277 | return; |
278 | } |
279 | |
280 | // Filters that use protected variables should always be hidden from public view |
281 | $lacksPermissionDueToProtectedVariables = $filterObj->isProtected() && |
282 | !$this->afPermManager->canViewProtectedVariablesInFilter( $authority, $filterObj )->isGood(); |
283 | |
284 | if ( $filter !== null && !$lacksPermissionDueToProtectedVariables ) { |
285 | $currentFilterObj = $this->filterLookup->getFilter( $filter, false ); |
286 | $lacksPermissionDueToProtectedVariables = $currentFilterObj->isProtected() && |
287 | !$this->afPermManager->canViewProtectedVariablesInFilter( $authority, $currentFilterObj )->isGood(); |
288 | } |
289 | |
290 | if ( $lacksPermissionDueToProtectedVariables ) { |
291 | $out->addHTML( $this->msg( 'abusefilter-edit-denied-protected-vars' )->escaped() ); |
292 | return; |
293 | } |
294 | |
295 | if ( $isCreatingNewFilter ) { |
296 | $title = $this->msg( 'abusefilter-add' ); |
297 | } elseif ( $this->afPermManager->canEditFilter( $authority, $filterObj ) ) { |
298 | $title = $this->msg( 'abusefilter-edit-specific' ) |
299 | ->numParams( $this->filter ) |
300 | ->params( $filterObj->getName() ); |
301 | } else { |
302 | $title = $this->msg( 'abusefilter-view-specific' ) |
303 | ->numParams( $this->filter ) |
304 | ->params( $filterObj->getName() ); |
305 | } |
306 | $out->setPageTitleMsg( $title ); |
307 | |
308 | $readOnly = !$this->afPermManager->canEditFilter( $authority, $filterObj ); |
309 | |
310 | if ( $history_id ) { |
311 | $oldWarningMessage = $readOnly |
312 | ? 'abusefilter-edit-oldwarning-view' |
313 | : 'abusefilter-edit-oldwarning'; |
314 | $out->addWikiMsg( $oldWarningMessage, $history_id, (string)$filter ); |
315 | } |
316 | |
317 | if ( $status !== null && !$status->isGood() ) { |
318 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
319 | foreach ( $status->getMessages( 'error' ) as $message ) { |
320 | $out->addHTML( Html::errorBox( $this->msg( $message )->parseAsBlock() ) ); |
321 | } |
322 | |
323 | foreach ( $status->getMessages( 'warning' ) as $message ) { |
324 | $out->addHTML( Html::warningBox( $this->msg( $message )->parseAsBlock() ) ); |
325 | } |
326 | } |
327 | |
328 | $fields = []; |
329 | |
330 | $fields['abusefilter-edit-id'] = |
331 | $isCreatingNewFilter ? |
332 | $this->msg( 'abusefilter-edit-new' )->escaped() : |
333 | htmlspecialchars( $lang->formatNum( (string)$filter ) ); |
334 | $fields['abusefilter-edit-description'] = |
335 | new OOUI\TextInputWidget( [ |
336 | 'name' => 'wpFilterDescription', |
337 | 'id' => 'mw-abusefilter-edit-description-input', |
338 | 'value' => $filterObj->getName(), |
339 | 'readOnly' => $readOnly |
340 | ] |
341 | ); |
342 | |
343 | $validGroups = $this->getConfig()->get( 'AbuseFilterValidGroups' ); |
344 | if ( count( $validGroups ) > 1 ) { |
345 | $groupSelector = |
346 | new OOUI\DropdownInputWidget( [ |
347 | 'name' => 'wpFilterGroup', |
348 | 'id' => 'mw-abusefilter-edit-group-input', |
349 | 'value' => $filterObj->getGroup(), |
350 | 'disabled' => $readOnly |
351 | ] ); |
352 | |
353 | $options = []; |
354 | foreach ( $validGroups as $group ) { |
355 | $options += [ $this->specsFormatter->nameGroup( $group ) => $group ]; |
356 | } |
357 | |
358 | $options = Html::listDropdownOptionsOoui( $options ); |
359 | $groupSelector->setOptions( $options ); |
360 | |
361 | $fields['abusefilter-edit-group'] = $groupSelector; |
362 | } |
363 | |
364 | // Hit count display |
365 | $hitCount = $filterObj->getHitCount(); |
366 | if ( $hitCount !== null && $this->afPermManager->canSeeLogDetails( $authority ) ) { |
367 | $count_display = $this->msg( 'abusefilter-hitcount' ) |
368 | ->numParams( $hitCount )->text(); |
369 | $hitCount = $this->linkRenderer->makeKnownLink( |
370 | SpecialPage::getTitleFor( 'AbuseLog' ), |
371 | $count_display, |
372 | [], |
373 | [ 'wpSearchFilter' => $filterObj->getID() ] |
374 | ); |
375 | |
376 | $fields['abusefilter-edit-hitcount'] = $hitCount; |
377 | } |
378 | |
379 | if ( $filter !== null && $filterObj->isEnabled() ) { |
380 | // Statistics |
381 | [ |
382 | 'count' => $totalCount, |
383 | 'matches' => $matchesCount, |
384 | 'total-time' => $curTotalTime, |
385 | 'total-cond' => $curTotalConds, |
386 | ] = $this->filterProfiler->getFilterProfile( $filter ); |
387 | |
388 | if ( $totalCount > 0 ) { |
389 | $matchesPercent = round( 100 * $matchesCount / $totalCount, 2 ); |
390 | $avgTime = round( $curTotalTime / $totalCount, 2 ); |
391 | $avgCond = round( $curTotalConds / $totalCount, 1 ); |
392 | |
393 | $fields['abusefilter-edit-status-label'] = $this->msg( 'abusefilter-edit-status' ) |
394 | ->numParams( $totalCount, $matchesCount, $matchesPercent, $avgTime, $avgCond ) |
395 | ->parse(); |
396 | } |
397 | } |
398 | |
399 | $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder( $this, $authority, $out ); |
400 | |
401 | $fields['abusefilter-edit-rules'] = $boxBuilder->buildEditBox( |
402 | $filterObj->getRules(), |
403 | true |
404 | ); |
405 | $fields['abusefilter-edit-notes'] = |
406 | new OOUI\MultilineTextInputWidget( [ |
407 | 'name' => 'wpFilterNotes', |
408 | 'value' => $filterObj->getComments() . "\n", |
409 | 'rows' => 15, |
410 | 'readOnly' => $readOnly, |
411 | 'id' => 'mw-abusefilter-notes-editor' |
412 | ] ); |
413 | |
414 | // Build checkboxes |
415 | $checkboxes = [ 'hidden', 'enabled', 'deleted' ]; |
416 | $flags = ''; |
417 | |
418 | // Show the 'protected' check box either to indicate that the filter is protected, or |
419 | // to allow a user to protect the filter, if the filter needs to be protected. |
420 | if ( |
421 | $filterObj->isProtected() || |
422 | ( |
423 | $status !== null && |
424 | $status->hasMessage( 'abusefilter-edit-protected-variable-not-protected' ) |
425 | ) |
426 | ) { |
427 | $checkboxes[] = 'protected'; |
428 | } |
429 | |
430 | if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) { |
431 | $checkboxes[] = 'global'; |
432 | } |
433 | |
434 | if ( $filterObj->isThrottled() ) { |
435 | $throttledActionNames = array_intersect( |
436 | $filterObj->getActionsNames(), |
437 | $this->consequencesRegistry->getDangerousActionNames() |
438 | ); |
439 | |
440 | if ( $throttledActionNames ) { |
441 | $throttledActionsLocalized = []; |
442 | foreach ( $throttledActionNames as $actionName ) { |
443 | $throttledActionsLocalized[] = $this->specsFormatter->getActionMessage( $actionName )->text(); |
444 | } |
445 | |
446 | $throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning' ) |
447 | ->plaintextParams( $lang->commaList( $throttledActionsLocalized ) ) |
448 | ->params( count( $throttledActionsLocalized ) ) |
449 | ->parseAsBlock(); |
450 | } else { |
451 | $throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning-no-actions' ) |
452 | ->parseAsBlock(); |
453 | } |
454 | $flags .= Html::warningBox( $throttledMsg ); |
455 | } |
456 | |
457 | foreach ( $checkboxes as $checkboxId ) { |
458 | // Messages that can be used here: |
459 | // * abusefilter-edit-enabled |
460 | // * abusefilter-edit-deleted |
461 | // * abusefilter-edit-hidden |
462 | // * abusefilter-edit-protected |
463 | // * abusefilter-edit-global |
464 | $message = "abusefilter-edit-$checkboxId"; |
465 | // isEnabled(), isDeleted(), isHidden(), isProtected(), isGlobal() |
466 | $method = 'is' . ucfirst( $checkboxId ); |
467 | // wpFilterEnabled, wpFilterDeleted, wpFilterHidden, wpFilterProtected, wpFilterGlobal |
468 | $postVar = 'wpFilter' . ucfirst( $checkboxId ); |
469 | |
470 | $checkboxAttribs = [ |
471 | 'name' => $postVar, |
472 | 'id' => $postVar, |
473 | 'selected' => $filterObj->$method(), |
474 | 'disabled' => $readOnly |
475 | ]; |
476 | $labelAttribs = [ |
477 | 'label' => $this->msg( $message )->text(), |
478 | 'align' => 'inline', |
479 | ]; |
480 | |
481 | if ( $checkboxId === 'global' && !$this->afPermManager->canEditGlobal( $authority ) ) { |
482 | $checkboxAttribs['disabled'] = 'disabled'; |
483 | } |
484 | |
485 | if ( $checkboxId == 'protected' ) { |
486 | if ( $filterObj->isProtected() ) { |
487 | $checkboxAttribs['disabled'] = true; |
488 | $labelAttribs['label'] = $this->msg( |
489 | 'abusefilter-edit-protected-variable-already-protected' |
490 | )->text(); |
491 | } else { |
492 | $labelAttribs['label'] = new OOUI\HtmlSnippet( |
493 | $this->msg( $message )->parse() |
494 | ); |
495 | $labelAttribs['help'] = $this->msg( 'abusefilter-edit-protected-help-message' )->text(); |
496 | $labelAttribs['helpInline'] = true; |
497 | } |
498 | } |
499 | |
500 | // Set readonly on deleted if the filter isn't disabled |
501 | if ( $checkboxId === 'deleted' && $filterObj->isEnabled() ) { |
502 | $checkboxAttribs['disabled'] = 'disabled'; |
503 | } |
504 | |
505 | // Add infusable where needed |
506 | if ( $checkboxId === 'deleted' || $checkboxId === 'enabled' ) { |
507 | $checkboxAttribs['infusable'] = true; |
508 | if ( $checkboxId === 'deleted' ) { |
509 | // wpFilterDeletedLabel |
510 | $labelAttribs['id'] = $postVar . 'Label'; |
511 | $labelAttribs['infusable'] = true; |
512 | } |
513 | } |
514 | |
515 | $checkbox = |
516 | new OOUI\FieldLayout( |
517 | new OOUI\CheckboxInputWidget( $checkboxAttribs ), |
518 | $labelAttribs |
519 | ); |
520 | $flags .= $checkbox; |
521 | } |
522 | |
523 | $fields['abusefilter-edit-flags'] = $flags; |
524 | |
525 | if ( $filter !== null ) { |
526 | $tools = ''; |
527 | if ( $this->afPermManager->canRevertFilterActions( $authority ) ) { |
528 | $tools .= Html::rawElement( |
529 | 'p', [], |
530 | $this->linkRenderer->makeLink( |
531 | $this->getTitle( "revert/$filter" ), |
532 | new HtmlArmor( $this->msg( 'abusefilter-edit-revert' )->parse() ) |
533 | ) |
534 | ); |
535 | } |
536 | |
537 | if ( $this->afPermManager->canUseTestTools( $authority ) ) { |
538 | // Test link |
539 | $tools .= Html::rawElement( |
540 | 'p', [], |
541 | $this->linkRenderer->makeLink( |
542 | $this->getTitle( "test/$filter" ), |
543 | new HtmlArmor( $this->msg( 'abusefilter-edit-test-link' )->parse() ) |
544 | ) |
545 | ); |
546 | } |
547 | |
548 | // Last modification details |
549 | $user = $this->getUser(); |
550 | $userLink = |
551 | Linker::userLink( $filterObj->getUserID(), $filterObj->getUserName() ) . |
552 | Linker::userToolLinks( $filterObj->getUserID(), $filterObj->getUserName() ); |
553 | $fields['abusefilter-edit-lastmod'] = |
554 | $this->msg( 'abusefilter-edit-lastmod-text' ) |
555 | ->rawParams( |
556 | $this->getLinkToLatestDiff( |
557 | $filter, |
558 | $lang->userTimeAndDate( $filterObj->getTimestamp(), $user ) |
559 | ), |
560 | $userLink, |
561 | $this->getLinkToLatestDiff( |
562 | $filter, |
563 | $lang->userDate( $filterObj->getTimestamp(), $user ) |
564 | ), |
565 | $this->getLinkToLatestDiff( |
566 | $filter, |
567 | $lang->userTime( $filterObj->getTimestamp(), $user ) |
568 | ) |
569 | )->params( |
570 | wfEscapeWikiText( $filterObj->getUserName() ) |
571 | )->parse(); |
572 | $history_display = new HtmlArmor( $this->msg( 'abusefilter-edit-viewhistory' )->parse() ); |
573 | $fields['abusefilter-edit-history'] = |
574 | $this->linkRenderer->makeKnownLink( $this->getTitle( 'history/' . $filter ), $history_display ); |
575 | |
576 | $exportText = $this->filterImporter->encodeData( $filterObj, $actions ); |
577 | $tools .= Html::rawElement( 'a', [ 'href' => '#', 'id' => 'mw-abusefilter-export-link' ], |
578 | $this->msg( 'abusefilter-edit-export' )->parse() ); |
579 | $tools .= |
580 | new OOUI\MultilineTextInputWidget( [ |
581 | 'id' => 'mw-abusefilter-export', |
582 | 'readOnly' => true, |
583 | 'value' => $exportText, |
584 | 'rows' => 10 |
585 | ] ); |
586 | |
587 | $fields['abusefilter-edit-tools'] = $tools; |
588 | } |
589 | |
590 | $form = Xml::fieldset( |
591 | $this->msg( 'abusefilter-edit-main' )->text(), |
592 | // TODO: deprecated, use OOUI or Codex widgets instead |
593 | Xml::buildForm( $fields ) |
594 | ); |
595 | $form .= Xml::fieldset( |
596 | $this->msg( 'abusefilter-edit-consequences' )->text(), |
597 | $this->buildConsequenceEditor( $filterObj, $actions ) |
598 | ); |
599 | |
600 | $urlFilter = $filter === null ? 'new' : (string)$filter; |
601 | if ( !$readOnly ) { |
602 | $form .= |
603 | new OOUI\ButtonInputWidget( [ |
604 | 'type' => 'submit', |
605 | 'label' => $this->msg( 'abusefilter-edit-save' )->text(), |
606 | 'useInputTag' => true, |
607 | 'accesskey' => 's', |
608 | 'flags' => [ 'progressive', 'primary' ] |
609 | ] ); |
610 | $form .= Html::hidden( |
611 | 'wpEditToken', |
612 | $this->getCsrfTokenSet()->getToken( [ 'abusefilter', $urlFilter ] )->toString() |
613 | ); |
614 | } |
615 | |
616 | $form = Html::rawElement( 'form', |
617 | [ |
618 | 'action' => $this->getTitle( $urlFilter )->getFullURL(), |
619 | 'method' => 'post', |
620 | 'id' => 'mw-abusefilter-editing-form' |
621 | ], |
622 | $form |
623 | ); |
624 | |
625 | $out->addHTML( $form ); |
626 | |
627 | if ( $history_id ) { |
628 | // @phan-suppress-next-line PhanPossiblyUndeclaredVariable,PhanTypeMismatchArgumentNullable |
629 | $out->addWikiMsg( $oldWarningMessage, $history_id, $filter ); |
630 | } |
631 | } |
632 | |
633 | /** |
634 | * Builds the "actions" editor for a given filter. |
635 | * @param Filter $filterObj |
636 | * @param array[] $actions Array of rows from the abuse_filter_action table |
637 | * corresponding to the filter object |
638 | * @return string HTML text for an action editor. |
639 | */ |
640 | private function buildConsequenceEditor( Filter $filterObj, array $actions ) { |
641 | $enabledActions = $this->consequencesRegistry->getAllEnabledActionNames(); |
642 | |
643 | $setActions = []; |
644 | foreach ( $enabledActions as $action ) { |
645 | $setActions[$action] = array_key_exists( $action, $actions ); |
646 | } |
647 | |
648 | $output = ''; |
649 | |
650 | foreach ( $enabledActions as $action ) { |
651 | $params = $actions[$action] ?? null; |
652 | $output .= $this->buildConsequenceSelector( |
653 | $action, $setActions[$action], $filterObj, $params ); |
654 | } |
655 | |
656 | return $output; |
657 | } |
658 | |
659 | /** |
660 | * @param string $action The action to build an editor for |
661 | * @param bool $set Whether or not the action is activated |
662 | * @param Filter $filterObj |
663 | * @param string[]|null $parameters Action parameters. Null iff $set is false. |
664 | * @return string|\OOUI\FieldLayout |
665 | */ |
666 | private function buildConsequenceSelector( $action, $set, $filterObj, ?array $parameters ) { |
667 | $config = $this->getConfig(); |
668 | $authority = $this->getAuthority(); |
669 | $actions = $this->consequencesRegistry->getAllEnabledActionNames(); |
670 | if ( !in_array( $action, $actions, true ) ) { |
671 | return ''; |
672 | } |
673 | |
674 | $readOnly = !$this->afPermManager->canEditFilter( $authority, $filterObj ); |
675 | |
676 | switch ( $action ) { |
677 | case 'throttle': |
678 | // Throttling is only available via object caching |
679 | if ( $config->get( MainConfigNames::MainCacheType ) === CACHE_NONE ) { |
680 | return ''; |
681 | } |
682 | $throttleSettings = |
683 | new OOUI\FieldLayout( |
684 | new OOUI\CheckboxInputWidget( [ |
685 | 'name' => 'wpFilterActionThrottle', |
686 | 'id' => 'mw-abusefilter-action-checkbox-throttle', |
687 | 'selected' => $set, |
688 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
689 | 'disabled' => $readOnly |
690 | ] |
691 | ), |
692 | [ |
693 | 'label' => $this->msg( 'abusefilter-edit-action-throttle' )->text(), |
694 | 'align' => 'inline' |
695 | ] |
696 | ); |
697 | $throttleFields = []; |
698 | |
699 | if ( $set ) { |
700 | // @phan-suppress-next-line PhanTypeArraySuspiciousNullable $parameters is array here |
701 | [ $throttleCount, $throttlePeriod ] = explode( ',', $parameters[1], 2 ); |
702 | |
703 | $throttleGroups = array_slice( $parameters, 2 ); |
704 | } else { |
705 | $throttleCount = 3; |
706 | $throttlePeriod = 60; |
707 | |
708 | $throttleGroups = [ 'user' ]; |
709 | } |
710 | |
711 | $throttleFields[] = |
712 | new OOUI\FieldLayout( |
713 | new OOUI\TextInputWidget( [ |
714 | 'type' => 'number', |
715 | 'name' => 'wpFilterThrottleCount', |
716 | 'value' => $throttleCount, |
717 | 'readOnly' => $readOnly |
718 | ] |
719 | ), |
720 | [ |
721 | 'label' => $this->msg( 'abusefilter-edit-throttle-count' )->text() |
722 | ] |
723 | ); |
724 | $throttleFields[] = |
725 | new OOUI\FieldLayout( |
726 | new OOUI\TextInputWidget( [ |
727 | 'type' => 'number', |
728 | 'name' => 'wpFilterThrottlePeriod', |
729 | 'value' => $throttlePeriod, |
730 | 'readOnly' => $readOnly |
731 | ] |
732 | ), |
733 | [ |
734 | 'label' => $this->msg( 'abusefilter-edit-throttle-period' )->text() |
735 | ] |
736 | ); |
737 | |
738 | $groupsHelpLink = Html::element( |
739 | 'a', |
740 | [ |
741 | 'href' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/' . |
742 | 'Extension:AbuseFilter/Actions#Throttling', |
743 | 'target' => '_blank' |
744 | ], |
745 | $this->msg( 'abusefilter-edit-throttle-groups-help-text' )->text() |
746 | ); |
747 | $groupsHelp = $this->msg( 'abusefilter-edit-throttle-groups-help' ) |
748 | ->rawParams( $groupsHelpLink )->escaped(); |
749 | $hiddenGroups = |
750 | new OOUI\FieldLayout( |
751 | new OOUI\MultilineTextInputWidget( [ |
752 | 'name' => 'wpFilterThrottleGroups', |
753 | 'value' => implode( "\n", $throttleGroups ), |
754 | 'rows' => 5, |
755 | 'placeholder' => $this->msg( 'abusefilter-edit-throttle-hidden-placeholder' )->text(), |
756 | 'infusable' => true, |
757 | 'id' => 'mw-abusefilter-hidden-throttle-field', |
758 | 'readOnly' => $readOnly |
759 | ] |
760 | ), |
761 | [ |
762 | 'label' => new OOUI\HtmlSnippet( |
763 | $this->msg( 'abusefilter-edit-throttle-groups' )->parse() |
764 | ), |
765 | 'align' => 'top', |
766 | 'id' => 'mw-abusefilter-hidden-throttle', |
767 | 'help' => new OOUI\HtmlSnippet( $groupsHelp ), |
768 | 'helpInline' => true |
769 | ] |
770 | ); |
771 | |
772 | $throttleFields[] = $hiddenGroups; |
773 | |
774 | $throttleConfig = [ |
775 | 'values' => $throttleGroups, |
776 | 'label' => $this->msg( 'abusefilter-edit-throttle-groups' )->parse(), |
777 | 'disabled' => $readOnly, |
778 | 'help' => $groupsHelp |
779 | ]; |
780 | $this->getOutput()->addJsConfigVars( 'throttleConfig', $throttleConfig ); |
781 | |
782 | $throttleSettings .= |
783 | Html::rawElement( |
784 | 'div', |
785 | [ 'id' => 'mw-abusefilter-throttle-parameters' ], |
786 | new OOUI\FieldsetLayout( [ 'items' => $throttleFields ] ) |
787 | ); |
788 | return $throttleSettings; |
789 | case 'disallow': |
790 | case 'warn': |
791 | $output = ''; |
792 | $formName = $action === 'warn' ? 'wpFilterActionWarn' : 'wpFilterActionDisallow'; |
793 | $checkbox = |
794 | new OOUI\FieldLayout( |
795 | new OOUI\CheckboxInputWidget( [ |
796 | 'name' => $formName, |
797 | // mw-abusefilter-action-checkbox-warn, mw-abusefilter-action-checkbox-disallow |
798 | 'id' => "mw-abusefilter-action-checkbox-$action", |
799 | 'selected' => $set, |
800 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
801 | 'disabled' => $readOnly |
802 | ] |
803 | ), |
804 | [ |
805 | // abusefilter-edit-action-warn, abusefilter-edit-action-disallow |
806 | 'label' => $this->msg( "abusefilter-edit-action-$action" )->text(), |
807 | 'align' => 'inline' |
808 | ] |
809 | ); |
810 | $output .= $checkbox; |
811 | $defaultWarnMsg = $config->get( 'AbuseFilterDefaultWarningMessage' ); |
812 | $defaultDisallowMsg = $config->get( 'AbuseFilterDefaultDisallowMessage' ); |
813 | |
814 | if ( $set && isset( $parameters[0] ) ) { |
815 | $msg = $parameters[0]; |
816 | } elseif ( |
817 | ( $action === 'warn' && isset( $defaultWarnMsg[$filterObj->getGroup()] ) ) || |
818 | ( $action === 'disallow' && isset( $defaultDisallowMsg[$filterObj->getGroup()] ) ) |
819 | ) { |
820 | $msg = $action === 'warn' ? $defaultWarnMsg[$filterObj->getGroup()] : |
821 | $defaultDisallowMsg[$filterObj->getGroup()]; |
822 | } else { |
823 | $msg = $action === 'warn' ? 'abusefilter-warning' : 'abusefilter-disallowed'; |
824 | } |
825 | |
826 | $fields = []; |
827 | $fields[] = |
828 | $this->getExistingSelector( $msg, $readOnly, $action ); |
829 | $otherFieldName = $action === 'warn' ? 'wpFilterWarnMessageOther' |
830 | : 'wpFilterDisallowMessageOther'; |
831 | |
832 | $fields[] = |
833 | new OOUI\FieldLayout( |
834 | new OOUI\TextInputWidget( [ |
835 | 'name' => $otherFieldName, |
836 | 'value' => $msg, |
837 | // mw-abusefilter-warn-message-other, mw-abusefilter-disallow-message-other |
838 | 'id' => "mw-abusefilter-$action-message-other", |
839 | 'infusable' => true, |
840 | 'readOnly' => $readOnly |
841 | ] |
842 | ), |
843 | [ |
844 | 'label' => new OOUI\HtmlSnippet( |
845 | // abusefilter-edit-warn-other-label, abusefilter-edit-disallow-other-label |
846 | $this->msg( "abusefilter-edit-$action-other-label" )->parse() |
847 | ) |
848 | ] |
849 | ); |
850 | |
851 | $previewButton = |
852 | new OOUI\ButtonInputWidget( [ |
853 | // abusefilter-edit-warn-preview, abusefilter-edit-disallow-preview |
854 | 'label' => $this->msg( "abusefilter-edit-$action-preview" )->text(), |
855 | // mw-abusefilter-warn-preview-button, mw-abusefilter-disallow-preview-button |
856 | 'id' => "mw-abusefilter-$action-preview-button", |
857 | 'infusable' => true, |
858 | 'flags' => 'progressive' |
859 | ] |
860 | ); |
861 | |
862 | $buttonGroup = $previewButton; |
863 | if ( $authority->isAllowed( 'editinterface' ) ) { |
864 | $editButton = |
865 | new OOUI\ButtonInputWidget( [ |
866 | // abusefilter-edit-warn-edit, abusefilter-edit-disallow-edit |
867 | 'label' => $this->msg( "abusefilter-edit-$action-edit" )->text(), |
868 | // mw-abusefilter-warn-edit-button, mw-abusefilter-disallow-edit-button |
869 | 'id' => "mw-abusefilter-$action-edit-button" |
870 | ] |
871 | ); |
872 | $buttonGroup = |
873 | new OOUI\Widget( [ |
874 | 'content' => |
875 | new OOUI\HorizontalLayout( [ |
876 | 'items' => [ $previewButton, $editButton ], |
877 | 'classes' => [ |
878 | 'mw-abusefilter-preview-buttons', |
879 | 'mw-abusefilter-javascript-tools' |
880 | ] |
881 | ] ) |
882 | ] ); |
883 | } |
884 | $previewHolder = Html::rawElement( |
885 | 'div', |
886 | [ |
887 | // mw-abusefilter-warn-preview, mw-abusefilter-disallow-preview |
888 | 'id' => "mw-abusefilter-$action-preview", |
889 | 'style' => 'display:none' |
890 | ], |
891 | '' |
892 | ); |
893 | $fields[] = $buttonGroup; |
894 | $output .= |
895 | Html::rawElement( |
896 | 'div', |
897 | // mw-abusefilter-warn-parameters, mw-abusefilter-disallow-parameters |
898 | [ 'id' => "mw-abusefilter-$action-parameters" ], |
899 | new OOUI\FieldsetLayout( [ 'items' => $fields ] ) |
900 | ) . $previewHolder; |
901 | |
902 | return $output; |
903 | case 'tag': |
904 | $tags = $set ? $parameters : []; |
905 | '@phan-var string[] $parameters'; |
906 | $output = ''; |
907 | |
908 | $checkbox = |
909 | new OOUI\FieldLayout( |
910 | new OOUI\CheckboxInputWidget( [ |
911 | 'name' => 'wpFilterActionTag', |
912 | 'id' => 'mw-abusefilter-action-checkbox-tag', |
913 | 'selected' => $set, |
914 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
915 | 'disabled' => $readOnly |
916 | ] |
917 | ), |
918 | [ |
919 | 'label' => $this->msg( 'abusefilter-edit-action-tag' )->text(), |
920 | 'align' => 'inline' |
921 | ] |
922 | ); |
923 | $output .= $checkbox; |
924 | |
925 | $tagConfig = [ |
926 | 'values' => $tags, |
927 | 'label' => $this->msg( 'abusefilter-edit-tag-tag' )->parse(), |
928 | 'disabled' => $readOnly |
929 | ]; |
930 | $this->getOutput()->addJsConfigVars( 'tagConfig', $tagConfig ); |
931 | |
932 | $hiddenTags = |
933 | new OOUI\FieldLayout( |
934 | new OOUI\MultilineTextInputWidget( [ |
935 | 'name' => 'wpFilterTags', |
936 | 'value' => implode( ',', $tags ), |
937 | 'rows' => 5, |
938 | 'placeholder' => $this->msg( 'abusefilter-edit-tag-hidden-placeholder' )->text(), |
939 | 'infusable' => true, |
940 | 'id' => 'mw-abusefilter-hidden-tag-field', |
941 | 'readOnly' => $readOnly |
942 | ] |
943 | ), |
944 | [ |
945 | 'label' => new OOUI\HtmlSnippet( |
946 | $this->msg( 'abusefilter-edit-tag-tag' )->parse() |
947 | ), |
948 | 'align' => 'top', |
949 | 'id' => 'mw-abusefilter-hidden-tag' |
950 | ] |
951 | ); |
952 | $output .= |
953 | Html::rawElement( 'div', |
954 | [ 'id' => 'mw-abusefilter-tag-parameters' ], |
955 | $hiddenTags |
956 | ); |
957 | return $output; |
958 | case 'block': |
959 | if ( $set && count( $parameters ) === 3 ) { |
960 | // Both blocktalk and custom block durations available |
961 | [ $blockTalk, $defaultAnonDuration, $defaultUserDuration ] = $parameters; |
962 | } else { |
963 | if ( $set && count( $parameters ) === 1 ) { |
964 | // Only blocktalk available |
965 | $blockTalk = $parameters[0]; |
966 | } |
967 | $defaultAnonDuration = $config->get( 'AbuseFilterAnonBlockDuration' ) ?? |
968 | $config->get( 'AbuseFilterBlockDuration' ); |
969 | $defaultUserDuration = $config->get( 'AbuseFilterBlockDuration' ); |
970 | } |
971 | $suggestedBlocks = $this->getLanguage()->getBlockDurations( false ); |
972 | $suggestedBlocks = self::normalizeBlocks( $suggestedBlocks ); |
973 | |
974 | $output = ''; |
975 | $checkbox = |
976 | new OOUI\FieldLayout( |
977 | new OOUI\CheckboxInputWidget( [ |
978 | 'name' => 'wpFilterActionBlock', |
979 | 'id' => 'mw-abusefilter-action-checkbox-block', |
980 | 'selected' => $set, |
981 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
982 | 'disabled' => $readOnly |
983 | ] |
984 | ), |
985 | [ |
986 | 'label' => $this->msg( 'abusefilter-edit-action-block' )->text(), |
987 | 'align' => 'inline' |
988 | ] |
989 | ); |
990 | $output .= $checkbox; |
991 | |
992 | $suggestedBlocks = Html::listDropdownOptionsOoui( $suggestedBlocks ); |
993 | |
994 | $anonDuration = |
995 | new OOUI\DropdownInputWidget( [ |
996 | 'name' => 'wpBlockAnonDuration', |
997 | 'options' => $suggestedBlocks, |
998 | 'value' => $defaultAnonDuration, |
999 | 'disabled' => $readOnly |
1000 | ] ); |
1001 | |
1002 | $userDuration = |
1003 | new OOUI\DropdownInputWidget( [ |
1004 | 'name' => 'wpBlockUserDuration', |
1005 | 'options' => $suggestedBlocks, |
1006 | 'value' => $defaultUserDuration, |
1007 | 'disabled' => $readOnly |
1008 | ] ); |
1009 | |
1010 | $blockOptions = []; |
1011 | if ( $config->get( MainConfigNames::BlockAllowsUTEdit ) === true ) { |
1012 | $talkCheckbox = |
1013 | new OOUI\FieldLayout( |
1014 | new OOUI\CheckboxInputWidget( [ |
1015 | 'name' => 'wpFilterBlockTalk', |
1016 | 'id' => 'mw-abusefilter-action-checkbox-blocktalk', |
1017 | 'selected' => isset( $blockTalk ) && $blockTalk === 'blocktalk', |
1018 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
1019 | 'disabled' => $readOnly |
1020 | ] |
1021 | ), |
1022 | [ |
1023 | 'label' => $this->msg( 'abusefilter-edit-action-blocktalk' )->text(), |
1024 | 'align' => 'left' |
1025 | ] |
1026 | ); |
1027 | |
1028 | $blockOptions[] = $talkCheckbox; |
1029 | } |
1030 | $blockOptions[] = |
1031 | new OOUI\FieldLayout( |
1032 | $anonDuration, |
1033 | [ |
1034 | 'label' => $this->msg( 'abusefilter-edit-block-anon-durations' )->text() |
1035 | ] |
1036 | ); |
1037 | $blockOptions[] = |
1038 | new OOUI\FieldLayout( |
1039 | $userDuration, |
1040 | [ |
1041 | 'label' => $this->msg( 'abusefilter-edit-block-user-durations' )->text() |
1042 | ] |
1043 | ); |
1044 | |
1045 | $output .= Html::rawElement( |
1046 | 'div', |
1047 | [ 'id' => 'mw-abusefilter-block-parameters' ], |
1048 | new OOUI\FieldsetLayout( [ 'items' => $blockOptions ] ) |
1049 | ); |
1050 | |
1051 | return $output; |
1052 | |
1053 | default: |
1054 | // Give grep a chance to find the usages: |
1055 | // abusefilter-edit-action-disallow, |
1056 | // abusefilter-edit-action-blockautopromote, |
1057 | // abusefilter-edit-action-degroup, |
1058 | // abusefilter-edit-action-rangeblock, |
1059 | $message = 'abusefilter-edit-action-' . $action; |
1060 | $form_field = 'wpFilterAction' . ucfirst( $action ); |
1061 | $status = $set; |
1062 | |
1063 | $thisAction = |
1064 | new OOUI\FieldLayout( |
1065 | new OOUI\CheckboxInputWidget( [ |
1066 | 'name' => $form_field, |
1067 | 'id' => "mw-abusefilter-action-checkbox-$action", |
1068 | 'selected' => $status, |
1069 | 'classes' => [ 'mw-abusefilter-action-checkbox' ], |
1070 | 'disabled' => $readOnly |
1071 | ] |
1072 | ), |
1073 | [ |
1074 | 'label' => $this->msg( $message )->text(), |
1075 | 'align' => 'inline' |
1076 | ] |
1077 | ); |
1078 | return $thisAction; |
1079 | } |
1080 | } |
1081 | |
1082 | /** |
1083 | * @param string $warnMsg |
1084 | * @param bool $readOnly |
1085 | * @param string $action |
1086 | * @return \OOUI\FieldLayout |
1087 | */ |
1088 | public function getExistingSelector( $warnMsg, $readOnly = false, $action = 'warn' ) { |
1089 | if ( $action === 'warn' ) { |
1090 | $action = 'warning'; |
1091 | $formId = 'warn'; |
1092 | $inputName = 'wpFilterWarnMessage'; |
1093 | } elseif ( $action === 'disallow' ) { |
1094 | $action = 'disallowed'; |
1095 | $formId = 'disallow'; |
1096 | $inputName = 'wpFilterDisallowMessage'; |
1097 | } else { |
1098 | throw new UnexpectedValueException( "Unexpected action value $action" ); |
1099 | } |
1100 | |
1101 | $existingSelector = |
1102 | new OOUI\DropdownInputWidget( [ |
1103 | 'name' => $inputName, |
1104 | // mw-abusefilter-warn-message-existing, mw-abusefilter-disallow-message-existing |
1105 | 'id' => "mw-abusefilter-$formId-message-existing", |
1106 | // abusefilter-warning, abusefilter-disallowed |
1107 | 'value' => $warnMsg === "abusefilter-$action" ? "abusefilter-$action" : 'other', |
1108 | 'infusable' => true |
1109 | ] ); |
1110 | |
1111 | // abusefilter-warning, abusefilter-disallowed |
1112 | $options = [ "abusefilter-$action" => "abusefilter-$action" ]; |
1113 | |
1114 | if ( $readOnly ) { |
1115 | $existingSelector->setDisabled( true ); |
1116 | } else { |
1117 | // Find other messages. |
1118 | $dbr = $this->lbFactory->getReplicaDatabase(); |
1119 | $pageTitlePrefix = "Abusefilter-$action"; |
1120 | $titles = $dbr->newSelectQueryBuilder() |
1121 | ->select( 'page_title' ) |
1122 | ->from( 'page' ) |
1123 | ->where( [ |
1124 | 'page_namespace' => 8, |
1125 | $dbr->expr( 'page_title', IExpression::LIKE, new LikeValue( $pageTitlePrefix, $dbr->anyString() ) ) |
1126 | ] ) |
1127 | ->caller( __METHOD__ ) |
1128 | ->fetchFieldValues(); |
1129 | |
1130 | $lang = $this->getLanguage(); |
1131 | foreach ( $titles as $title ) { |
1132 | if ( $lang->lcfirst( $title ) === $lang->lcfirst( $warnMsg ) ) { |
1133 | $existingSelector->setValue( $lang->lcfirst( $warnMsg ) ); |
1134 | } |
1135 | |
1136 | if ( $title !== "Abusefilter-$action" ) { |
1137 | $options[ $lang->lcfirst( $title ) ] = $lang->lcfirst( $title ); |
1138 | } |
1139 | } |
1140 | } |
1141 | |
1142 | // abusefilter-edit-warn-other, abusefilter-edit-disallow-other |
1143 | $options[ $this->msg( "abusefilter-edit-$formId-other" )->text() ] = 'other'; |
1144 | |
1145 | $options = Html::listDropdownOptionsOoui( $options ); |
1146 | $existingSelector->setOptions( $options ); |
1147 | |
1148 | $existingSelector = |
1149 | new OOUI\FieldLayout( |
1150 | $existingSelector, |
1151 | [ |
1152 | // abusefilter-edit-warn-message, abusefilter-edit-disallow-message |
1153 | 'label' => $this->msg( "abusefilter-edit-$formId-message" )->text() |
1154 | ] |
1155 | ); |
1156 | |
1157 | return $existingSelector; |
1158 | } |
1159 | |
1160 | /** |
1161 | * @todo Maybe we should also check if global values belong to $durations |
1162 | * and determine the right point to add them if missing. |
1163 | * |
1164 | * @param string[] $durations |
1165 | * @return string[] |
1166 | */ |
1167 | private static function normalizeBlocks( array $durations ) { |
1168 | global $wgAbuseFilterBlockDuration, $wgAbuseFilterAnonBlockDuration; |
1169 | // We need to have same values since it may happen that ipblocklist |
1170 | // and one (or both) of the global variables use different wording |
1171 | // for the same duration. In such case, when setting the default of |
1172 | // the dropdowns it would fail. |
1173 | $anonDuration = self::getAbsoluteBlockDuration( $wgAbuseFilterAnonBlockDuration ?? |
1174 | $wgAbuseFilterBlockDuration ); |
1175 | $userDuration = self::getAbsoluteBlockDuration( $wgAbuseFilterBlockDuration ); |
1176 | foreach ( $durations as &$duration ) { |
1177 | $currentDuration = self::getAbsoluteBlockDuration( $duration ); |
1178 | |
1179 | if ( $duration !== $wgAbuseFilterBlockDuration && |
1180 | $currentDuration === $userDuration ) { |
1181 | $duration = $wgAbuseFilterBlockDuration; |
1182 | |
1183 | } elseif ( $duration !== $wgAbuseFilterAnonBlockDuration && |
1184 | $currentDuration === $anonDuration ) { |
1185 | $duration = $wgAbuseFilterAnonBlockDuration; |
1186 | } |
1187 | } |
1188 | |
1189 | return $durations; |
1190 | } |
1191 | |
1192 | /** |
1193 | * Converts a string duration to an absolute timestamp, i.e. unrelated to the current |
1194 | * time, taking into account infinity durations as well. The second parameter of |
1195 | * strtotime is set to 0 in order to convert the duration in seconds (instead of |
1196 | * a timestamp), thus making it unaffected by the execution time of the code. |
1197 | * |
1198 | * @param string $duration |
1199 | * @return string|int |
1200 | */ |
1201 | private static function getAbsoluteBlockDuration( $duration ) { |
1202 | if ( wfIsInfinity( $duration ) ) { |
1203 | return 'infinity'; |
1204 | } |
1205 | return strtotime( $duration, 0 ); |
1206 | } |
1207 | |
1208 | /** |
1209 | * Loads filter data from the database by ID. |
1210 | * @param int|null $id The filter's ID number, or null for a new filter |
1211 | * @return Filter |
1212 | * @throws FilterNotFoundException |
1213 | */ |
1214 | private function loadFilterData( ?int $id ): Filter { |
1215 | if ( $id === null ) { |
1216 | return MutableFilter::newDefault(); |
1217 | } |
1218 | |
1219 | $flags = $this->getRequest()->wasPosted() |
1220 | // Load from primary database to avoid unintended reversions where there's replication lag. |
1221 | ? IDBAccessObject::READ_LATEST |
1222 | : IDBAccessObject::READ_NORMAL; |
1223 | |
1224 | return $this->filterLookup->getFilter( $id, false, $flags ); |
1225 | } |
1226 | |
1227 | /** |
1228 | * Load filter data to show in the edit view from the DB. |
1229 | * @param int|null $filter The filter ID being requested or null for a new filter |
1230 | * @param int|null $history_id If any, the history ID being requested. |
1231 | * @return Filter|null Null if the filter does not exist. |
1232 | */ |
1233 | private function loadFromDatabase( ?int $filter, $history_id = null ): ?Filter { |
1234 | if ( $history_id ) { |
1235 | try { |
1236 | return $this->filterLookup->getFilterVersion( $history_id ); |
1237 | } catch ( FilterVersionNotFoundException $_ ) { |
1238 | return null; |
1239 | } |
1240 | } else { |
1241 | return $this->loadFilterData( $filter ); |
1242 | } |
1243 | } |
1244 | |
1245 | /** |
1246 | * Load data from the HTTP request. Used for saving the filter, and when the token doesn't match |
1247 | * @param int|null $filter |
1248 | * @return Filter[] |
1249 | */ |
1250 | private function loadRequest( ?int $filter ): array { |
1251 | $request = $this->getRequest(); |
1252 | if ( !$request->wasPosted() ) { |
1253 | // Sanity |
1254 | throw new LogicException( __METHOD__ . ' called without the request being POSTed.' ); |
1255 | } |
1256 | |
1257 | $origFilter = $this->loadFilterData( $filter ); |
1258 | |
1259 | /** @var MutableFilter $newFilter */ |
1260 | $newFilter = $origFilter instanceof MutableFilter |
1261 | ? clone $origFilter |
1262 | : MutableFilter::newFromParentFilter( $origFilter ); |
1263 | |
1264 | if ( $filter !== null ) { |
1265 | // Unchangeable values |
1266 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
1267 | $newFilter->setThrottled( $origFilter->isThrottled() ); |
1268 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
1269 | $newFilter->setHitCount( $origFilter->getHitCount() ); |
1270 | // These are needed if the save fails and the filter is not new |
1271 | $newFilter->setID( $origFilter->getID() ); |
1272 | $newFilter->setUserID( $origFilter->getUserID() ); |
1273 | $newFilter->setUserName( $origFilter->getUserName() ); |
1274 | $newFilter->setTimestamp( $origFilter->getTimestamp() ); |
1275 | } |
1276 | |
1277 | $newFilter->setName( trim( $request->getVal( 'wpFilterDescription' ) ) ); |
1278 | $newFilter->setRules( trim( $request->getVal( 'wpFilterRules' ) ) ); |
1279 | $newFilter->setComments( trim( $request->getVal( 'wpFilterNotes' ) ) ); |
1280 | |
1281 | $newFilter->setGroup( $request->getVal( 'wpFilterGroup', 'default' ) ); |
1282 | |
1283 | $newFilter->setDeleted( $request->getCheck( 'wpFilterDeleted' ) ); |
1284 | $newFilter->setEnabled( $request->getCheck( 'wpFilterEnabled' ) ); |
1285 | $newFilter->setHidden( $request->getCheck( 'wpFilterHidden' ) ); |
1286 | $newFilter->setProtected( $request->getCheck( 'wpFilterProtected' ) ); |
1287 | $newFilter->setGlobal( $request->getCheck( 'wpFilterGlobal' ) |
1288 | && $this->getConfig()->get( 'AbuseFilterIsCentral' ) ); |
1289 | |
1290 | $actions = $this->loadActions(); |
1291 | |
1292 | $newFilter->setActions( $actions ); |
1293 | |
1294 | return [ $newFilter, $origFilter ]; |
1295 | } |
1296 | |
1297 | /** |
1298 | * @return Filter|null |
1299 | */ |
1300 | private function loadImportRequest(): ?Filter { |
1301 | $request = $this->getRequest(); |
1302 | if ( !$request->wasPosted() ) { |
1303 | // Sanity |
1304 | throw new LogicException( __METHOD__ . ' called without the request being POSTed.' ); |
1305 | } |
1306 | |
1307 | try { |
1308 | $filter = $this->filterImporter->decodeData( $request->getVal( 'wpImportText' ) ); |
1309 | } catch ( InvalidImportDataException $_ ) { |
1310 | return null; |
1311 | } |
1312 | |
1313 | return $filter; |
1314 | } |
1315 | |
1316 | /** |
1317 | * @return array[] |
1318 | */ |
1319 | private function loadActions(): array { |
1320 | $request = $this->getRequest(); |
1321 | $allActions = $this->consequencesRegistry->getAllEnabledActionNames(); |
1322 | $actions = []; |
1323 | foreach ( $allActions as $action ) { |
1324 | // Check if it's set |
1325 | $enabled = $request->getCheck( 'wpFilterAction' . ucfirst( $action ) ); |
1326 | |
1327 | if ( $enabled ) { |
1328 | $parameters = []; |
1329 | |
1330 | if ( $action === 'throttle' ) { |
1331 | // We need to load the parameters |
1332 | $throttleCount = $request->getIntOrNull( 'wpFilterThrottleCount' ); |
1333 | $throttlePeriod = $request->getIntOrNull( 'wpFilterThrottlePeriod' ); |
1334 | // First explode with \n, which is the delimiter used in the textarea |
1335 | $rawGroups = explode( "\n", $request->getText( 'wpFilterThrottleGroups' ) ); |
1336 | // Trim any space, both as an actual group and inside subgroups |
1337 | $throttleGroups = []; |
1338 | foreach ( $rawGroups as $group ) { |
1339 | if ( strpos( $group, ',' ) !== false ) { |
1340 | $subGroups = explode( ',', $group ); |
1341 | $throttleGroups[] = implode( ',', array_map( 'trim', $subGroups ) ); |
1342 | } elseif ( trim( $group ) !== '' ) { |
1343 | $throttleGroups[] = trim( $group ); |
1344 | } |
1345 | } |
1346 | |
1347 | $parameters[0] = $this->filter; |
1348 | $parameters[1] = "$throttleCount,$throttlePeriod"; |
1349 | $parameters = array_merge( $parameters, $throttleGroups ); |
1350 | } elseif ( $action === 'warn' ) { |
1351 | $specMsg = $request->getVal( 'wpFilterWarnMessage' ); |
1352 | |
1353 | if ( $specMsg === 'other' ) { |
1354 | $specMsg = $request->getVal( 'wpFilterWarnMessageOther' ); |
1355 | } |
1356 | |
1357 | $parameters[0] = $specMsg; |
1358 | } elseif ( $action === 'block' ) { |
1359 | // TODO: Should save a boolean |
1360 | $parameters[0] = $request->getCheck( 'wpFilterBlockTalk' ) ? |
1361 | 'blocktalk' : 'noTalkBlockSet'; |
1362 | $parameters[1] = $request->getVal( 'wpBlockAnonDuration' ); |
1363 | $parameters[2] = $request->getVal( 'wpBlockUserDuration' ); |
1364 | } elseif ( $action === 'disallow' ) { |
1365 | $specMsg = $request->getVal( 'wpFilterDisallowMessage' ); |
1366 | |
1367 | if ( $specMsg === 'other' ) { |
1368 | $specMsg = $request->getVal( 'wpFilterDisallowMessageOther' ); |
1369 | } |
1370 | |
1371 | $parameters[0] = $specMsg; |
1372 | } elseif ( $action === 'tag' ) { |
1373 | $parameters = explode( ',', trim( $request->getText( 'wpFilterTags' ) ) ); |
1374 | if ( $parameters === [ '' ] ) { |
1375 | // Since it's not possible to manually add an empty tag, this only happens |
1376 | // if the form is submitted without touching the tag input field. |
1377 | // We pass an empty array so that the widget won't show an empty tag in the topbar |
1378 | $parameters = []; |
1379 | } |
1380 | } |
1381 | |
1382 | $actions[$action] = $parameters; |
1383 | } |
1384 | } |
1385 | return $actions; |
1386 | } |
1387 | |
1388 | /** |
1389 | * Exports the default warning and disallow messages to a JS variable |
1390 | */ |
1391 | private function exposeMessages() { |
1392 | $this->getOutput()->addJsConfigVars( |
1393 | 'wgAbuseFilterDefaultWarningMessage', |
1394 | $this->getConfig()->get( 'AbuseFilterDefaultWarningMessage' ) |
1395 | ); |
1396 | $this->getOutput()->addJsConfigVars( |
1397 | 'wgAbuseFilterDefaultDisallowMessage', |
1398 | $this->getConfig()->get( 'AbuseFilterDefaultDisallowMessage' ) |
1399 | ); |
1400 | } |
1401 | } |