Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
3.85% |
8 / 208 |
|
12.50% |
1 / 8 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewRevert | |
3.85% |
8 / 208 |
|
12.50% |
1 / 8 |
776.65 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
show | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
30 | |||
showRevertableActions | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
20 | |||
doLookup | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
42 | |||
loadParameters | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
attemptRevert | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
getConsequence | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
revertAction | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\View; |
4 | |
5 | use HTMLForm; |
6 | use IContextSource; |
7 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
8 | use MediaWiki\Extension\AbuseFilter\ActionSpecifier; |
9 | use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\ReversibleConsequence; |
10 | use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesFactory; |
11 | use MediaWiki\Extension\AbuseFilter\Consequences\Parameters; |
12 | use MediaWiki\Extension\AbuseFilter\FilterLookup; |
13 | use MediaWiki\Extension\AbuseFilter\SpecsFormatter; |
14 | use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException; |
15 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore; |
16 | use MediaWiki\Html\Html; |
17 | use MediaWiki\Linker\Linker; |
18 | use MediaWiki\Linker\LinkRenderer; |
19 | use MediaWiki\SpecialPage\SpecialPage; |
20 | use MediaWiki\Title\TitleValue; |
21 | use MediaWiki\User\UserFactory; |
22 | use Message; |
23 | use PermissionsError; |
24 | use UnexpectedValueException; |
25 | use UserBlockedError; |
26 | use Wikimedia\Rdbms\LBFactory; |
27 | use Xml; |
28 | |
29 | class AbuseFilterViewRevert extends AbuseFilterView { |
30 | /** @var int */ |
31 | private $filter; |
32 | /** |
33 | * @var string|null The start time of the lookup period |
34 | */ |
35 | private $periodStart; |
36 | /** |
37 | * @var string|null The end time of the lookup period |
38 | */ |
39 | private $periodEnd; |
40 | /** |
41 | * @var string|null The reason provided for the revert |
42 | */ |
43 | private $reason; |
44 | /** |
45 | * @var LBFactory |
46 | */ |
47 | private $lbFactory; |
48 | /** |
49 | * @var UserFactory |
50 | */ |
51 | private $userFactory; |
52 | /** |
53 | * @var FilterLookup |
54 | */ |
55 | private $filterLookup; |
56 | /** |
57 | * @var ConsequencesFactory |
58 | */ |
59 | private $consequencesFactory; |
60 | /** |
61 | * @var VariablesBlobStore |
62 | */ |
63 | private $varBlobStore; |
64 | /** |
65 | * @var SpecsFormatter |
66 | */ |
67 | private $specsFormatter; |
68 | |
69 | /** |
70 | * @param LBFactory $lbFactory |
71 | * @param UserFactory $userFactory |
72 | * @param AbuseFilterPermissionManager $afPermManager |
73 | * @param FilterLookup $filterLookup |
74 | * @param ConsequencesFactory $consequencesFactory |
75 | * @param VariablesBlobStore $varBlobStore |
76 | * @param SpecsFormatter $specsFormatter |
77 | * @param IContextSource $context |
78 | * @param LinkRenderer $linkRenderer |
79 | * @param string $basePageName |
80 | * @param array $params |
81 | */ |
82 | public function __construct( |
83 | LBFactory $lbFactory, |
84 | UserFactory $userFactory, |
85 | AbuseFilterPermissionManager $afPermManager, |
86 | FilterLookup $filterLookup, |
87 | ConsequencesFactory $consequencesFactory, |
88 | VariablesBlobStore $varBlobStore, |
89 | SpecsFormatter $specsFormatter, |
90 | IContextSource $context, |
91 | LinkRenderer $linkRenderer, |
92 | string $basePageName, |
93 | array $params |
94 | ) { |
95 | parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params ); |
96 | $this->lbFactory = $lbFactory; |
97 | $this->userFactory = $userFactory; |
98 | $this->filterLookup = $filterLookup; |
99 | $this->consequencesFactory = $consequencesFactory; |
100 | $this->varBlobStore = $varBlobStore; |
101 | $this->specsFormatter = $specsFormatter; |
102 | $this->specsFormatter->setMessageLocalizer( $this->getContext() ); |
103 | } |
104 | |
105 | /** |
106 | * Shows the page |
107 | */ |
108 | public function show() { |
109 | $lang = $this->getLanguage(); |
110 | |
111 | $performer = $this->getAuthority(); |
112 | $out = $this->getOutput(); |
113 | |
114 | if ( !$this->afPermManager->canRevertFilterActions( $performer ) ) { |
115 | throw new PermissionsError( 'abusefilter-revert' ); |
116 | } |
117 | |
118 | $block = $performer->getBlock(); |
119 | if ( $block && $block->isSitewide() ) { |
120 | throw new UserBlockedError( $block ); |
121 | } |
122 | |
123 | $this->loadParameters(); |
124 | |
125 | if ( $this->attemptRevert() ) { |
126 | return; |
127 | } |
128 | |
129 | $filter = $this->filter; |
130 | |
131 | $out->addWikiMsg( 'abusefilter-revert-intro', Message::numParam( $filter ) ); |
132 | // Parse wikitext in this message to allow formatting of numero signs (T343994#9209383) |
133 | $out->setPageTitle( $this->msg( 'abusefilter-revert-title' )->numParams( $filter )->parse() ); |
134 | |
135 | // First, the search form. Limit dates to avoid huge queries |
136 | $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); |
137 | $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge ); |
138 | $max = wfTimestampNow(); |
139 | $filterLink = |
140 | $this->linkRenderer->makeLink( |
141 | $this->getTitle( $filter ), |
142 | $lang->formatNum( $filter ) |
143 | ); |
144 | $searchFields = []; |
145 | $searchFields['filterid'] = [ |
146 | 'type' => 'info', |
147 | 'default' => $filterLink, |
148 | 'raw' => true, |
149 | 'label-message' => 'abusefilter-revert-filter' |
150 | ]; |
151 | $searchFields['PeriodStart'] = [ |
152 | 'type' => 'datetime', |
153 | 'label-message' => 'abusefilter-revert-periodstart', |
154 | 'min' => $min, |
155 | 'max' => $max |
156 | ]; |
157 | $searchFields['PeriodEnd'] = [ |
158 | 'type' => 'datetime', |
159 | 'label-message' => 'abusefilter-revert-periodend', |
160 | 'min' => $min, |
161 | 'max' => $max |
162 | ]; |
163 | |
164 | HTMLForm::factory( 'ooui', $searchFields, $this->getContext() ) |
165 | ->setTitle( $this->getTitle( "revert/$filter" ) ) |
166 | ->setWrapperLegendMsg( 'abusefilter-revert-search-legend' ) |
167 | ->setSubmitTextMsg( 'abusefilter-revert-search' ) |
168 | ->setMethod( 'get' ) |
169 | ->setFormIdentifier( 'revert-select-date' ) |
170 | ->setSubmitCallback( [ $this, 'showRevertableActions' ] ) |
171 | ->showAlways(); |
172 | } |
173 | |
174 | /** |
175 | * Show revertable actions, called as submit callback by HTMLForm |
176 | * @param array $formData |
177 | * @param HTMLForm $dateForm |
178 | * @return bool |
179 | */ |
180 | public function showRevertableActions( array $formData, HTMLForm $dateForm ): bool { |
181 | $lang = $this->getLanguage(); |
182 | $user = $this->getUser(); |
183 | $filter = $this->filter; |
184 | |
185 | // Look up all of them. |
186 | $results = $this->doLookup(); |
187 | if ( $results === [] ) { |
188 | $dateForm->addPostHtml( $this->msg( 'abusefilter-revert-preview-no-results' )->escaped() ); |
189 | return true; |
190 | } |
191 | |
192 | // Add a summary of everything that will be reversed. |
193 | $dateForm->addPostHtml( $this->msg( 'abusefilter-revert-preview-intro' )->parseAsBlock() ); |
194 | $list = []; |
195 | |
196 | foreach ( $results as $result ) { |
197 | $displayActions = []; |
198 | foreach ( $result['actions'] as $action ) { |
199 | $displayActions[] = $this->specsFormatter->getActionDisplay( $action ); |
200 | } |
201 | |
202 | /** @var ActionSpecifier $spec */ |
203 | $spec = $result['spec']; |
204 | $msg = $this->msg( 'abusefilter-revert-preview-item' ) |
205 | ->params( |
206 | $lang->userTimeAndDate( $result['timestamp'], $user ) |
207 | )->rawParams( |
208 | Linker::userLink( $spec->getUser()->getId(), $spec->getUser()->getName() ) |
209 | )->params( |
210 | $spec->getAction() |
211 | )->rawParams( |
212 | $this->linkRenderer->makeLink( $spec->getTitle() ) |
213 | )->params( |
214 | $lang->commaList( $displayActions ) |
215 | )->rawParams( |
216 | $this->linkRenderer->makeLink( |
217 | SpecialPage::getTitleFor( 'AbuseLog' ), |
218 | $this->msg( 'abusefilter-log-detailslink' )->text(), |
219 | [], |
220 | [ 'details' => $result['id'] ] |
221 | ) |
222 | )->params( |
223 | $spec->getUser()->getName() |
224 | )->parse(); |
225 | $list[] = Xml::tags( 'li', null, $msg ); |
226 | } |
227 | |
228 | $dateForm->addPostHtml( Xml::tags( 'ul', null, implode( "\n", $list ) ) ); |
229 | |
230 | // Add a button down the bottom. |
231 | $confirmForm = []; |
232 | $confirmForm['PeriodStart'] = [ |
233 | 'type' => 'hidden', |
234 | ]; |
235 | $confirmForm['PeriodEnd'] = [ |
236 | 'type' => 'hidden', |
237 | ]; |
238 | $confirmForm['Reason'] = [ |
239 | 'type' => 'text', |
240 | 'label-message' => 'abusefilter-revert-reasonfield', |
241 | 'id' => 'wpReason', |
242 | ]; |
243 | |
244 | $revertForm = HTMLForm::factory( 'ooui', $confirmForm, $this->getContext() ) |
245 | ->setTitle( $this->getTitle( "revert/$filter" ) ) |
246 | ->setTokenSalt( "abusefilter-revert-$filter" ) |
247 | ->setWrapperLegendMsg( 'abusefilter-revert-confirm-legend' ) |
248 | ->setSubmitTextMsg( 'abusefilter-revert-confirm' ) |
249 | ->prepareForm() |
250 | ->getHTML( true ); |
251 | $dateForm->addPostHtml( $revertForm ); |
252 | |
253 | return true; |
254 | } |
255 | |
256 | /** |
257 | * @return array[] |
258 | */ |
259 | public function doLookup() { |
260 | $periodStart = $this->periodStart; |
261 | $periodEnd = $this->periodEnd; |
262 | $filter = $this->filter; |
263 | $dbr = $this->lbFactory->getReplicaDatabase(); |
264 | |
265 | // Only hits from local filters can be reverted |
266 | $conds = [ 'afl_filter_id' => $filter, 'afl_global' => 0 ]; |
267 | |
268 | if ( $periodStart !== null ) { |
269 | $conds[] = 'afl_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $periodStart ) ); |
270 | } |
271 | if ( $periodEnd !== null ) { |
272 | $conds[] = 'afl_timestamp <= ' . $dbr->addQuotes( $dbr->timestamp( $periodEnd ) ); |
273 | } |
274 | |
275 | // Don't revert if there was no action, or the action was global |
276 | $conds[] = 'afl_actions != ' . $dbr->addQuotes( '' ); |
277 | $conds[] = 'afl_wiki IS NULL'; |
278 | |
279 | $selectFields = [ |
280 | 'afl_id', |
281 | 'afl_user', |
282 | 'afl_user_text', |
283 | 'afl_ip', |
284 | 'afl_action', |
285 | 'afl_actions', |
286 | 'afl_var_dump', |
287 | 'afl_timestamp', |
288 | 'afl_namespace', |
289 | 'afl_title', |
290 | ]; |
291 | $res = $dbr->select( |
292 | 'abuse_filter_log', |
293 | $selectFields, |
294 | $conds, |
295 | __METHOD__, |
296 | [ 'ORDER BY' => 'afl_timestamp DESC' ] |
297 | ); |
298 | |
299 | // TODO: get the following from ConsequencesRegistry or sth else |
300 | static $reversibleActions = [ 'block', 'blockautopromote', 'degroup' ]; |
301 | |
302 | $results = []; |
303 | foreach ( $res as $row ) { |
304 | $actions = explode( ',', $row->afl_actions ); |
305 | $currentReversibleActions = array_intersect( $actions, $reversibleActions ); |
306 | if ( count( $currentReversibleActions ) ) { |
307 | $vars = $this->varBlobStore->loadVarDump( $row->afl_var_dump ); |
308 | try { |
309 | // The variable is not lazy-loaded |
310 | $accountName = $vars->getComputedVariable( 'accountname' )->toNative(); |
311 | } catch ( UnsetVariableException $_ ) { |
312 | $accountName = null; |
313 | } |
314 | $results[] = [ |
315 | 'id' => $row->afl_id, |
316 | 'actions' => $currentReversibleActions, |
317 | 'vars' => $vars, |
318 | 'spec' => new ActionSpecifier( |
319 | $row->afl_action, |
320 | new TitleValue( (int)$row->afl_namespace, $row->afl_title ), |
321 | $this->userFactory->newFromAnyId( (int)$row->afl_user, $row->afl_user_text ), |
322 | $row->afl_ip, |
323 | $accountName |
324 | ), |
325 | 'timestamp' => $row->afl_timestamp |
326 | ]; |
327 | } |
328 | } |
329 | |
330 | return $results; |
331 | } |
332 | |
333 | /** |
334 | * Loads parameters from request |
335 | */ |
336 | public function loadParameters() { |
337 | $request = $this->getRequest(); |
338 | |
339 | $this->filter = (int)$this->mParams[1]; |
340 | $this->periodStart = strtotime( $request->getText( 'wpPeriodStart' ) ) ?: null; |
341 | $this->periodEnd = strtotime( $request->getText( 'wpPeriodEnd' ) ) ?: null; |
342 | $this->reason = $request->getVal( 'wpReason' ); |
343 | } |
344 | |
345 | /** |
346 | * @return bool |
347 | */ |
348 | public function attemptRevert() { |
349 | $filter = $this->filter; |
350 | $token = $this->getRequest()->getVal( 'wpEditToken' ); |
351 | if ( !$this->getUser()->matchEditToken( $token, "abusefilter-revert-$filter" ) ) { |
352 | return false; |
353 | } |
354 | |
355 | $results = $this->doLookup(); |
356 | foreach ( $results as $result ) { |
357 | foreach ( $result['actions'] as $action ) { |
358 | $this->revertAction( $action, $result ); |
359 | } |
360 | } |
361 | $this->getOutput()->addHTML( Html::successBox( |
362 | $this->msg( |
363 | 'abusefilter-revert-success', |
364 | $filter, |
365 | $this->getLanguage()->formatNum( $filter ) |
366 | )->parse() |
367 | ) ); |
368 | |
369 | return true; |
370 | } |
371 | |
372 | /** |
373 | * Helper method for typing |
374 | * @param string $action |
375 | * @param array $result |
376 | * @return ReversibleConsequence |
377 | */ |
378 | private function getConsequence( string $action, array $result ): ReversibleConsequence { |
379 | $params = new Parameters( |
380 | $this->filterLookup->getFilter( $this->filter, false ), |
381 | false, |
382 | $result['spec'] |
383 | ); |
384 | |
385 | switch ( $action ) { |
386 | case 'block': |
387 | return $this->consequencesFactory->newBlock( $params, '', false ); |
388 | case 'blockautopromote': |
389 | $duration = $this->getConfig()->get( 'AbuseFilterBlockAutopromoteDuration' ) * 86400; |
390 | return $this->consequencesFactory->newBlockAutopromote( $params, $duration ); |
391 | case 'degroup': |
392 | return $this->consequencesFactory->newDegroup( $params, $result['vars'] ); |
393 | default: |
394 | throw new UnexpectedValueException( "Invalid action $action" ); |
395 | } |
396 | } |
397 | |
398 | /** |
399 | * @param string $action |
400 | * @param array $result |
401 | * @return bool |
402 | */ |
403 | public function revertAction( string $action, array $result ): bool { |
404 | $message = $this->msg( |
405 | 'abusefilter-revert-reason', $this->filter, $this->reason |
406 | )->inContentLanguage()->text(); |
407 | |
408 | $consequence = $this->getConsequence( $action, $result ); |
409 | return $consequence->revert( $this->getUser(), $message ); |
410 | } |
411 | } |