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