Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
KeywordsManager
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
11 / 11
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisabledVariables
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeprecatedVariables
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getBuilderValues
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isVarDisabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isVarDeprecated
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isVarInUse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 varExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getMessageKeyForVar
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getVarsMappings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCoreVariables
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter;
4
5use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
6
7/**
8 * This service can be used to manage the list of keywords recognized by the Parser
9 */
10class KeywordsManager {
11    public const SERVICE_NAME = 'AbuseFilterKeywordsManager';
12
13    private const BUILDER_VALUES = [
14        'op-arithmetic' => [
15            '+' => 'addition',
16            '-' => 'subtraction',
17            '*' => 'multiplication',
18            '/' => 'divide',
19            '%' => 'modulo',
20            '**' => 'pow'
21        ],
22        'op-comparison' => [
23            '==' => 'equal',
24            '===' => 'equal-strict',
25            '!=' => 'notequal',
26            '!==' => 'notequal-strict',
27            '<' => 'lt',
28            '>' => 'gt',
29            '<=' => 'lte',
30            '>=' => 'gte'
31        ],
32        'op-bool' => [
33            '!' => 'not',
34            '&' => 'and',
35            '|' => 'or',
36            '^' => 'xor'
37        ],
38        'misc' => [
39            'in' => 'in',
40            'contains' => 'contains',
41            'like' => 'like',
42            '""' => 'stringlit',
43            'rlike' => 'rlike',
44            'irlike' => 'irlike',
45            'cond ? iftrue : iffalse' => 'tern',
46            'if cond then iftrue else iffalse end' => 'cond',
47            'if cond then iftrue end' => 'cond-short',
48        ],
49        'funcs' => [
50            'length(string)' => 'length',
51            'lcase(string)' => 'lcase',
52            'ucase(string)' => 'ucase',
53            'ccnorm(string)' => 'ccnorm',
54            'ccnorm_contains_any(haystack,needle1,needle2,..)' => 'ccnorm-contains-any',
55            'ccnorm_contains_all(haystack,needle1,needle2,..)' => 'ccnorm-contains-all',
56            'rmdoubles(string)' => 'rmdoubles',
57            'specialratio(string)' => 'specialratio',
58            'norm(string)' => 'norm',
59            'count(needle,haystack)' => 'count',
60            'rcount(needle,haystack)' => 'rcount',
61            'get_matches(needle,haystack)' => 'get_matches',
62            'rmwhitespace(text)' => 'rmwhitespace',
63            'rmspecials(text)' => 'rmspecials',
64            'ip_in_range(ip, range)' => 'ip_in_range',
65            'ip_in_ranges(ip, range1, range2, ...)' => 'ip_in_ranges',
66            'contains_any(haystack,needle1,needle2,...)' => 'contains-any',
67            'contains_all(haystack,needle1,needle2,...)' => 'contains-all',
68            'equals_to_any(haystack,needle1,needle2,...)' => 'equals-to-any',
69            'substr(subject, offset, length)' => 'substr',
70            'strpos(haystack, needle)' => 'strpos',
71            'str_replace(subject, search, replace)' => 'str_replace',
72            'str_replace_regexp(subject, search, replace)' => 'str_replace_regexp',
73            'rescape(string)' => 'rescape',
74            'set_var(var,value)' => 'set_var',
75            'sanitize(string)' => 'sanitize',
76        ],
77        'vars' => [
78            'timestamp' => 'timestamp',
79            'accountname' => 'accountname',
80            'action' => 'action',
81            'added_lines' => 'addedlines',
82            'edit_delta' => 'delta',
83            'edit_diff' => 'diff',
84            'new_size' => 'newsize',
85            'old_size' => 'oldsize',
86            'new_content_model' => 'new-content-model',
87            'old_content_model' => 'old-content-model',
88            'removed_lines' => 'removedlines',
89            'summary' => 'summary',
90            'page_id' => 'page-id',
91            'page_namespace' => 'page-ns',
92            'page_title' => 'page-title',
93            'page_prefixedtitle' => 'page-prefixedtitle',
94            'page_age' => 'page-age',
95            'moved_from_id' => 'movedfrom-id',
96            'moved_from_namespace' => 'movedfrom-ns',
97            'moved_from_title' => 'movedfrom-title',
98            'moved_from_prefixedtitle' => 'movedfrom-prefixedtitle',
99            'moved_from_age' => 'movedfrom-age',
100            'moved_to_id' => 'movedto-id',
101            'moved_to_namespace' => 'movedto-ns',
102            'moved_to_title' => 'movedto-title',
103            'moved_to_prefixedtitle' => 'movedto-prefixedtitle',
104            'moved_to_age' => 'movedto-age',
105            'user_editcount' => 'user-editcount',
106            'user_age' => 'user-age',
107            'user_name' => 'user-name',
108            'user_groups' => 'user-groups',
109            'user_rights' => 'user-rights',
110            'user_blocked' => 'user-blocked',
111            'user_emailconfirm' => 'user-emailconfirm',
112            'old_wikitext' => 'old-wikitext',
113            'new_wikitext' => 'new-wikitext',
114            'added_links' => 'added-links',
115            'removed_links' => 'removed-links',
116            'all_links' => 'all-links',
117            'new_pst' => 'new-pst',
118            'edit_diff_pst' => 'diff-pst',
119            'added_lines_pst' => 'addedlines-pst',
120            'new_text' => 'new-text',
121            'new_html' => 'new-html',
122            'page_restrictions_edit' => 'restrictions-edit',
123            'page_restrictions_move' => 'restrictions-move',
124            'page_restrictions_create' => 'restrictions-create',
125            'page_restrictions_upload' => 'restrictions-upload',
126            'page_recent_contributors' => 'recent-contributors',
127            'page_first_contributor' => 'first-contributor',
128            'moved_from_restrictions_edit' => 'movedfrom-restrictions-edit',
129            'moved_from_restrictions_move' => 'movedfrom-restrictions-move',
130            'moved_from_restrictions_create' => 'movedfrom-restrictions-create',
131            'moved_from_restrictions_upload' => 'movedfrom-restrictions-upload',
132            'moved_from_recent_contributors' => 'movedfrom-recent-contributors',
133            'moved_from_first_contributor' => 'movedfrom-first-contributor',
134            'moved_to_restrictions_edit' => 'movedto-restrictions-edit',
135            'moved_to_restrictions_move' => 'movedto-restrictions-move',
136            'moved_to_restrictions_create' => 'movedto-restrictions-create',
137            'moved_to_restrictions_upload' => 'movedto-restrictions-upload',
138            'moved_to_recent_contributors' => 'movedto-recent-contributors',
139            'moved_to_first_contributor' => 'movedto-first-contributor',
140            'old_links' => 'old-links',
141            'file_sha1' => 'file-sha1',
142            'file_size' => 'file-size',
143            'file_mime' => 'file-mime',
144            'file_mediatype' => 'file-mediatype',
145            'file_width' => 'file-width',
146            'file_height' => 'file-height',
147            'file_bits_per_channel' => 'file-bits-per-channel',
148            'wiki_name' => 'wiki-name',
149            'wiki_language' => 'wiki-language',
150        ],
151    ];
152
153    /** @var array Old vars which aren't in use anymore */
154    private const DISABLED_VARS = [
155        'old_text' => 'old-text',
156        'old_html' => 'old-html',
157        'minor_edit' => 'minor-edit'
158    ];
159
160    private const DEPRECATED_VARS = [
161        'article_text' => 'page_title',
162        'article_prefixedtext' => 'page_prefixedtitle',
163        'article_namespace' => 'page_namespace',
164        'article_articleid' => 'page_id',
165        'article_restrictions_edit' => 'page_restrictions_edit',
166        'article_restrictions_move' => 'page_restrictions_move',
167        'article_restrictions_create' => 'page_restrictions_create',
168        'article_restrictions_upload' => 'page_restrictions_upload',
169        'article_recent_contributors' => 'page_recent_contributors',
170        'article_first_contributor' => 'page_first_contributor',
171        'moved_from_text' => 'moved_from_title',
172        'moved_from_prefixedtext' => 'moved_from_prefixedtitle',
173        'moved_from_articleid' => 'moved_from_id',
174        'moved_to_text' => 'moved_to_title',
175        'moved_to_prefixedtext' => 'moved_to_prefixedtitle',
176        'moved_to_articleid' => 'moved_to_id',
177    ];
178
179    /** @var string[][] Final list of builder values */
180    private $builderValues;
181
182    /** @var string[] Final list of deprecated vars */
183    private $deprecatedVars;
184
185    /** @var AbuseFilterHookRunner */
186    private $hookRunner;
187
188    /**
189     * @param AbuseFilterHookRunner $hookRunner
190     */
191    public function __construct( AbuseFilterHookRunner $hookRunner ) {
192        $this->hookRunner = $hookRunner;
193    }
194
195    /**
196     * @return array
197     */
198    public function getDisabledVariables(): array {
199        return self::DISABLED_VARS;
200    }
201
202    /**
203     * @return array
204     */
205    public function getDeprecatedVariables(): array {
206        if ( $this->deprecatedVars === null ) {
207            $this->deprecatedVars = self::DEPRECATED_VARS;
208            $this->hookRunner->onAbuseFilter_deprecatedVariables( $this->deprecatedVars );
209        }
210        return $this->deprecatedVars;
211    }
212
213    /**
214     * @return array
215     */
216    public function getBuilderValues(): array {
217        if ( $this->builderValues === null ) {
218            $this->builderValues = self::BUILDER_VALUES;
219            $this->hookRunner->onAbuseFilter_builder( $this->builderValues );
220        }
221        return $this->builderValues;
222    }
223
224    /**
225     * @param string $name
226     * @return bool
227     */
228    public function isVarDisabled( string $name ): bool {
229        return array_key_exists( $name, self::DISABLED_VARS );
230    }
231
232    /**
233     * @param string $name
234     * @return bool
235     */
236    public function isVarDeprecated( string $name ): bool {
237        return array_key_exists( $name, $this->getDeprecatedVariables() );
238    }
239
240    /**
241     * @param string $name
242     * @return bool
243     */
244    public function isVarInUse( string $name ): bool {
245        return array_key_exists( $name, $this->getVarsMappings() );
246    }
247
248    /**
249     * Check whether the given name corresponds to a known variable.
250     * @param string $name
251     * @return bool
252     */
253    public function varExists( string $name ): bool {
254        return $this->isVarInUse( $name ) ||
255            $this->isVarDisabled( $name ) ||
256            $this->isVarDeprecated( $name );
257    }
258
259    /**
260     * Get the message for a builtin variable; takes deprecated variables into account.
261     * Returns null for non-builtin variables.
262     *
263     * @param string $var
264     * @return string|null
265     */
266    public function getMessageKeyForVar( string $var ): ?string {
267        if ( !$this->varExists( $var ) ) {
268            return null;
269        }
270        if ( $this->isVarDeprecated( $var ) ) {
271            $var = $this->getDeprecatedVariables()[$var];
272        }
273
274        $key = self::DISABLED_VARS[$var] ??
275            $this->getVarsMappings()[$var];
276        return "abusefilter-edit-builder-vars-$key";
277    }
278
279    /**
280     * @return array
281     */
282    public function getVarsMappings(): array {
283        return $this->getBuilderValues()['vars'];
284    }
285
286    /**
287     * Get a list of core variables, i.e. variables defined in AbuseFilter (ignores hooks).
288     * You usually want to use getVarsMappings(), not this one.
289     * @return string[]
290     */
291    public function getCoreVariables(): array {
292        return array_keys( self::BUILDER_VALUES['vars'] );
293    }
294}