Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
50.24% |
105 / 209 |
|
50.00% |
1 / 2 |
CRAP | |
0.00% |
0 / 1 |
QueryAbuseLog | |
50.24% |
105 / 209 |
|
50.00% |
1 / 2 |
620.11 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
execute | |
48.51% |
98 / 202 |
|
0.00% |
0 / 1 |
604.66 | |||
getAllowedParams | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
getExamplesMessages | n/a |
0 / 0 |
n/a |
0 / 0 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | */ |
18 | |
19 | namespace MediaWiki\Extension\AbuseFilter\Api; |
20 | |
21 | use InvalidArgumentException; |
22 | use MediaWiki\Api\ApiBase; |
23 | use MediaWiki\Api\ApiQuery; |
24 | use MediaWiki\Api\ApiQueryBase; |
25 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
26 | use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory; |
27 | use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException; |
28 | use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException; |
29 | use MediaWiki\Extension\AbuseFilter\Filter\Flags; |
30 | use MediaWiki\Extension\AbuseFilter\FilterLookup; |
31 | use MediaWiki\Extension\AbuseFilter\GlobalNameUtils; |
32 | use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog; |
33 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore; |
34 | use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager; |
35 | use MediaWiki\Title\Title; |
36 | use MediaWiki\User\UserFactory; |
37 | use MediaWiki\Utils\MWTimestamp; |
38 | use Wikimedia\IPUtils; |
39 | use Wikimedia\ParamValidator\ParamValidator; |
40 | use Wikimedia\ParamValidator\TypeDef\IntegerDef; |
41 | |
42 | /** |
43 | * Query module to list abuse log entries. |
44 | * |
45 | * @copyright 2009 Alex Z. <mrzmanwiki AT gmail DOT com> |
46 | * Based mostly on code by Bryan Tong Minh and Roan Kattouw |
47 | * |
48 | * @ingroup API |
49 | * @ingroup Extensions |
50 | */ |
51 | class QueryAbuseLog extends ApiQueryBase { |
52 | |
53 | private FilterLookup $afFilterLookup; |
54 | private AbuseFilterPermissionManager $afPermManager; |
55 | private VariablesBlobStore $afVariablesBlobStore; |
56 | private VariablesManager $afVariablesManager; |
57 | private UserFactory $userFactory; |
58 | private AbuseLoggerFactory $abuseLoggerFactory; |
59 | |
60 | public function __construct( |
61 | ApiQuery $query, |
62 | string $moduleName, |
63 | FilterLookup $afFilterLookup, |
64 | AbuseFilterPermissionManager $afPermManager, |
65 | VariablesBlobStore $afVariablesBlobStore, |
66 | VariablesManager $afVariablesManager, |
67 | UserFactory $userFactory, |
68 | AbuseLoggerFactory $abuseLoggerFactory |
69 | ) { |
70 | parent::__construct( $query, $moduleName, 'afl' ); |
71 | $this->afFilterLookup = $afFilterLookup; |
72 | $this->afPermManager = $afPermManager; |
73 | $this->afVariablesBlobStore = $afVariablesBlobStore; |
74 | $this->afVariablesManager = $afVariablesManager; |
75 | $this->userFactory = $userFactory; |
76 | $this->abuseLoggerFactory = $abuseLoggerFactory; |
77 | } |
78 | |
79 | /** |
80 | * @inheritDoc |
81 | */ |
82 | public function execute() { |
83 | $lookup = $this->afFilterLookup; |
84 | |
85 | // Same check as in SpecialAbuseLog |
86 | $this->checkUserRightsAny( 'abusefilter-log' ); |
87 | |
88 | $performer = $this->getAuthority(); |
89 | $params = $this->extractRequestParams(); |
90 | |
91 | $prop = array_fill_keys( $params['prop'], true ); |
92 | $fld_ids = isset( $prop['ids'] ); |
93 | $fld_filter = isset( $prop['filter'] ); |
94 | $fld_user = isset( $prop['user'] ); |
95 | $fld_title = isset( $prop['title'] ); |
96 | $fld_action = isset( $prop['action'] ); |
97 | $fld_details = isset( $prop['details'] ); |
98 | $fld_result = isset( $prop['result'] ); |
99 | $fld_timestamp = isset( $prop['timestamp'] ); |
100 | $fld_hidden = isset( $prop['hidden'] ); |
101 | $fld_revid = isset( $prop['revid'] ); |
102 | $isCentral = $this->getConfig()->get( 'AbuseFilterIsCentral' ); |
103 | $fld_wiki = $isCentral && isset( $prop['wiki'] ); |
104 | |
105 | if ( $fld_details ) { |
106 | $this->checkUserRightsAny( 'abusefilter-log-detail' ); |
107 | } |
108 | |
109 | $canViewPrivate = $this->afPermManager->canViewPrivateFiltersLogs( $performer ); |
110 | $canViewProtected = $this->afPermManager->canViewProtectedVariables( $performer ); |
111 | $canViewProtectedValues = $this->afPermManager->canViewProtectedVariableValues( $performer ); |
112 | |
113 | // Map of [ [ id, global ], ... ] |
114 | $searchFilters = []; |
115 | // Match permissions for viewing events on private filters to SpecialAbuseLog (bug 42814) |
116 | // @todo Avoid code duplication with SpecialAbuseLog::showList, make it so that, if hidden |
117 | // filters are specified, we only filter them out instead of failing. |
118 | if ( $params['filter'] ) { |
119 | if ( !is_array( $params['filter'] ) ) { |
120 | $params['filter'] = [ $params['filter'] ]; |
121 | } |
122 | $foundInvalid = false; |
123 | foreach ( $params['filter'] as $filter ) { |
124 | try { |
125 | $searchFilters[] = GlobalNameUtils::splitGlobalName( $filter ); |
126 | } catch ( InvalidArgumentException $e ) { |
127 | $foundInvalid = true; |
128 | continue; |
129 | } |
130 | } |
131 | |
132 | if ( !$canViewPrivate || !$canViewProtected || !$canViewProtectedValues ) { |
133 | foreach ( $searchFilters as [ $filterID, $global ] ) { |
134 | try { |
135 | $privacyLevel = $lookup->getFilter( $filterID, $global )->getPrivacyLevel(); |
136 | } catch ( CentralDBNotAvailableException $_ ) { |
137 | // Conservatively assume it's hidden and protected, like in AbuseLogPager::doFormatRow |
138 | $privacyLevel = Flags::FILTER_HIDDEN & Flags::FILTER_USES_PROTECTED_VARS; |
139 | } catch ( FilterNotFoundException $_ ) { |
140 | $privacyLevel = Flags::FILTER_PUBLIC; |
141 | $foundInvalid = true; |
142 | } |
143 | if ( !$canViewPrivate && ( Flags::FILTER_HIDDEN & $privacyLevel ) ) { |
144 | $this->dieWithError( |
145 | [ 'apierror-permissiondenied', $this->msg( 'action-abusefilter-log-private' ) ] |
146 | ); |
147 | } |
148 | if ( !$canViewProtected && ( Flags::FILTER_USES_PROTECTED_VARS & $privacyLevel ) ) { |
149 | $this->dieWithError( |
150 | [ 'apierror-permissiondenied', $this->msg( 'action-abusefilter-log-protected' ) ] |
151 | ); |
152 | } |
153 | if ( !$canViewProtectedValues && ( Flags::FILTER_USES_PROTECTED_VARS & $privacyLevel ) ) { |
154 | $this->dieWithError( |
155 | [ 'apierror-permissiondenied', $this->msg( 'action-abusefilter-log-protected-access' ) ] |
156 | ); |
157 | } |
158 | } |
159 | } |
160 | |
161 | if ( $foundInvalid ) { |
162 | // @todo Tell what the invalid IDs are |
163 | $this->addWarning( 'abusefilter-log-invalid-filter' ); |
164 | } |
165 | } |
166 | |
167 | $result = $this->getResult(); |
168 | |
169 | $this->addTables( 'abuse_filter_log' ); |
170 | $this->addFields( 'afl_timestamp' ); |
171 | $this->addFields( 'afl_rev_id' ); |
172 | $this->addFields( 'afl_deleted' ); |
173 | $this->addFields( 'afl_filter_id' ); |
174 | $this->addFields( 'afl_global' ); |
175 | $this->addFields( 'afl_ip' ); |
176 | $this->addFieldsIf( 'afl_id', $fld_ids ); |
177 | $this->addFieldsIf( 'afl_user_text', $fld_user ); |
178 | $this->addFieldsIf( [ 'afl_namespace', 'afl_title' ], $fld_title ); |
179 | $this->addFieldsIf( 'afl_action', $fld_action ); |
180 | $this->addFieldsIf( 'afl_var_dump', $fld_details ); |
181 | $this->addFieldsIf( 'afl_actions', $fld_result ); |
182 | $this->addFieldsIf( 'afl_wiki', $fld_wiki ); |
183 | |
184 | if ( $fld_filter ) { |
185 | $this->addTables( 'abuse_filter' ); |
186 | $this->addFields( 'af_public_comments' ); |
187 | |
188 | $this->addJoinConds( [ |
189 | 'abuse_filter' => [ |
190 | 'LEFT JOIN', |
191 | [ |
192 | 'af_id=afl_filter_id', |
193 | 'afl_global' => 0 |
194 | ] |
195 | ] |
196 | ] ); |
197 | } |
198 | |
199 | $this->addOption( 'LIMIT', $params['limit'] + 1 ); |
200 | |
201 | $this->addWhereIf( [ 'afl_id' => $params['logid'] ], isset( $params['logid'] ) ); |
202 | |
203 | $this->addWhereRange( 'afl_timestamp', $params['dir'], $params['start'], $params['end'] ); |
204 | |
205 | if ( isset( $params['user'] ) ) { |
206 | $u = $this->userFactory->newFromName( $params['user'] ); |
207 | if ( $u ) { |
208 | // Username normalisation |
209 | $params['user'] = $u->getName(); |
210 | $userId = $u->getId(); |
211 | } elseif ( IPUtils::isIPAddress( $params['user'] ) ) { |
212 | // It's an IP, sanitize it |
213 | $params['user'] = IPUtils::sanitizeIP( $params['user'] ); |
214 | $userId = 0; |
215 | } |
216 | |
217 | if ( isset( $userId ) ) { |
218 | // Only add the WHERE for user in case it's either a valid user |
219 | // (but not necessary an existing one) or an IP. |
220 | $this->addWhere( |
221 | [ |
222 | 'afl_user' => $userId, |
223 | 'afl_user_text' => $params['user'] |
224 | ] |
225 | ); |
226 | } |
227 | } |
228 | |
229 | $this->addWhereIf( [ 'afl_deleted' => 0 ], !$this->afPermManager->canSeeHiddenLogEntries( $performer ) ); |
230 | |
231 | if ( $searchFilters ) { |
232 | // @todo Avoid code duplication with SpecialAbuseLog::showList |
233 | $filterConds = [ 'local' => [], 'global' => [] ]; |
234 | foreach ( $searchFilters as $filter ) { |
235 | $isGlobal = $filter[1]; |
236 | $key = $isGlobal ? 'global' : 'local'; |
237 | $filterConds[$key][] = $filter[0]; |
238 | } |
239 | $dbr = $this->getDB(); |
240 | $conds = []; |
241 | if ( $filterConds['local'] ) { |
242 | $conds[] = $dbr->andExpr( [ |
243 | 'afl_global' => 0, |
244 | // @phan-suppress-previous-line PhanTypeMismatchArgument Array is non-empty |
245 | 'afl_filter_id' => $filterConds['local'], |
246 | ] ); |
247 | } |
248 | if ( $filterConds['global'] ) { |
249 | $conds[] = $dbr->andExpr( [ |
250 | 'afl_global' => 1, |
251 | // @phan-suppress-previous-line PhanTypeMismatchArgument Array is non-empty |
252 | 'afl_filter_id' => $filterConds['global'], |
253 | ] ); |
254 | } |
255 | $this->addWhere( $dbr->orExpr( $conds ) ); |
256 | } |
257 | |
258 | if ( isset( $params['wiki'] ) ) { |
259 | // 'wiki' won't be set if $wgAbuseFilterIsCentral = false |
260 | $this->addWhereIf( [ 'afl_wiki' => $params['wiki'] ], $isCentral ); |
261 | } |
262 | |
263 | $title = $params['title']; |
264 | if ( $title !== null ) { |
265 | $titleObj = Title::newFromText( $title ); |
266 | if ( $titleObj === null ) { |
267 | $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] ); |
268 | } |
269 | $this->addWhereFld( 'afl_namespace', $titleObj->getNamespace() ); |
270 | $this->addWhereFld( 'afl_title', $titleObj->getDBkey() ); |
271 | } |
272 | $res = $this->select( __METHOD__ ); |
273 | |
274 | $count = 0; |
275 | foreach ( $res as $row ) { |
276 | if ( ++$count > $params['limit'] ) { |
277 | // We've had enough |
278 | $ts = new MWTimestamp( $row->afl_timestamp ); |
279 | $this->setContinueEnumParameter( 'start', $ts->getTimestamp( TS_ISO_8601 ) ); |
280 | break; |
281 | } |
282 | $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager ); |
283 | if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) { |
284 | continue; |
285 | } |
286 | |
287 | $filterID = $row->afl_filter_id; |
288 | $global = $row->afl_global; |
289 | $fullName = GlobalNameUtils::buildGlobalName( $filterID, $global ); |
290 | $privacyLevel = $lookup->getFilter( $filterID, $global )->getPrivacyLevel(); |
291 | $canSeeDetails = $this->afPermManager->canSeeLogDetailsForFilter( $performer, $privacyLevel ); |
292 | |
293 | $entry = []; |
294 | if ( $fld_ids ) { |
295 | $entry['id'] = intval( $row->afl_id ); |
296 | $entry['filter_id'] = $canSeeDetails ? $fullName : ''; |
297 | } |
298 | if ( $fld_filter ) { |
299 | if ( $global ) { |
300 | $entry['filter'] = $lookup->getFilter( $filterID, true )->getName(); |
301 | } else { |
302 | $entry['filter'] = $row->af_public_comments; |
303 | } |
304 | } |
305 | if ( $fld_user ) { |
306 | $entry['user'] = $row->afl_user_text; |
307 | } |
308 | if ( $fld_wiki ) { |
309 | $entry['wiki'] = $row->afl_wiki; |
310 | } |
311 | if ( $fld_title ) { |
312 | $title = Title::makeTitle( $row->afl_namespace, $row->afl_title ); |
313 | ApiQueryBase::addTitleInfo( $entry, $title ); |
314 | } |
315 | if ( $fld_action ) { |
316 | $entry['action'] = $row->afl_action; |
317 | } |
318 | if ( $fld_result ) { |
319 | $entry['result'] = $row->afl_actions; |
320 | } |
321 | if ( $fld_revid && $row->afl_rev_id !== null ) { |
322 | $entry['revid'] = $canSeeDetails ? (int)$row->afl_rev_id : ''; |
323 | } |
324 | if ( $fld_timestamp ) { |
325 | $ts = new MWTimestamp( $row->afl_timestamp ); |
326 | $entry['timestamp'] = $ts->getTimestamp( TS_ISO_8601 ); |
327 | } |
328 | if ( $fld_details ) { |
329 | $entry['details'] = []; |
330 | if ( $canSeeDetails ) { |
331 | $vars = $this->afVariablesBlobStore->loadVarDump( $row ); |
332 | $varManager = $this->afVariablesManager; |
333 | $entry['details'] = $varManager->exportAllVars( $vars ); |
334 | |
335 | $usedProtectedVars = $this->afPermManager |
336 | ->getUsedProtectedVariables( array_keys( $entry['details'] ) ); |
337 | if ( $usedProtectedVars ) { |
338 | // Unset the variable if the user can't see protected variables |
339 | // Additionally, a protected variable is considered used if the key exists |
340 | // but since it can have a null value, check isset before logging access |
341 | $shouldLog = false; |
342 | foreach ( $usedProtectedVars as $protectedVariable ) { |
343 | if ( isset( $entry['details'][$protectedVariable] ) ) { |
344 | if ( $canViewProtectedValues ) { |
345 | $shouldLog = true; |
346 | } else { |
347 | $entry['details'][$protectedVariable] = ''; |
348 | } |
349 | } |
350 | } |
351 | |
352 | if ( $shouldLog ) { |
353 | // user_name or accountname should always exist -- just in case |
354 | // if it doesn't, unset the protected variables since they shouldn't be accessed if |
355 | // the access isn't logged |
356 | if ( isset( $entry['details']['user_name'] ) || |
357 | isset( $entry['details']['accountname'] ) |
358 | ) { |
359 | $logger = $this->abuseLoggerFactory->getProtectedVarsAccessLogger(); |
360 | $logger->logViewProtectedVariableValue( |
361 | $performer->getUser(), |
362 | $entry['details']['user_name'] ?? $entry['details']['accountname'] |
363 | ); |
364 | } else { |
365 | foreach ( $usedProtectedVars as $protectedVariable ) { |
366 | if ( isset( $entry['details'][$protectedVariable] ) ) { |
367 | $entry['details'][$protectedVariable] = ''; |
368 | } |
369 | } |
370 | } |
371 | |
372 | } |
373 | } |
374 | } |
375 | } |
376 | |
377 | if ( $fld_hidden ) { |
378 | $entry['hidden'] = (bool)$row->afl_deleted; |
379 | } |
380 | |
381 | if ( $entry ) { |
382 | $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $entry ); |
383 | if ( !$fit ) { |
384 | $ts = new MWTimestamp( $row->afl_timestamp ); |
385 | $this->setContinueEnumParameter( 'start', $ts->getTimestamp( TS_ISO_8601 ) ); |
386 | break; |
387 | } |
388 | } |
389 | } |
390 | $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' ); |
391 | } |
392 | |
393 | /** |
394 | * @codeCoverageIgnore Merely declarative |
395 | * @inheritDoc |
396 | */ |
397 | public function getAllowedParams() { |
398 | $params = [ |
399 | 'logid' => [ |
400 | ParamValidator::PARAM_TYPE => 'integer' |
401 | ], |
402 | 'start' => [ |
403 | ParamValidator::PARAM_TYPE => 'timestamp' |
404 | ], |
405 | 'end' => [ |
406 | ParamValidator::PARAM_TYPE => 'timestamp' |
407 | ], |
408 | 'dir' => [ |
409 | ParamValidator::PARAM_TYPE => [ |
410 | 'newer', |
411 | 'older' |
412 | ], |
413 | ParamValidator::PARAM_DEFAULT => 'older', |
414 | ApiBase::PARAM_HELP_MSG => 'api-help-param-direction', |
415 | ], |
416 | 'user' => null, |
417 | 'title' => null, |
418 | 'filter' => [ |
419 | ParamValidator::PARAM_TYPE => 'string', |
420 | ParamValidator::PARAM_ISMULTI => true, |
421 | ApiBase::PARAM_HELP_MSG => [ |
422 | 'apihelp-query+abuselog-param-filter', |
423 | GlobalNameUtils::GLOBAL_FILTER_PREFIX |
424 | ] |
425 | ], |
426 | 'limit' => [ |
427 | ParamValidator::PARAM_DEFAULT => 10, |
428 | ParamValidator::PARAM_TYPE => 'limit', |
429 | IntegerDef::PARAM_MIN => 1, |
430 | IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1, |
431 | IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2 |
432 | ], |
433 | 'prop' => [ |
434 | ParamValidator::PARAM_DEFAULT => 'ids|user|title|action|result|timestamp|hidden|revid', |
435 | ParamValidator::PARAM_TYPE => [ |
436 | 'ids', |
437 | 'filter', |
438 | 'user', |
439 | 'title', |
440 | 'action', |
441 | 'details', |
442 | 'result', |
443 | 'timestamp', |
444 | 'hidden', |
445 | 'revid', |
446 | ], |
447 | ParamValidator::PARAM_ISMULTI => true |
448 | ] |
449 | ]; |
450 | if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) { |
451 | $params['wiki'] = [ |
452 | ParamValidator::PARAM_TYPE => 'string', |
453 | ]; |
454 | $params['prop'][ParamValidator::PARAM_DEFAULT] .= '|wiki'; |
455 | $params['prop'][ParamValidator::PARAM_TYPE][] = 'wiki'; |
456 | $params['filter'][ApiBase::PARAM_HELP_MSG] = 'apihelp-query+abuselog-param-filter-central'; |
457 | } |
458 | return $params; |
459 | } |
460 | |
461 | /** |
462 | * @codeCoverageIgnore Merely declarative |
463 | * @inheritDoc |
464 | */ |
465 | protected function getExamplesMessages() { |
466 | return [ |
467 | 'action=query&list=abuselog' |
468 | => 'apihelp-query+abuselog-example-1', |
469 | 'action=query&list=abuselog&afltitle=API' |
470 | => 'apihelp-query+abuselog-example-2', |
471 | ]; |
472 | } |
473 | } |