Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
46 / 48
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbuseFilterChecker
95.83% covered (success)
95.83%
46 / 48
50.00% covered (danger)
50.00%
2 / 4
11
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 checkTitleForUser
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 checkSectionForTitleAndUser
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 getResults
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare( strict_types = 1 );
3
4namespace ContentTranslation;
5
6use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup;
7use MediaWiki\Extension\AbuseFilter\FilterLookup;
8use MediaWiki\Extension\AbuseFilter\FilterRunnerFactory;
9use MediaWiki\Extension\AbuseFilter\GlobalNameUtils;
10use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory;
11use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
12use MediaWiki\Page\WikiPageFactory;
13use MediaWiki\Title\Title;
14use MediaWiki\User\User;
15
16/**
17 * Utility class for checking AbuseFilter rules before publishing.
18 *
19 * The results from these methods return an array, where the keys are
20 * the public names of the rules, and values are arrays consisting of
21 * different actions the rules would cause. Those can be tag, warn,
22 * disallow and others. Example:
23 * @code
24 * array = [
25 *   'rule1' => [
26 *     'warn' => [
27 *       'action' => 'warn',
28 *       'parameters' => [ 'abusefilter-warning' ]
29 *     ]
30 *   ]
31 * ];
32 * @endcode
33 *
34 * With type 'warn' there is also warning_html for html warning message.
35 *
36 * @copyright See AUTHORS.txt
37 * @license GPL-2.0-or-later
38 */
39class AbuseFilterChecker {
40    private ?VariableGeneratorFactory $variableGeneratorFactory;
41
42    private bool $isAbuseFilterExtensionLoaded;
43
44    private WikiPageFactory $wikiPageFactory;
45
46    private ?ConsequencesLookup $consequencesLookup;
47
48    private ?FilterLookup $filterLookup;
49
50    private ?FilterRunnerFactory $filterRunnerFactory;
51
52    public function __construct(
53        $isAbuseFilterExtensionLoaded,
54        WikiPageFactory $wikiPageFactory,
55        ?VariableGeneratorFactory $variableGeneratorFactory,
56        ?ConsequencesLookup $consequencesLookup,
57        ?FilterLookup $filterLookup,
58        ?FilterRunnerFactory $filterRunnerFactory
59    ) {
60        $this->isAbuseFilterExtensionLoaded = $isAbuseFilterExtensionLoaded;
61        $this->wikiPageFactory = $wikiPageFactory;
62        $this->variableGeneratorFactory = $variableGeneratorFactory;
63        $this->consequencesLookup = $consequencesLookup;
64        $this->filterLookup = $filterLookup;
65        $this->filterRunnerFactory = $filterRunnerFactory;
66    }
67
68    /**
69     * Check a title for any rule violations.
70     *
71     * @param Title $title Title to check
72     * @param User $user User performing the action
73     *
74     * @return array List of any rule violations
75     */
76    public function checkTitleForUser( Title $title, User $user ): array {
77        if ( !$this->isAbuseFilterExtensionLoaded ) {
78            return [];
79        }
80
81        $gen = $this->variableGeneratorFactory->newGenerator();
82        $vars = $gen
83            ->addUserVars( $user )
84            ->addTitleVars( $title, 'page' )
85            ->addGenericVars()
86            ->getVariableHolder();
87        $vars->setVar( 'action', 'edit' );
88
89        return $this->getResults( $user, $title, $vars );
90    }
91
92    /**
93     * Check some text for rule violations.
94     *
95     * @param User $user
96     * @param Title $title
97     * @param string|null $text Text to check
98     * @return array List of any rule violations
99     */
100    public function checkSectionForTitleAndUser( User $user, Title $title, ?string $text ): array {
101        if ( !$this->isAbuseFilterExtensionLoaded ) {
102            return [];
103        }
104
105        if ( $text === null || mb_strlen( $text ) < 150 ) {
106            // Don't validate sections that are too short. The validations
107            // happen while editing is going on.
108            return [];
109        }
110
111        // Add AbuseFilter variables. Note that we are adding the title
112        // here. That will cause filters about titles executed for every section.
113        // But not passing title will cause content filters with namespace rules
114        // not to produce results. We will attempt to filter out title errors
115        // away with array_diff_key.
116        $gen = $this->variableGeneratorFactory->newGenerator();
117        $vars = $gen
118            ->addUserVars( $user )
119            ->addTitleVars( $title, 'page' )
120            ->addEditVars( $this->wikiPageFactory->newFromTitle( $title ), $user )
121            ->addGenericVars()
122            ->getVariableHolder();
123        $vars->setVar( 'action', 'edit' );
124        $vars->setVar( 'old_wikitext', '' );
125        $vars->setVar( 'new_wikitext', $text );
126
127        $results = $this->getResults( $user, $title, $vars );
128        return array_diff_key( $results, $this->checkTitleForUser( $title, $user ) );
129    }
130
131    private function getResults( User $user, Title $title, VariableHolder $vars ): array {
132        static $seriousActions = [ 'warn', 'block', 'disallow', 'degroup' ];
133
134        $runner = $this->filterRunnerFactory->newRunner( $user, $title, $vars, 'default' );
135        $filters = $runner->checkAllFilters();
136
137        $filters = array_keys( array_filter( $filters ) );
138        $actions = $this->consequencesLookup->getConsequencesForFilters( $filters );
139
140        $results = [];
141        foreach ( $actions as $key => $val ) {
142            [ $filterID, $isGlobal ] = GlobalNameUtils::splitGlobalName( $key );
143            $rulename = $this->filterLookup->getFilter( $filterID, $isGlobal )->getName();
144
145            // No point alerting the user about non-serious actions. T136596
146            $actionsForRule = array_keys( $val );
147            if ( array_intersect( $seriousActions, $actionsForRule ) === [] ) {
148                continue;
149            }
150
151            if ( isset( $val['warn'][0] ) ) {
152                $val['warn']['messageHtml'] = wfMessage( $val['warn'][0] )->params( $rulename )->parse();
153            }
154
155            $results[$key] = $val;
156        }
157
158        return $results;
159    }
160}