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