Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
1.50% |
13 / 867 |
|
6.25% |
1 / 16 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewEdit | |
1.50% |
13 / 867 |
|
6.25% |
1 / 16 |
21081.40 | |
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 / 30 |
|
0.00% |
0 / 1 |
42 | |||
showUnrecoverableError | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
buildFilterEditor | |
0.00% |
0 / 256 |
|
0.00% |
0 / 1 |
2070 | |||
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 / 27 |
|
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 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\Permissions\PermissionManager; |
26 | use MediaWiki\SpecialPage\SpecialPage; |
27 | use MediaWiki\Xml\Xml; |
28 | use OOUI; |
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 PermissionManager */ |
48 | private $permissionManager; |
49 | |
50 | /** @var FilterProfiler */ |
51 | private $filterProfiler; |
52 | |
53 | /** @var FilterLookup */ |
54 | private $filterLookup; |
55 | |
56 | /** @var FilterImporter */ |
57 | private $filterImporter; |
58 | |
59 | /** @var FilterStore */ |
60 | private $filterStore; |
61 | |
62 | /** @var EditBoxBuilderFactory */ |
63 | private $boxBuilderFactory; |
64 | |
65 | /** @var ConsequencesRegistry */ |
66 | private $consequencesRegistry; |
67 | |
68 | /** @var SpecsFormatter */ |
69 | private $specsFormatter; |
70 | |
71 | /** |
72 | * @param LBFactory $lbFactory |
73 | * @param PermissionManager $permissionManager |
74 | * @param AbuseFilterPermissionManager $afPermManager |
75 | * @param FilterProfiler $filterProfiler |
76 | * @param FilterLookup $filterLookup |
77 | * @param FilterImporter $filterImporter |
78 | * @param FilterStore $filterStore |
79 | * @param EditBoxBuilderFactory $boxBuilderFactory |
80 | * @param ConsequencesRegistry $consequencesRegistry |
81 | * @param SpecsFormatter $specsFormatter |
82 | * @param IContextSource $context |
83 | * @param LinkRenderer $linkRenderer |
84 | * @param string $basePageName |
85 | * @param array $params |
86 | */ |
87 | public function __construct( |
88 | LBFactory $lbFactory, |
89 | PermissionManager $permissionManager, |
90 | AbuseFilterPermissionManager $afPermManager, |
91 | FilterProfiler $filterProfiler, |
92 | FilterLookup $filterLookup, |
93 | FilterImporter $filterImporter, |
94 | FilterStore $filterStore, |
95 | EditBoxBuilderFactory $boxBuilderFactory, |
96 | ConsequencesRegistry $consequencesRegistry, |
97 | SpecsFormatter $specsFormatter, |
98 | IContextSource $context, |
99 | LinkRenderer $linkRenderer, |
100 | string $basePageName, |
101 | array $params |
102 | ) { |
103 | parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params ); |
104 | $this->lbFactory = $lbFactory; |
105 | $this->permissionManager = $permissionManager; |
106 | $this->filterProfiler = $filterProfiler; |
107 | $this->filterLookup = $filterLookup; |
108 | $this->filterImporter = $filterImporter; |
109 | $this->filterStore = $filterStore; |
110 | $this->boxBuilderFactory = $boxBuilderFactory; |
111 | $this->consequencesRegistry = $consequencesRegistry; |
112 | $this->specsFormatter = $specsFormatter; |
113 | $this->specsFormatter->setMessageLocalizer( $this->getContext() ); |
114 | $this->filter = $this->mParams['filter']; |
115 | $this->historyID = $this->mParams['history'] ?? null; |
116 | } |
117 | |
118 | /** |
119 | * Shows the page |
120 | */ |
121 | public function show() { |
122 | $out = $this->getOutput(); |
123 | $out->enableOOUI(); |
124 | $request = $this->getRequest(); |
125 | $out->setPageTitleMsg( $this->msg( 'abusefilter-edit' ) ); |
126 | $out->addHelpLink( 'Extension:AbuseFilter/Rules format' ); |
127 | |
128 | if ( !is_numeric( $this->filter ) && $this->filter !== null ) { |
129 | $this->showUnrecoverableError( 'abusefilter-edit-badfilter' ); |
130 | return; |
131 | } |
132 | $filter = $this->filter ? (int)$this->filter : null; |
133 | $history_id = $this->historyID; |
134 | if ( $this->historyID ) { |
135 | $dbr = $this->lbFactory->getReplicaDatabase(); |
136 | $lastID = (int)$dbr->newSelectQueryBuilder() |
137 | ->select( 'afh_id' ) |
138 | ->from( 'abuse_filter_history' ) |
139 | ->where( [ |
140 | 'afh_filter' => $filter, |
141 | ] ) |
142 | ->orderBy( 'afh_id', SelectQueryBuilder::SORT_DESC ) |
143 | ->caller( __METHOD__ ) |
144 | ->fetchField(); |
145 | // change $history_id to null if it's current version id |
146 | if ( $lastID === $this->historyID ) { |
147 | $history_id = null; |
148 | } |
149 | } |
150 | |
151 | // Add the default warning and disallow messages in a JS variable |
152 | $this->exposeMessages(); |
153 | |
154 | $canEdit = $this->afPermManager->canEdit( $this->getAuthority() ); |
155 | |
156 | if ( $filter === null && !$canEdit ) { |
157 | // Special case: Special:AbuseFilter/new is certainly not usable if the user cannot edit |
158 | $this->showUnrecoverableError( 'abusefilter-edit-notallowed' ); |
159 | return; |
160 | } |
161 | |
162 | $isImport = $request->wasPosted() && $request->getRawVal( 'wpImportText' ) !== null; |
163 | |
164 | if ( !$isImport && $request->wasPosted() && $canEdit ) { |
165 | $this->attemptSave( $filter, $history_id ); |
166 | return; |
167 | } |
168 | |
169 | if ( $isImport ) { |
170 | $filterObj = $this->loadImportRequest(); |
171 | if ( $filterObj === null ) { |
172 | $this->showUnrecoverableError( 'abusefilter-import-invalid-data' ); |
173 | return; |
174 | } |
175 | } else { |
176 | // The request wasn't posted (i.e. just viewing the filter) or the user cannot edit |
177 | try { |
178 | $filterObj = $this->loadFromDatabase( $filter, $history_id ); |
179 | } catch ( FilterNotFoundException $_ ) { |
180 | $filterObj = null; |
181 | } |
182 | if ( $filterObj === null || ( $history_id && (int)$filterObj->getID() !== $filter ) ) { |
183 | $this->showUnrecoverableError( 'abusefilter-edit-badfilter' ); |
184 | return; |
185 | } |
186 | } |
187 | |
188 | $this->buildFilterEditor( null, $filterObj, $filter, $history_id ); |
189 | } |
190 | |
191 | /** |
192 | * @param int|null $filter The filter ID or null for a new filter |
193 | * @param int|null $history_id The history ID of the filter, if applicable. Otherwise null |
194 | */ |
195 | private function attemptSave( ?int $filter, $history_id ): void { |
196 | $out = $this->getOutput(); |
197 | $request = $this->getRequest(); |
198 | $user = $this->getUser(); |
199 | |
200 | [ $newFilter, $origFilter ] = $this->loadRequest( $filter ); |
201 | |
202 | $tokenFilter = $filter === null ? 'new' : (string)$filter; |
203 | $editToken = $request->getVal( 'wpEditToken' ); |
204 | $tokenMatches = $this->getCsrfTokenSet()->matchToken( $editToken, [ 'abusefilter', $tokenFilter ] ); |
205 | |
206 | if ( !$tokenMatches ) { |
207 | // Token invalid or expired while the page was open, warn to retry |
208 | $error = Html::warningBox( $this->msg( 'abusefilter-edit-token-not-match' )->parseAsBlock() ); |
209 | $this->buildFilterEditor( $error, $newFilter, $filter, $history_id ); |
210 | return; |
211 | } |
212 | |
213 | $status = $this->filterStore->saveFilter( $user, $filter, $newFilter, $origFilter ); |
214 | |
215 | if ( !$status->isGood() ) { |
216 | $msg = $status->getMessages()[0]; |
217 | if ( $status->isOK() ) { |
218 | // Fixable error, show the editing interface |
219 | $error = Html::errorBox( $this->msg( $msg )->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|\Wikimedia\Message\MessageSpecifier $msg |
244 | */ |
245 | private function showUnrecoverableError( $msg ): void { |
246 | $out = $this->getOutput(); |
247 | |
248 | $out->addHTML( Html::errorBox( $this->msg( $msg )->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 | // Grab the current hidden flag from the DB, in case we're editing an older, public revision of a filter that is |
291 | // currently hidden, so that we can also hide that public revision. |
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 | // Filters that use protected variables should always be hidden from public view |
302 | if ( |
303 | ( |
304 | $filterObj->isProtected() || |
305 | ( $filter !== null && $this->filterLookup->getFilter( $filter, false )->isProtected() ) |
306 | ) && |
307 | !$this->afPermManager->canViewProtectedVariables( $user ) |
308 | ) { |
309 | $out->addHTML( $this->msg( 'abusefilter-edit-denied-protected-vars' )->escaped() ); |
310 | return; |
311 | } |
312 | |
313 | if ( $isCreatingNewFilter ) { |
314 | $title = $this->msg( 'abusefilter-add' ); |
315 | } elseif ( $this->afPermManager->canEditFilter( $user, $filterObj ) ) { |
316 | $title = $this->msg( 'abusefilter-edit-specific' ) |
317 | ->numParams( $this->filter ) |
318 | ->params( $filterObj->getName() ); |
319 | } else { |
320 | $title = $this->msg( 'abusefilter-view-specific' ) |
321 | ->numParams( $this->filter ) |
322 | ->params( $filterObj->getName() ); |
323 | } |
324 | $out->setPageTitleMsg( $title ); |
325 | |
326 | $readOnly = !$this->afPermManager->canEditFilter( $user, $filterObj ); |
327 | |
328 | if ( $history_id ) { |
329 | $oldWarningMessage = $readOnly |
330 | ? 'abusefilter-edit-oldwarning-view' |
331 | : 'abusefilter-edit-oldwarning'; |
332 | $out->addWikiMsg( $oldWarningMessage, $history_id, $filter ); |
333 | } |
334 | |
335 | if ( $error !== null ) { |
336 | $out->addHTML( $error ); |
337 | } |
338 | |
339 | $fields = []; |
340 | |
341 | $fields['abusefilter-edit-id'] = |
342 | $isCreatingNewFilter ? |
343 | $this->msg( 'abusefilter-edit-new' )->escaped() : |
344 | htmlspecialchars( $lang->formatNum( (string)$filter ) ); |
345 | $fields['abusefilter-edit-description'] = |
346 | new OOUI\TextInputWidget( [ |
347 | 'name' => 'wpFilterDescription', |
348 | 'id' => 'mw-abusefilter-edit-description-input', |
349 | 'value' => $filterObj->getName(), |
350 | 'readOnly' => $readOnly |
351 | ] |
352 | ); |
353 | |
354 | $validGroups = $this->getConfig()->get( 'AbuseFilterValidGroups' ); |
355 | if ( count( $validGroups ) > 1 ) { |
356 | $groupSelector = |
357 | new OOUI\DropdownInputWidget( [ |
358 | 'name' => 'wpFilterGroup', |
359 | 'id' => 'mw-abusefilter-edit-group-input', |
360 | 'value' => $filterObj->getGroup(), |
361 | 'disabled' => $readOnly |
362 | ] ); |
363 | |
364 | $options = []; |
365 | foreach ( $validGroups as $group ) { |
366 | $options += [ $this->specsFormatter->nameGroup( $group ) => $group ]; |
367 | } |
368 | |
369 | $options = Html::listDropdownOptionsOoui( $options ); |
370 | $groupSelector->setOptions( $options ); |
371 | |
372 | $fields['abusefilter-edit-group'] = $groupSelector; |
373 | } |
374 | |
375 | // Hit count display |
376 | $hitCount = $filterObj->getHitCount(); |
377 | if ( $hitCount !== null && $this->afPermManager->canSeeLogDetails( $user ) ) { |
378 | $count_display = $this->msg( 'abusefilter-hitcount' ) |
379 | ->numParams( $hitCount )->text(); |
380 | $hitCount = $this->linkRenderer->makeKnownLink( |
381 | SpecialPage::getTitleFor( 'AbuseLog' ), |
382 | $count_display, |
383 | [], |
384 | [ 'wpSearchFilter' => $filterObj->getID() ] |
385 | ); |
386 | |
387 | $fields['abusefilter-edit-hitcount'] = $hitCount; |
388 | } |
389 | |
390 | if ( $filter !== null && $filterObj->isEnabled() ) { |
391 | // Statistics |
392 | [ |
393 | 'count' => $totalCount, |
394 | 'matches' => $matchesCount, |
395 | 'total-time' => $curTotalTime, |
396 | 'total-cond' => $curTotalConds, |
397 | ] = $this->filterProfiler->getFilterProfile( $filter ); |
398 | |
399 | if ( $totalCount > 0 ) { |
400 | $matchesPercent = round( 100 * $matchesCount / $totalCount, 2 ); |
401 | $avgTime = round( $curTotalTime / $totalCount, 2 ); |
402 | $avgCond = round( $curTotalConds / $totalCount, 1 ); |
403 | |
404 | $fields['abusefilter-edit-status-label'] = $this->msg( 'abusefilter-edit-status' ) |
405 | ->numParams( $totalCount, $matchesCount, $matchesPercent, $avgTime, $avgCond ) |
406 | ->parse(); |
407 | } |
408 | } |
409 | |
410 | $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder( $this, $user, $out ); |
411 | |
412 | $fields['abusefilter-edit-rules'] = $boxBuilder->buildEditBox( |
413 | $filterObj->getRules(), |
414 | true |
415 | ); |
416 | $fields['abusefilter-edit-notes'] = |
417 | new OOUI\MultilineTextInputWidget( [ |
418 | 'name' => 'wpFilterNotes', |
419 | 'value' => $filterObj->getComments() . "\n", |
420 | 'rows' => 15, |
421 | 'readOnly' => $readOnly, |
422 | 'id' => 'mw-abusefilter-notes-editor' |
423 | ] ); |
424 | |
425 | // Build checkboxes |
426 | $checkboxes = [ 'hidden', 'enabled', 'protected', 'deleted' ]; |
427 | $flags = ''; |
428 | |
429 | if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) { |
430 | $checkboxes[] = 'global'; |
431 | } |
432 | |
433 | if ( $filterObj->isThrottled() ) { |
434 | $throttledActionNames = array_intersect( |
435 | $filterObj->getActionsNames(), |
436 | $this->consequencesRegistry->getDangerousActionNames() |
437 | ); |
438 | |
439 | if ( $throttledActionNames ) { |
440 | $throttledActionsLocalized = []; |
441 | foreach ( $throttledActionNames as $actionName ) { |
442 | $throttledActionsLocalized[] = $this->specsFormatter->getActionMessage( $actionName )->text(); |
443 | } |
444 | |
445 | $throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning' ) |
446 | ->plaintextParams( $lang->commaList( $throttledActionsLocalized ) ) |
447 | ->params( count( $throttledActionsLocalized ) ) |
448 | ->parseAsBlock(); |
449 | } else { |
450 | $throttledMsg = $this->msg( 'abusefilter-edit-throttled-warning-no-actions' ) |
451 | ->parseAsBlock(); |
452 | } |
453 | $flags .= Html::warningBox( $throttledMsg ); |
454 | } |
455 | |
456 | foreach ( $checkboxes as $checkboxId ) { |
457 | // Messages that can be used here: |
458 | // * abusefilter-edit-enabled |
459 | // * abusefilter-edit-deleted |
460 | // * abusefilter-edit-hidden |
461 | // * abusefilter-edit-protected |
462 | // * abusefilter-edit-global |
463 | $message = "abusefilter-edit-$checkboxId"; |
464 | // isEnabled(), isDeleted(), isHidden(), isProtected(), isGlobal() |
465 | $method = 'is' . ucfirst( $checkboxId ); |
466 | // wpFilterEnabled, wpFilterDeleted, wpFilterHidden, wpFilterProtected, wpFilterGlobal |
467 | $postVar = 'wpFilter' . ucfirst( $checkboxId ); |
468 | |
469 | $checkboxAttribs = [ |
470 | 'name' => $postVar, |
471 | 'id' => $postVar, |
472 | 'selected' => $filterObj->$method(), |
473 | 'disabled' => $readOnly |
474 | ]; |
475 | $labelAttribs = [ |
476 | 'label' => $this->msg( $message )->text(), |
477 | 'align' => 'inline', |
478 | ]; |
479 | |
480 | if ( $checkboxId === 'global' && !$this->afPermManager->canEditGlobal( $user ) ) { |
481 | $checkboxAttribs['disabled'] = 'disabled'; |
482 | } |
483 | |
484 | if ( $checkboxId == 'protected' ) { |
485 | if ( !$this->afPermManager->canViewProtectedVariables( $user ) ) { |
486 | $checkboxAttribs['classes'] = [ 'oo-ui-element-hidden' ]; |
487 | $labelAttribs['classes'] = [ 'oo-ui-element-hidden' ]; |
488 | } elseif ( $filterObj->isProtected() ) { |
489 | $checkboxAttribs['disabled'] = true; |
490 | $labelAttribs['label'] = $this->msg( |
491 | 'abusefilter-edit-protected-variable-already-protected' |
492 | )->text(); |
493 | } else { |
494 | $labelAttribs['label'] = new OOUI\HtmlSnippet( |
495 | $this->msg( $message )->parse() |
496 | ); |
497 | $labelAttribs['help'] = $this->msg( 'abusefilter-edit-protected-help-message' )->text(); |
498 | $labelAttribs['helpInline'] = true; |
499 | } |
500 | } |
501 | |
502 | // Set readonly on deleted if the filter isn't disabled |
503 | if ( $checkboxId === 'deleted' && $filterObj->isEnabled() ) { |
504 | $checkboxAttribs['disabled'] = 'disabled'; |
505 | } |
506 | |
507 | // Add infusable where needed |
508 | if ( $checkboxId === 'deleted' || $checkboxId === 'enabled' ) { |
509 | $checkboxAttribs['infusable'] = true; |
510 | if ( $checkboxId === 'deleted' ) { |
511 | // wpFilterDeletedLabel |
512 | $labelAttribs['id'] = $postVar . 'Label'; |
513 | $labelAttribs['infusable'] = true; |
514 | } |
515 | } |
516 | |
517 | $checkbox = |
518 | new OOUI\FieldLayout( |
519 | new OOUI\CheckboxInputWidget( $checkboxAttribs ), |
520 | $labelAttribs |
521 | ); |
522 | $flags .= $checkbox; |
523 | } |
524 | |
525 | $fields['abusefilter-edit-flags'] = $flags; |
526 | |
527 | if ( $filter !== null ) { |
528 | $tools = ''; |
529 | if ( $this->afPermManager->canRevertFilterActions( $user ) ) { |
530 | $tools .= Html::rawElement( |
531 | 'p', [], |
532 | $this->linkRenderer->makeLink( |
533 | $this->getTitle( "revert/$filter" ), |
534 | new HtmlArmor( $this->msg( 'abusefilter-edit-revert' )->parse() ) |
535 | ) |
536 | ); |
537 | } |
538 | |
539 | if ( $this->afPermManager->canUseTestTools( $user ) ) { |
540 | // Test link |
541 | $tools .= Html::rawElement( |
542 | 'p', [], |
543 | $this->linkRenderer->makeLink( |
544 | $this->getTitle( "test/$filter" ), |
545 | new HtmlArmor( $this->msg( 'abusefilter-edit-test-link' )->parse() ) |
546 | ) |
547 | ); |
548 | } |
549 | // Last modification details |
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 | $user = $this->getUser(); |
669 | $actions = $this->consequencesRegistry->getAllEnabledActionNames(); |
670 | if ( !in_array( $action, $actions, true ) ) { |
671 | return ''; |
672 | } |
673 | |
674 | $readOnly = !$this->afPermManager->canEditFilter( $user, $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 ( $this->permissionManager->userHasRight( $user, '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 | } |