Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.24% covered (success)
93.24%
69 / 74
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckMatch
93.24% covered (success)
93.24%
69 / 74
50.00% covered (danger)
50.00%
1 / 2
26.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 execute
93.15% covered (success)
93.15%
68 / 73
0.00% covered (danger)
0.00%
0 / 1
23.17
 getAllowedParams
n/a
0 / 0
n/a
0 / 0
1
 getExamplesMessages
n/a
0 / 0
n/a
0 / 0
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Api;
4
5use LogicException;
6use MediaWiki\Api\ApiBase;
7use MediaWiki\Api\ApiMain;
8use MediaWiki\Api\ApiResult;
9use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
10use MediaWiki\Extension\AbuseFilter\AbuseLoggerFactory;
11use MediaWiki\Extension\AbuseFilter\FilterLookup;
12use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory;
13use MediaWiki\Extension\AbuseFilter\Special\SpecialAbuseLog;
14use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
15use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
16use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
17use MediaWiki\Extension\AbuseFilter\Variables\VariablesBlobStore;
18use MediaWiki\Json\FormatJson;
19use MediaWiki\RecentChanges\RecentChangeLookup;
20use Wikimedia\ParamValidator\ParamValidator;
21use Wikimedia\Rdbms\ReadOnlyMode;
22
23class CheckMatch extends ApiBase {
24
25    public function __construct(
26        ApiMain $main,
27        string $action,
28        private readonly RuleCheckerFactory $ruleCheckerFactory,
29        private readonly AbuseFilterPermissionManager $afPermManager,
30        private readonly VariablesBlobStore $afVariablesBlobStore,
31        private readonly VariableGeneratorFactory $afVariableGeneratorFactory,
32        private readonly FilterLookup $filterLookup,
33        private readonly AbuseLoggerFactory $abuseLoggerFactory,
34        private readonly RecentChangeLookup $recentChangeLookup,
35        private readonly ReadOnlyMode $readOnlyMode,
36    ) {
37        parent::__construct( $main, $action );
38    }
39
40    /**
41     * @inheritDoc
42     */
43    public function execute() {
44        $performer = $this->getAuthority();
45        $params = $this->extractRequestParams();
46        $this->requireOnlyOneParameter( $params, 'vars', 'rcid', 'logid' );
47
48        // "Anti-DoS"
49        if ( !$this->afPermManager->canUseTestTools( $performer ) ) {
50            $this->dieWithError( 'apierror-abusefilter-canttest', 'permissiondenied' );
51        }
52
53        $vars = null;
54        if ( $params['vars'] ) {
55            $pairs = FormatJson::decode( $params['vars'], true );
56            $vars = VariableHolder::newFromArray( $pairs );
57        } elseif ( $params['rcid'] ) {
58            $rc = $this->recentChangeLookup->getRecentChangeById( $params['rcid'] );
59
60            if ( !$rc ) {
61                $this->dieWithError( [ 'apierror-nosuchrcid', $params['rcid'] ] );
62            }
63
64            if ( !$this->afPermManager::hasRCEntryAccess( $rc, $performer ) ) {
65                // T223654 - Same check as in AbuseFilterChangesList
66                $this->dieWithError( 'apierror-permissiondenied-generic', 'deletedrc' );
67            }
68
69            $varGenerator = $this->afVariableGeneratorFactory->newRCGenerator( $rc, $this->getUser() );
70            $vars = $varGenerator->getVars();
71            if ( $vars === null ) {
72                $this->dieWithError( 'apierror-abusefilter-incompatible' );
73            }
74        } elseif ( $params['logid'] ) {
75            $row = $this->getDB()->newSelectQueryBuilder()
76                ->select( '*' )
77                ->from( 'abuse_filter_log' )
78                ->where( [ 'afl_id' => $params['logid'] ] )
79                ->caller( __METHOD__ )
80                ->fetchRow();
81
82            if ( !$row ) {
83                $this->dieWithError( [ 'apierror-abusefilter-nosuchlogid', $params['logid'] ], 'nosuchlogid' );
84            }
85
86            $filter = $this->filterLookup->getFilter( $row->afl_filter_id, $row->afl_global );
87            $canSeeDetails = $this->afPermManager->canSeeLogDetailsForFilter( $performer, $filter );
88            if ( !$canSeeDetails ) {
89                $this->dieWithError( 'apierror-permissiondenied-generic', 'cannotseedetails' );
90            }
91
92            $visibility = SpecialAbuseLog::getEntryVisibilityForUser( $row, $performer, $this->afPermManager );
93            if ( $visibility !== SpecialAbuseLog::VISIBILITY_VISIBLE ) {
94                // T223654 - Same check as in SpecialAbuseLog. Both the visibility of the AbuseLog entry
95                // and the corresponding revision are checked.
96                $this->dieWithError( 'apierror-permissiondenied-generic', 'deletedabuselog' );
97            }
98
99            $vars = $this->afVariablesBlobStore->loadVarDump( $row );
100
101            // Check that the user can see all the protected variables in the abuse_filter_log log.
102            if ( $filter->isProtected() ) {
103                $permStatus = $this->afPermManager->canViewProtectedVariables(
104                    $this->getAuthority(), array_keys( $vars->getVars() )
105                );
106                if ( !$permStatus->isGood() ) {
107                    $this->dieWithError( 'apierror-permissiondenied-generic', 'cannotseedetails' );
108                }
109            }
110        }
111        if ( $vars === null ) {
112            // @codeCoverageIgnoreStart
113            throw new LogicException( 'Variables were not loaded, this should not happen.' );
114            // @codeCoverageIgnoreEnd
115        }
116
117        $ruleChecker = $this->ruleCheckerFactory->newRuleChecker( $vars );
118        if ( !$ruleChecker->checkSyntax( $params['filter'] )->isValid() ) {
119            $this->dieWithError( 'apierror-abusefilter-badsyntax', 'badsyntax' );
120        }
121
122        // Check if the provided pattern uses protected variables. If it does, then refuse to check the pattern
123        // if the user cannot see the used protected variables. This prevents matching against protected variables
124        // generated when providing 'rcid' and is a fail-safe in the same way for 'logid'.
125        $usedVars = $ruleChecker->getUsedVars( $params['filter'] );
126        if ( $this->afPermManager->getForbiddenVariables( $this->getAuthority(), $usedVars ) ) {
127            $this->dieWithError( 'apierror-permissiondenied-generic', 'cannotseeprotectedvariables' );
128        }
129
130        $result = [
131            ApiResult::META_BC_BOOLS => [ 'result' ],
132            'result' => $ruleChecker->checkConditions( $params['filter'] )->getResult(),
133        ];
134
135        // If the test filter pattern contains protected variables and this entry had a value set for the
136        // protected variables that were in the pattern, then log that protected variables were accessed.
137        // This is to avoid a user being able to know the value of the variable if they repeatedly try values to
138        // find the actual value through trial-and-error.
139        $protectedVariableValuesShown = [];
140        foreach ( $this->afPermManager->getUsedProtectedVariables( $usedVars ) as $protectedVariable ) {
141            if ( $vars->varIsSet( $protectedVariable ) ) {
142                $protectedVariableValue = $vars->getVarThrow( $protectedVariable );
143                if (
144                    !( $protectedVariableValue instanceof LazyLoadedVariable ) &&
145                    $protectedVariableValue->toNative() !== null
146                ) {
147                    $protectedVariableValuesShown[] = $protectedVariable;
148                }
149            }
150        }
151
152        if ( count( $protectedVariableValuesShown ) ) {
153            if ( $this->readOnlyMode->isReadOnly() ) {
154                $this->dieReadOnly();
155            }
156
157            // Either 'user_name' or 'account_name' should be set which are not lazily loaded, so get one of
158            // them to use as the target
159            if ( $vars->varIsSet( 'user_name' ) ) {
160                $target = $vars->getComputedVariable( 'user_name' )->toNative();
161            } else {
162                $target = $vars->getComputedVariable( 'account_name' )->toNative();
163            }
164            $logger = $this->abuseLoggerFactory->getProtectedVarsAccessLogger();
165            $logger->logViewProtectedVariableValue( $this->getUser(), $target, $protectedVariableValuesShown );
166        }
167
168        $this->getResult()->addValue(
169            null,
170            $this->getModuleName(),
171            $result
172        );
173    }
174
175    /**
176     * @codeCoverageIgnore Merely declarative
177     * @inheritDoc
178     */
179    public function getAllowedParams() {
180        return [
181            'filter' => [
182                ParamValidator::PARAM_REQUIRED => true,
183            ],
184            'vars' => null,
185            'rcid' => [
186                ParamValidator::PARAM_TYPE => 'integer'
187            ],
188            'logid' => [
189                ParamValidator::PARAM_TYPE => 'integer'
190            ],
191        ];
192    }
193
194    /**
195     * @codeCoverageIgnore Merely declarative
196     * @inheritDoc
197     */
198    protected function getExamplesMessages() {
199        return [
200            'action=abusefiltercheckmatch&filter=!("autoconfirmed"%20in%20user_groups)&rcid=15'
201                => 'apihelp-abusefiltercheckmatch-example-1',
202        ];
203    }
204}