Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.29% covered (danger)
30.29%
53 / 175
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
QueryAbuseLog
30.29% covered (danger)
30.29%
53 / 175
50.00% covered (danger)
50.00%
1 / 2
932.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 execute
28.24% covered (danger)
28.24%
48 / 170
0.00% covered (danger)
0.00%
0 / 1
863.45
 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
19namespace MediaWiki\Extension\AbuseFilter\Api;
20
21use ApiBase;
22use ApiQuery;
23use ApiQueryBase;
24use InvalidArgumentException;
25use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
26use MediaWiki\Extension\AbuseFilter\CentralDBNotAvailableException;
27use MediaWiki\Extension\AbuseFilter\Filter\FilterNotFoundException;
28use MediaWiki\Extension\AbuseFilter\FilterLookup;
29use MediaWiki\Extension\AbuseFilter\GlobalNameUtils;
30use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
31use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
32use MediaWiki\Extension\AbuseFilter\Variables\VariablesManager;
33use MediaWiki\Title\Title;
34use MediaWiki\User\User;
35use MediaWiki\Utils\MWTimestamp;
36use Wikimedia\IPUtils;
37use Wikimedia\ParamValidator\ParamValidator;
38use Wikimedia\ParamValidator\TypeDef\IntegerDef;
39
40/**
41 * Query module to list abuse log entries.
42 *
43 * @copyright 2009 Alex Z. <mrzmanwiki AT gmail DOT com>
44 * Based mostly on code by Bryan Tong Minh and Roan Kattouw
45 *
46 * @ingroup API
47 * @ingroup Extensions
48 */
49class QueryAbuseLog extends ApiQueryBase {
50
51    /** @var FilterLookup */
52    private $afFilterLookup;
53
54    /** @var AbuseFilterPermissionManager */
55    private $afPermManager;
56
57    /** @var VariablesBlobStore */
58    private $afVariablesBlobStore;
59
60    /** @var VariablesManager */
61    private $afVariablesManager;
62
63    /**
64     * @param ApiQuery $query
65     * @param string $moduleName
66     * @param FilterLookup $afFilterLookup
67     * @param AbuseFilterPermissionManager $afPermManager
68     * @param VariablesBlobStore $afVariablesBlobStore
69     * @param VariablesManager $afVariablesManager
70     */
71    public function __construct(
72        ApiQuery $query,
73        $moduleName,
74        FilterLookup $afFilterLookup,
75        AbuseFilterPermissionManager $afPermManager,
76        VariablesBlobStore $afVariablesBlobStore,
77        VariablesManager $afVariablesManager
78    ) {
79        parent::__construct( $query, $moduleName, 'afl' );
80        $this->afFilterLookup = $afFilterLookup;
81        $this->afPermManager = $afPermManager;
82        $this->afVariablesBlobStore = $afVariablesBlobStore;
83        $this->afVariablesManager = $afVariablesManager;
84    }
85
86    /**
87     * @inheritDoc
88     */
89    public function execute() {
90        $lookup = $this->afFilterLookup;
91
92        // Same check as in SpecialAbuseLog
93        $this->checkUserRightsAny( 'abusefilter-log' );
94
95        $performer = $this->getAuthority();
96        $params = $this->extractRequestParams();
97
98        $prop = array_fill_keys( $params['prop'], true );
99        $fld_ids = isset( $prop['ids'] );
100        $fld_filter = isset( $prop['filter'] );
101        $fld_user = isset( $prop['user'] );
102        $fld_title = isset( $prop['title'] );
103        $fld_action = isset( $prop['action'] );
104        $fld_details = isset( $prop['details'] );
105        $fld_result = isset( $prop['result'] );
106        $fld_timestamp = isset( $prop['timestamp'] );
107        $fld_hidden = isset( $prop['hidden'] );
108        $fld_revid = isset( $prop['revid'] );
109        $isCentral = $this->getConfig()->get( 'AbuseFilterIsCentral' );
110        $fld_wiki = $isCentral && isset( $prop['wiki'] );
111
112        if ( $fld_details ) {
113            $this->checkUserRightsAny( 'abusefilter-log-detail' );
114        }
115
116        // Map of [ [ id, global ], ... ]
117        $searchFilters = [];
118        // Match permissions for viewing events on private filters to SpecialAbuseLog (bug 42814)
119        // @todo Avoid code duplication with SpecialAbuseLog::showList, make it so that, if hidden
120        // filters are specified, we only filter them out instead of failing.
121        if ( $params['filter'] ) {
122            if ( !is_array( $params['filter'] ) ) {
123                $params['filter'] = [ $params['filter'] ];
124            }
125
126            $foundInvalid = false;
127            foreach ( $params['filter'] as $filter ) {
128                try {
129                    $searchFilters[] = GlobalNameUtils::splitGlobalName( $filter );
130                } catch ( InvalidArgumentException $e ) {
131                    $foundInvalid = true;
132                    continue;
133                }
134            }
135            if ( !$this->afPermManager->canViewPrivateFiltersLogs( $performer ) ) {
136                foreach ( $searchFilters as [ $filterID, $global ] ) {
137                    try {
138                        $isHidden = $lookup->getFilter( $filterID, $global )->isHidden();
139                    } catch ( CentralDBNotAvailableException $_ ) {
140                        // Conservatively assume it's hidden, like in SpecialAbuseLog
141                        $isHidden = true;
142                    } catch ( FilterNotFoundException $_ ) {
143                        $isHidden = false;
144                        $foundInvalid = true;
145                    }
146                    if ( $isHidden ) {
147                        $this->dieWithError(
148                            [ 'apierror-permissiondenied', $this->msg( 'action-abusefilter-log-private' ) ]
149                        );
150                    }
151                }
152            }
153
154            if ( $foundInvalid ) {
155                // @todo Tell what the invalid IDs are
156                $this->addWarning( 'abusefilter-log-invalid-filter' );
157            }
158        }
159
160        $result = $this->getResult();
161
162        $this->addTables( 'abuse_filter_log' );
163        $this->addFields( 'afl_timestamp' );
164        $this->addFields( 'afl_rev_id' );
165        $this->addFields( 'afl_deleted' );
166        $this->addFields( 'afl_filter_id' );
167        $this->addFields( 'afl_global' );
168        $this->addFieldsIf( 'afl_id', $fld_ids );
169        $this->addFieldsIf( 'afl_user_text', $fld_user );
170        $this->addFieldsIf( [ 'afl_namespace', 'afl_title' ], $fld_title );
171        $this->addFieldsIf( 'afl_action', $fld_action );
172        $this->addFieldsIf( 'afl_var_dump', $fld_details );
173        $this->addFieldsIf( 'afl_actions', $fld_result );
174        $this->addFieldsIf( 'afl_wiki', $fld_wiki );
175
176        if ( $fld_filter ) {
177            $this->addTables( 'abuse_filter' );
178            $this->addFields( 'af_public_comments' );
179
180            $this->addJoinConds( [
181                'abuse_filter' => [
182                    'LEFT JOIN',
183                    [
184                        'af_id=afl_filter_id',
185                        'afl_global' => 0
186                    ]
187                ]
188            ] );
189        }
190
191        $this->addOption( 'LIMIT', $params['limit'] + 1 );
192
193        $this->addWhereIf( [ 'afl_id' => $params['logid'] ], isset( $params['logid'] ) );
194
195        $this->addWhereRange( 'afl_timestamp', $params['dir'], $params['start'], $params['end'] );
196
197        if ( isset( $params['user'] ) ) {
198            $u = User::newFromName( $params['user'] );
199            if ( $u ) {
200                // Username normalisation
201                $params['user'] = $u->getName();
202                $userId = $u->getId();
203            } elseif ( IPUtils::isIPAddress( $params['user'] ) ) {
204                // It's an IP, sanitize it
205                $params['user'] = IPUtils::sanitizeIP( $params['user'] );
206                $userId = 0;
207            }
208
209            if ( isset( $userId ) ) {
210                // Only add the WHERE for user in case it's either a valid user
211                // (but not necessary an existing one) or an IP.
212                $this->addWhere(
213                    [
214                        'afl_user' => $userId,
215                        'afl_user_text' => $params['user']
216                    ]
217                );
218            }
219        }
220
221        $this->addWhereIf( [ 'afl_deleted' => 0 ], !$this->afPermManager->canSeeHiddenLogEntries( $performer ) );
222
223        if ( $searchFilters ) {
224            // @todo Avoid code duplication with SpecialAbuseLog::showList
225            $filterConds = [ 'local' => [], 'global' => [] ];
226            foreach ( $searchFilters as $filter ) {
227                $isGlobal = $filter[1];
228                $key = $isGlobal ? 'global' : 'local';
229                $filterConds[$key][] = $filter[0];
230            }
231            $conds = [];
232            if ( $filterConds['local'] ) {
233                $conds[] = $this->getDB()->makeList(
234                    [ 'afl_global' => 0, 'afl_filter_id' => $filterConds['local'] ],
235                    LIST_AND
236                );
237            }
238            if ( $filterConds['global'] ) {
239                $conds[] = $this->getDB()->makeList(
240                    [ 'afl_global' => 1, 'afl_filter_id' => $filterConds['global'] ],
241                    LIST_AND
242                );
243            }
244            $conds = $this->getDB()->makeList( $conds, LIST_OR );
245
246            $this->addWhere( $conds );
247        }
248
249        if ( isset( $params['wiki'] ) ) {
250            // 'wiki' won't be set if $wgAbuseFilterIsCentral = false
251            $this->addWhereIf( [ 'afl_wiki' => $params['wiki'] ], $isCentral );
252        }
253
254        $title = $params['title'];
255        if ( $title !== null ) {
256            $titleObj = Title::newFromText( $title );
257            if ( $titleObj === null ) {
258                $this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $title ) ] );
259            }
260            $this->addWhereFld( 'afl_namespace', $titleObj->getNamespace() );
261            $this->addWhereFld( 'afl_title', $titleObj->getDBkey() );
262        }
263        $res = $this->select( __METHOD__ );
264
265        $count = 0;
266        foreach ( $res as $row ) {
267            if ( ++$count > $params['limit'] ) {
268                // We've had enough
269                $ts = new MWTimestamp( $row->afl_timestamp );
270                $this->setContinueEnumParameter( 'start', $ts->getTimestamp( TS_ISO_8601 ) );
271                break;
272            }
273            $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager );
274            if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) {
275                continue;
276            }
277
278            $filterID = $row->afl_filter_id;
279            $global = $row->afl_global;
280            $fullName = GlobalNameUtils::buildGlobalName( $filterID, $global );
281            $isHidden = $lookup->getFilter( $filterID, $global )->isHidden();
282            $canSeeDetails = $this->afPermManager->canSeeLogDetailsForFilter( $performer, $isHidden );
283
284            $entry = [];
285            if ( $fld_ids ) {
286                $entry['id'] = intval( $row->afl_id );
287                $entry['filter_id'] = $canSeeDetails ? $fullName : '';
288            }
289            if ( $fld_filter ) {
290                if ( $global ) {
291                    $entry['filter'] = $lookup->getFilter( $filterID, true )->getName();
292                } else {
293                    $entry['filter'] = $row->af_public_comments;
294                }
295            }
296            if ( $fld_user ) {
297                $entry['user'] = $row->afl_user_text;
298            }
299            if ( $fld_wiki ) {
300                $entry['wiki'] = $row->afl_wiki;
301            }
302            if ( $fld_title ) {
303                $title = Title::makeTitle( $row->afl_namespace, $row->afl_title );
304                ApiQueryBase::addTitleInfo( $entry, $title );
305            }
306            if ( $fld_action ) {
307                $entry['action'] = $row->afl_action;
308            }
309            if ( $fld_result ) {
310                $entry['result'] = $row->afl_actions;
311            }
312            if ( $fld_revid && $row->afl_rev_id !== null ) {
313                $entry['revid'] = $canSeeDetails ? (int)$row->afl_rev_id : '';
314            }
315            if ( $fld_timestamp ) {
316                $ts = new MWTimestamp( $row->afl_timestamp );
317                $entry['timestamp'] = $ts->getTimestamp( TS_ISO_8601 );
318            }
319            if ( $fld_details ) {
320                $entry['details'] = [];
321                if ( $canSeeDetails ) {
322                    $vars = $this->afVariablesBlobStore->loadVarDump( $row->afl_var_dump );
323                    $varManager = $this->afVariablesManager;
324                    $entry['details'] = $varManager->exportAllVars( $vars );
325                }
326            }
327
328            if ( $fld_hidden ) {
329                $entry['hidden'] = (bool)$row->afl_deleted;
330            }
331
332            if ( $entry ) {
333                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $entry );
334                if ( !$fit ) {
335                    $ts = new MWTimestamp( $row->afl_timestamp );
336                    $this->setContinueEnumParameter( 'start', $ts->getTimestamp( TS_ISO_8601 ) );
337                    break;
338                }
339            }
340        }
341        $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'item' );
342    }
343
344    /**
345     * @codeCoverageIgnore Merely declarative
346     * @inheritDoc
347     */
348    public function getAllowedParams() {
349        $params = [
350            'logid' => [
351                ParamValidator::PARAM_TYPE => 'integer'
352            ],
353            'start' => [
354                ParamValidator::PARAM_TYPE => 'timestamp'
355            ],
356            'end' => [
357                ParamValidator::PARAM_TYPE => 'timestamp'
358            ],
359            'dir' => [
360                ParamValidator::PARAM_TYPE => [
361                    'newer',
362                    'older'
363                ],
364                ParamValidator::PARAM_DEFAULT => 'older',
365                ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
366            ],
367            'user' => null,
368            'title' => null,
369            'filter' => [
370                ParamValidator::PARAM_TYPE => 'string',
371                ParamValidator::PARAM_ISMULTI => true,
372                ApiBase::PARAM_HELP_MSG => [
373                    'apihelp-query+abuselog-param-filter',
374                    GlobalNameUtils::GLOBAL_FILTER_PREFIX
375                ]
376            ],
377            'limit' => [
378                ParamValidator::PARAM_DEFAULT => 10,
379                ParamValidator::PARAM_TYPE => 'limit',
380                IntegerDef::PARAM_MIN => 1,
381                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
382                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
383            ],
384            'prop' => [
385                ParamValidator::PARAM_DEFAULT => 'ids|user|title|action|result|timestamp|hidden|revid',
386                ParamValidator::PARAM_TYPE => [
387                    'ids',
388                    'filter',
389                    'user',
390                    'title',
391                    'action',
392                    'details',
393                    'result',
394                    'timestamp',
395                    'hidden',
396                    'revid',
397                ],
398                ParamValidator::PARAM_ISMULTI => true
399            ]
400        ];
401        if ( $this->getConfig()->get( 'AbuseFilterIsCentral' ) ) {
402            $params['wiki'] = [
403                ParamValidator::PARAM_TYPE => 'string',
404            ];
405            $params['prop'][ParamValidator::PARAM_DEFAULT] .= '|wiki';
406            $params['prop'][ParamValidator::PARAM_TYPE][] = 'wiki';
407            $params['filter'][ApiBase::PARAM_HELP_MSG] = 'apihelp-query+abuselog-param-filter-central';
408        }
409        return $params;
410    }
411
412    /**
413     * @codeCoverageIgnore Merely declarative
414     * @inheritDoc
415     */
416    protected function getExamplesMessages() {
417        return [
418            'action=query&list=abuselog'
419                => 'apihelp-query+abuselog-example-1',
420            'action=query&list=abuselog&afltitle=API'
421                => 'apihelp-query+abuselog-example-2',
422        ];
423    }
424}