Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.71% |
116 / 201 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewExamine | |
57.71% |
116 / 201 |
|
28.57% |
2 / 7 |
115.36 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
show | |
78.57% |
11 / 14 |
|
0.00% |
0 / 1 |
7.48 | |||
showSearch | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
2 | |||
showResults | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
showExaminerForRC | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
showExaminerForLogEntry | |
87.30% |
55 / 63 |
|
0.00% |
0 / 1 |
13.35 | |||
showExaminer | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\View; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Context\IContextSource; |
7 | use MediaWiki\Extension\AbuseFilter\AbuseFilterChangesList; |
8 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
9 | use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory; |
10 | use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException; |
11 | use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory; |
12 | use MediaWiki\Extension\AbuseFilter\Filter\MutableFilter; |
13 | use MediaWiki\Extension\AbuseFilter\FilterLookup; |
14 | use MediaWiki\Extension\AbuseFilter\Pager\AbuseFilterExaminePager; |
15 | use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog; |
16 | use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory; |
17 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
18 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore; |
19 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesFormatter; |
20 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager; |
21 | use MediaWiki\Html\Html; |
22 | use MediaWiki\HTMLForm\HTMLForm; |
23 | use MediaWiki\Linker\LinkRenderer; |
24 | use MediaWiki\RecentChanges\ChangesList; |
25 | use MediaWiki\RecentChanges\RecentChange; |
26 | use MediaWiki\Revision\RevisionRecord; |
27 | use MediaWiki\Title\Title; |
28 | use OOUI; |
29 | use Wikimedia\Rdbms\LBFactory; |
30 | |
31 | class AbuseFilterViewExamine extends AbuseFilterView { |
32 | /** |
33 | * @var string The rules of the filter we're examining |
34 | */ |
35 | private $testFilter; |
36 | /** |
37 | * @var LBFactory |
38 | */ |
39 | private $lbFactory; |
40 | /** |
41 | * @var FilterLookup |
42 | */ |
43 | private $filterLookup; |
44 | /** |
45 | * @var EditBoxBuilderFactory |
46 | */ |
47 | private $boxBuilderFactory; |
48 | /** |
49 | * @var VariablesBlobStore |
50 | */ |
51 | private $varBlobStore; |
52 | /** |
53 | * @var VariablesFormatter |
54 | */ |
55 | private $variablesFormatter; |
56 | /** |
57 | * @var VariablesManager |
58 | */ |
59 | private $varManager; |
60 | /** |
61 | * @var VariableGeneratorFactory |
62 | */ |
63 | private $varGeneratorFactory; |
64 | |
65 | private AbuseLoggerFactory $abuseLoggerFactory; |
66 | |
67 | /** |
68 | * @param LBFactory $lbFactory |
69 | * @param AbuseFilterPermissionManager $afPermManager |
70 | * @param FilterLookup $filterLookup |
71 | * @param EditBoxBuilderFactory $boxBuilderFactory |
72 | * @param VariablesBlobStore $varBlobStore |
73 | * @param VariablesFormatter $variablesFormatter |
74 | * @param VariablesManager $varManager |
75 | * @param VariableGeneratorFactory $varGeneratorFactory |
76 | * @param AbuseLoggerFactory $abuseLoggerFactory |
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 | AbuseFilterPermissionManager $afPermManager, |
85 | FilterLookup $filterLookup, |
86 | EditBoxBuilderFactory $boxBuilderFactory, |
87 | VariablesBlobStore $varBlobStore, |
88 | VariablesFormatter $variablesFormatter, |
89 | VariablesManager $varManager, |
90 | VariableGeneratorFactory $varGeneratorFactory, |
91 | AbuseLoggerFactory $abuseLoggerFactory, |
92 | IContextSource $context, |
93 | LinkRenderer $linkRenderer, |
94 | string $basePageName, |
95 | array $params |
96 | ) { |
97 | parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params ); |
98 | $this->lbFactory = $lbFactory; |
99 | $this->filterLookup = $filterLookup; |
100 | $this->boxBuilderFactory = $boxBuilderFactory; |
101 | $this->varBlobStore = $varBlobStore; |
102 | $this->variablesFormatter = $variablesFormatter; |
103 | $this->variablesFormatter->setMessageLocalizer( $context ); |
104 | $this->varManager = $varManager; |
105 | $this->varGeneratorFactory = $varGeneratorFactory; |
106 | $this->abuseLoggerFactory = $abuseLoggerFactory; |
107 | } |
108 | |
109 | /** |
110 | * Shows the page |
111 | */ |
112 | public function show() { |
113 | $out = $this->getOutput(); |
114 | $out->setPageTitleMsg( $this->msg( 'abusefilter-examine' ) ); |
115 | $out->addHelpLink( 'Extension:AbuseFilter/Rules format' ); |
116 | if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) { |
117 | $out->addWikiMsg( 'abusefilter-examine-intro' ); |
118 | } else { |
119 | $out->addWikiMsg( 'abusefilter-examine-intro-examine-only' ); |
120 | } |
121 | |
122 | $this->testFilter = $this->getRequest()->getText( 'testfilter' ); |
123 | |
124 | // Check if we've got a subpage |
125 | if ( count( $this->mParams ) > 1 && is_numeric( $this->mParams[1] ) ) { |
126 | $this->showExaminerForRC( $this->mParams[1] ); |
127 | } elseif ( count( $this->mParams ) > 2 |
128 | && $this->mParams[1] === 'log' |
129 | && is_numeric( $this->mParams[2] ) |
130 | ) { |
131 | $this->showExaminerForLogEntry( $this->mParams[2] ); |
132 | } else { |
133 | $this->showSearch(); |
134 | } |
135 | } |
136 | |
137 | /** |
138 | * Shows the search form |
139 | */ |
140 | public function showSearch() { |
141 | $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); |
142 | $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge ); |
143 | $max = wfTimestampNow(); |
144 | $formDescriptor = [ |
145 | 'SearchUser' => [ |
146 | 'label-message' => 'abusefilter-test-user', |
147 | 'type' => 'user', |
148 | 'ipallowed' => true, |
149 | ], |
150 | 'SearchPeriodStart' => [ |
151 | 'label-message' => 'abusefilter-test-period-start', |
152 | 'type' => 'datetime', |
153 | 'min' => $min, |
154 | 'max' => $max, |
155 | ], |
156 | 'SearchPeriodEnd' => [ |
157 | 'label-message' => 'abusefilter-test-period-end', |
158 | 'type' => 'datetime', |
159 | 'min' => $min, |
160 | 'max' => $max, |
161 | ], |
162 | ]; |
163 | HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ) |
164 | ->addHiddenField( 'testfilter', $this->testFilter ) |
165 | ->setWrapperLegendMsg( 'abusefilter-examine-legend' ) |
166 | ->setSubmitTextMsg( 'abusefilter-examine-submit' ) |
167 | ->setSubmitCallback( [ $this, 'showResults' ] ) |
168 | ->showAlways(); |
169 | } |
170 | |
171 | /** |
172 | * Show search results, called as submit callback by HTMLForm |
173 | * @param array $formData |
174 | * @param HTMLForm $form |
175 | * @return bool |
176 | */ |
177 | public function showResults( array $formData, HTMLForm $form ): bool { |
178 | $changesList = new AbuseFilterChangesList( $this->getContext(), $this->testFilter ); |
179 | |
180 | $dbr = $this->lbFactory->getReplicaDatabase(); |
181 | $conds = $this->buildVisibilityConditions( $dbr, $this->getAuthority() ); |
182 | $conds[] = $this->buildTestConditions( $dbr ); |
183 | |
184 | // Normalise username |
185 | $userTitle = Title::newFromText( $formData['SearchUser'], NS_USER ); |
186 | $userName = $userTitle ? $userTitle->getText() : ''; |
187 | |
188 | if ( $userName !== '' ) { |
189 | $rcQuery = RecentChange::getQueryInfo(); |
190 | $conds[$rcQuery['fields']['rc_user_text']] = $userName; |
191 | } |
192 | |
193 | $startTS = strtotime( $formData['SearchPeriodStart'] ); |
194 | if ( $startTS ) { |
195 | $conds[] = $dbr->expr( 'rc_timestamp', '>=', $dbr->timestamp( $startTS ) ); |
196 | } |
197 | $endTS = strtotime( $formData['SearchPeriodEnd'] ); |
198 | if ( $endTS ) { |
199 | $conds[] = $dbr->expr( 'rc_timestamp', '<=', $dbr->timestamp( $endTS ) ); |
200 | } |
201 | $pager = new AbuseFilterExaminePager( |
202 | $changesList, |
203 | $this->linkRenderer, |
204 | $dbr, |
205 | $this->getTitle( 'examine' ), |
206 | $conds |
207 | ); |
208 | |
209 | $output = $changesList->beginRecentChangesList() |
210 | . $pager->getNavigationBar() |
211 | . $pager->getBody() |
212 | . $pager->getNavigationBar() |
213 | . $changesList->endRecentChangesList(); |
214 | |
215 | $form->addPostHtml( $output ); |
216 | return true; |
217 | } |
218 | |
219 | /** |
220 | * @param int $rcid |
221 | */ |
222 | public function showExaminerForRC( $rcid ) { |
223 | // Get data |
224 | $rc = RecentChange::newFromId( $rcid ); |
225 | $out = $this->getOutput(); |
226 | if ( !$rc ) { |
227 | $out->addWikiMsg( 'abusefilter-examine-notfound' ); |
228 | return; |
229 | } |
230 | |
231 | if ( !ChangesList::userCan( $rc, RevisionRecord::SUPPRESSED_ALL ) ) { |
232 | $out->addWikiMsg( 'abusefilter-log-details-hidden-implicit' ); |
233 | return; |
234 | } |
235 | |
236 | $varGenerator = $this->varGeneratorFactory->newRCGenerator( $rc, $this->getUser() ); |
237 | $vars = $varGenerator->getVars(); |
238 | if ( !$vars ) { |
239 | $out->addWikiMsg( 'abusefilter-examine-incompatible' ); |
240 | return; |
241 | } |
242 | |
243 | $out->addJsConfigVars( [ |
244 | 'abuseFilterExamine' => [ 'type' => 'rc', 'id' => $rcid ] |
245 | ] ); |
246 | |
247 | $this->showExaminer( $vars ); |
248 | } |
249 | |
250 | /** |
251 | * @param int $logid |
252 | */ |
253 | public function showExaminerForLogEntry( $logid ) { |
254 | // Get data |
255 | $dbr = $this->lbFactory->getReplicaDatabase(); |
256 | $performer = $this->getAuthority(); |
257 | $out = $this->getOutput(); |
258 | |
259 | $row = $dbr->newSelectQueryBuilder() |
260 | ->select( [ |
261 | 'afl_deleted', |
262 | 'afl_ip', |
263 | 'afl_var_dump', |
264 | 'afl_rev_id', |
265 | 'afl_filter_id', |
266 | 'afl_global' |
267 | ] ) |
268 | ->from( 'abuse_filter_log' ) |
269 | ->where( [ 'afl_id' => $logid ] ) |
270 | ->caller( __METHOD__ ) |
271 | ->fetchRow(); |
272 | |
273 | if ( !$row ) { |
274 | $out->addWikiMsg( 'abusefilter-examine-notfound' ); |
275 | return; |
276 | } |
277 | |
278 | try { |
279 | $filter = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global ); |
280 | } catch ( CentralDBNotAvailableException $_ ) { |
281 | // Conservatively assume that it's hidden and protected, like in AbuseLogPager::doFormatRow |
282 | $filter = MutableFilter::newDefault(); |
283 | $filter->setProtected( true ); |
284 | $filter->setHidden( true ); |
285 | } |
286 | if ( !$this->afPermManager->canSeeLogDetailsForFilter( $performer, $filter ) ) { |
287 | $out->addWikiMsg( 'abusefilter-log-cannot-see-details' ); |
288 | return; |
289 | } |
290 | |
291 | $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager ); |
292 | if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) { |
293 | if ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN ) { |
294 | $msg = 'abusefilter-log-details-hidden'; |
295 | } elseif ( $visibility === SpecialAbuseLog::VISIBILITY_HIDDEN_IMPLICIT ) { |
296 | $msg = 'abusefilter-log-details-hidden-implicit'; |
297 | } else { |
298 | throw new LogicException( "Unexpected visibility $visibility" ); |
299 | } |
300 | $out->addWikiMsg( $msg ); |
301 | return; |
302 | } |
303 | |
304 | $vars = $this->varBlobStore->loadVarDump( $row ); |
305 | $varsArray = $this->varManager->dumpAllVars( $vars, true ); |
306 | |
307 | // Check that the user can see the protected variables that are being examined if the filter is protected. |
308 | $userAuthority = $this->getAuthority(); |
309 | if ( |
310 | $filter->isProtected() && |
311 | !$this->afPermManager->canViewProtectedVariables( $userAuthority, array_keys( $varsArray ) )->isGood() |
312 | ) { |
313 | $out->addWikiMsg( 'abusefilter-examine-protected-vars-permission' ); |
314 | return; |
315 | } |
316 | |
317 | // AbuseFilter logs created before T390086 may have protected variables present in the variable dump |
318 | // when the filter itself isn't protected. This is because a different filter matched against the |
319 | // a protected variable which caused the value to be added to the var dump for the public filter |
320 | // match. |
321 | // We shouldn't block access to the details of an otherwise public filter hit so |
322 | // instead only check for access to the protected variables and redact them if the user |
323 | // shouldn't see them. |
324 | $protectedVariableValuesShown = []; |
325 | foreach ( $this->afPermManager->getProtectedVariables() as $protectedVariable ) { |
326 | if ( isset( $varsArray[$protectedVariable] ) ) { |
327 | // Try each variable at a time, as the user may be able to see some but not all of the |
328 | // protected variables. We only want to redact what is necessary to redact. |
329 | $canViewProtectedVariable = $this->afPermManager |
330 | ->canViewProtectedVariables( $userAuthority, [ $protectedVariable ] )->isGood(); |
331 | if ( !$canViewProtectedVariable ) { |
332 | $varsArray[$protectedVariable] = ''; |
333 | } else { |
334 | $protectedVariableValuesShown[] = $protectedVariable; |
335 | } |
336 | } |
337 | } |
338 | $vars = VariableHolder::newFromArray( $varsArray ); |
339 | |
340 | if ( $filter->isProtected() ) { |
341 | $logger = $this->abuseLoggerFactory->getProtectedVarsAccessLogger(); |
342 | $logger->logViewProtectedVariableValue( |
343 | $userAuthority->getUser(), |
344 | $varsArray['user_name'] ?? $varsArray['accountname'], |
345 | $protectedVariableValuesShown |
346 | ); |
347 | } |
348 | |
349 | $out->addJsConfigVars( [ |
350 | 'abuseFilterExamine' => [ 'type' => 'log', 'id' => $logid ] |
351 | ] ); |
352 | $this->showExaminer( $vars ); |
353 | } |
354 | |
355 | /** |
356 | * @param VariableHolder $vars |
357 | */ |
358 | public function showExaminer( VariableHolder $vars ) { |
359 | $output = $this->getOutput(); |
360 | $output->enableOOUI(); |
361 | |
362 | $html = ''; |
363 | |
364 | $output->addModules( 'ext.abuseFilter.examine' ); |
365 | $output->addJsConfigVars( [ |
366 | 'wgAbuseFilterVariables' => $this->varManager->dumpAllVars( $vars, true ), |
367 | ] ); |
368 | |
369 | // Add test bit |
370 | if ( $this->afPermManager->canUseTestTools( $this->getAuthority() ) ) { |
371 | $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder( |
372 | $this, |
373 | $this->getAuthority(), |
374 | $output |
375 | ); |
376 | |
377 | $tester = Html::rawElement( 'h2', [], $this->msg( 'abusefilter-examine-test' )->parse() ); |
378 | $tester .= $boxBuilder->buildEditBox( $this->testFilter, false, false, false ); |
379 | $tester .= $this->buildFilterLoader(); |
380 | $html .= Html::rawElement( 'div', [ 'id' => 'mw-abusefilter-examine-editor' ], $tester ); |
381 | $html .= Html::rawElement( 'p', |
382 | [], |
383 | new OOUI\ButtonInputWidget( |
384 | [ |
385 | 'label' => $this->msg( 'abusefilter-examine-test-button' )->text(), |
386 | 'id' => 'mw-abusefilter-examine-test', |
387 | 'flags' => [ 'primary', 'progressive' ] |
388 | ] |
389 | ) . |
390 | Html::element( 'div', |
391 | [ |
392 | 'id' => 'mw-abusefilter-syntaxresult', |
393 | 'style' => 'display: none;' |
394 | ] |
395 | ) |
396 | ); |
397 | } |
398 | |
399 | // Variable dump |
400 | $html .= Html::rawElement( |
401 | 'h2', |
402 | [], |
403 | $this->msg( 'abusefilter-examine-vars' )->parse() |
404 | ); |
405 | $html .= $this->variablesFormatter->buildVarDumpTable( $vars ); |
406 | |
407 | $output->addHTML( $html ); |
408 | } |
409 | |
410 | } |