Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.68% covered (success)
98.68%
149 / 151
75.00% covered (warning)
75.00%
6 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
VariableGenerator
98.68% covered (success)
98.68%
149 / 151
75.00% covered (warning)
75.00%
6 / 8
15
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
 getVariableHolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addGenericVars
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
 addUserVars
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
2
 addTitleVars
96.77% covered (success)
96.77%
30 / 31
0.00% covered (danger)
0.00%
0 / 1
5
 addDerivedEditVars
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
1
 addEditVarsFromUpdate
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 addEditVars
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\VariableGenerator;
4
5use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner;
6use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
7use MediaWiki\Page\WikiPage;
8use MediaWiki\RecentChanges\RecentChange;
9use MediaWiki\Storage\PreparedUpdate;
10use MediaWiki\Title\Title;
11use MediaWiki\User\User;
12use MediaWiki\User\UserFactory;
13use MediaWiki\User\UserIdentity;
14use MediaWiki\Utils\MWTimestamp;
15
16/**
17 * Class used to generate variables, for instance related to a given user or title.
18 */
19class VariableGenerator {
20    /** @var VariableHolder */
21    protected $vars;
22
23    /**
24     * @param AbuseFilterHookRunner $hookRunner
25     * @param UserFactory $userFactory
26     * @param VariableHolder|null $vars
27     */
28    public function __construct(
29        protected readonly AbuseFilterHookRunner $hookRunner,
30        protected readonly UserFactory $userFactory,
31        ?VariableHolder $vars = null
32    ) {
33        $this->vars = $vars ?? new VariableHolder();
34    }
35
36    public function getVariableHolder(): VariableHolder {
37        return $this->vars;
38    }
39
40    /**
41     * Computes all variables unrelated to title and user. In general, these variables may be known
42     * even without an ongoing action.
43     *
44     * @param RecentChange|null $rc If the variables should be generated for an RC entry,
45     *   this is the entry. Null if it's for the current action being filtered.
46     * @return $this For chaining
47     */
48    public function addGenericVars( ?RecentChange $rc = null ): self {
49        $timestamp = $rc
50            ? MWTimestamp::convert( TS_UNIX, $rc->getAttribute( 'rc_timestamp' ) )
51            : wfTimestamp( TS_UNIX );
52        $this->vars->setVar( 'timestamp', $timestamp );
53        // These are lazy-loaded just to reduce the amount of preset variables, but they
54        // shouldn't be expensive.
55        $this->vars->setLazyLoadVar( 'wiki_name', 'get-wiki-name', [] );
56        $this->vars->setLazyLoadVar( 'wiki_language', 'get-wiki-language', [] );
57
58        $this->hookRunner->onAbuseFilter_generateGenericVars( $this->vars, $rc );
59        return $this;
60    }
61
62    /**
63     * @param UserIdentity $userIdentity
64     * @param RecentChange|null $rc If the variables should be generated for an RC entry,
65     *   this is the entry. Null if it's for the current action being filtered.
66     * @return $this For chaining
67     */
68    public function addUserVars( UserIdentity $userIdentity, ?RecentChange $rc = null ): self {
69        $asOf = $rc ? $rc->getAttribute( 'rc_timestamp' ) : wfTimestampNow();
70        $user = $this->userFactory->newFromUserIdentity( $userIdentity );
71
72        $this->vars->setLazyLoadVar(
73            'user_editcount',
74            'user-editcount',
75            [ 'user-identity' => $userIdentity ]
76        );
77
78        $this->vars->setVar( 'user_name', $user->getName() );
79
80        $this->vars->setLazyLoadVar(
81            'user_unnamed_ip',
82            'user-unnamed-ip',
83            [
84                'user' => $user,
85                'rc' => $rc,
86            ]
87        );
88
89        $this->vars->setLazyLoadVar(
90            'user_type',
91            'user-type',
92            [ 'user-identity' => $userIdentity ]
93        );
94
95        $this->vars->setLazyLoadVar(
96            'user_emailconfirm',
97            'user-emailconfirm',
98            [ 'user' => $user ]
99        );
100
101        $this->vars->setLazyLoadVar(
102            'user_age',
103            'user-age',
104            [ 'user' => $user, 'asof' => $asOf ]
105        );
106
107        $this->vars->setLazyLoadVar(
108            'user_groups',
109            'user-groups',
110            [ 'user-identity' => $userIdentity ]
111        );
112
113        $this->vars->setLazyLoadVar(
114            'user_rights',
115            'user-rights',
116            [ 'user-identity' => $userIdentity ]
117        );
118
119        $this->vars->setLazyLoadVar(
120            'user_blocked',
121            'user-block',
122            [ 'user' => $user ]
123        );
124
125        $this->hookRunner->onAbuseFilter_generateUserVars( $this->vars, $user, $rc );
126
127        return $this;
128    }
129
130    /**
131     * @param Title $title
132     * @param string $prefix
133     * @param RecentChange|null $rc If the variables should be generated for an RC entry,
134     *   this is the entry. Null if it's for the current action being filtered.
135     * @return $this For chaining
136     */
137    public function addTitleVars(
138        Title $title,
139        string $prefix,
140        ?RecentChange $rc = null
141    ): self {
142        if ( $rc && $rc->getAttribute( 'rc_source' ) === RecentChange::SRC_NEW ) {
143            $this->vars->setVar( $prefix . '_id', 0 );
144        } else {
145            $this->vars->setVar( $prefix . '_id', $title->getArticleID() );
146        }
147        $this->vars->setVar( $prefix . '_namespace', $title->getNamespace() );
148        $this->vars->setVar( $prefix . '_title', $title->getText() );
149        $this->vars->setVar( $prefix . '_prefixedtitle', $title->getPrefixedText() );
150
151        // We only support the default values in $wgRestrictionTypes. Custom restrictions wouldn't
152        // have i18n messages. If a restriction is not enabled we'll just return the empty array.
153        $types = [ 'edit', 'move', 'create', 'upload' ];
154        foreach ( $types as $action ) {
155            $this->vars->setLazyLoadVar(
156                "{$prefix}_restrictions_$action",
157                'get-page-restrictions',
158                [ 'title' => $title, 'action' => $action ]
159            );
160        }
161
162        $asOf = $rc ? $rc->getAttribute( 'rc_timestamp' ) : wfTimestampNow();
163
164        // TODO: add 'asof' to this as well
165        $this->vars->setLazyLoadVar(
166            "{$prefix}_recent_contributors",
167            'load-recent-authors',
168            [ 'title' => $title ]
169        );
170
171        $this->vars->setLazyLoadVar(
172            "{$prefix}_age",
173            'page-age',
174            [ 'title' => $title, 'asof' => $asOf ]
175        );
176
177        $this->vars->setLazyLoadVar(
178            "{$prefix}_first_contributor",
179            'load-first-author',
180            [ 'title' => $title ]
181        );
182
183        $this->hookRunner->onAbuseFilter_generateTitleVars( $this->vars, $title, $prefix, $rc );
184
185        return $this;
186    }
187
188    public function addDerivedEditVars(): self {
189        $this->vars->setLazyLoadVar( 'edit_diff', 'diff',
190            [ 'oldtext-var' => 'old_wikitext', 'newtext-var' => 'new_wikitext' ] );
191        $this->vars->setLazyLoadVar( 'edit_diff_pst', 'diff',
192            [ 'oldtext-var' => 'old_wikitext', 'newtext-var' => 'new_pst' ] );
193        $this->vars->setLazyLoadVar( 'new_size', 'length', [ 'length-var' => 'new_wikitext' ] );
194        $this->vars->setLazyLoadVar( 'old_size', 'length', [ 'length-var' => 'old_wikitext' ] );
195        $this->vars->setLazyLoadVar( 'edit_delta', 'subtract-int',
196            [ 'val1-var' => 'new_size', 'val2-var' => 'old_size' ] );
197
198        // Some more specific/useful details about the changes.
199        $this->vars->setLazyLoadVar( 'added_lines', 'diff-split',
200            [ 'diff-var' => 'edit_diff', 'line-prefix' => '+' ] );
201        $this->vars->setLazyLoadVar( 'removed_lines', 'diff-split',
202            [ 'diff-var' => 'edit_diff', 'line-prefix' => '-' ] );
203        $this->vars->setLazyLoadVar( 'added_lines_pst', 'diff-split',
204            [ 'diff-var' => 'edit_diff_pst', 'line-prefix' => '+' ] );
205
206        // Links
207        $this->vars->setLazyLoadVar( 'added_links', 'array-diff',
208            [ 'base-var' => 'new_links', 'minus-var' => 'old_links' ] );
209        $this->vars->setLazyLoadVar( 'removed_links', 'array-diff',
210            [ 'base-var' => 'old_links', 'minus-var' => 'new_links' ] );
211
212        // Text
213        $this->vars->setLazyLoadVar( 'new_text', 'strip-html',
214            [ 'html-var' => 'new_html' ] );
215
216        return $this;
217    }
218
219    /**
220     * Add variables for an edit action when a PreparedUpdate instance is available.
221     * This is equivalent to ::addEditVars, and the preferred method.
222     *
223     * @param PreparedUpdate $update
224     * @param User $contextUser
225     * @return $this For chaining
226     */
227    public function addEditVarsFromUpdate( PreparedUpdate $update, User $contextUser ): self {
228        $this->addDerivedEditVars();
229
230        $this->vars->setLazyLoadVar( 'new_links', 'links-from-update',
231            [ 'update' => $update ] );
232        $this->vars->setLazyLoadVar( 'old_links', 'links-from-database',
233            [ 'article' => $update->getPage() ] );
234        $this->vars->setLazyLoadVar( 'new_pst', 'pst-from-update',
235            [ 'update' => $update, 'contextUser' => $contextUser ] );
236        $this->vars->setLazyLoadVar( 'new_html', 'html-from-update',
237            [ 'update' => $update ] );
238
239        return $this;
240    }
241
242    /**
243     * Add variables for an edit action. The method assumes that old_wikitext and new_wikitext
244     * will have been set prior to filter execution.
245     *
246     * @note This is a legacy method. Code using it likely relies on legacy hooks.
247     *
248     * @param WikiPage $page
249     * @param UserIdentity $userIdentity The current user
250     * @param bool $linksFromDatabase Whether links variables should be loaded
251     *   from the database. If set to false, they will be parsed from the text variables.
252     * @return $this For chaining
253     */
254    public function addEditVars(
255        WikiPage $page,
256        UserIdentity $userIdentity,
257        bool $linksFromDatabase = true
258    ): self {
259        $this->addDerivedEditVars();
260
261        $this->vars->setLazyLoadVar( 'new_links', 'links-from-wikitext',
262            [
263                'text-var' => 'new_wikitext',
264                'article' => $page,
265                // XXX: this has never made sense
266                'forFilter' => $linksFromDatabase,
267                'contextUserIdentity' => $userIdentity
268            ] );
269
270        if ( $linksFromDatabase ) {
271            $this->vars->setLazyLoadVar( 'old_links', 'links-from-database',
272                [ 'article' => $page ] );
273        } else {
274            // Note: this claims "or database" but it will never reach it
275            $this->vars->setLazyLoadVar( 'old_links', 'links-from-wikitext-or-database',
276                [
277                    'article' => $page,
278                    'text-var' => 'old_wikitext',
279                    'contextUserIdentity' => $userIdentity
280                ] );
281        }
282
283        $this->vars->setLazyLoadVar( 'new_pst', 'parse-wikitext',
284            [
285                'wikitext-var' => 'new_wikitext',
286                'article' => $page,
287                'pst' => true,
288                'contextUserIdentity' => $userIdentity
289            ] );
290
291        $this->vars->setLazyLoadVar( 'new_html', 'parse-wikitext',
292            [
293                'wikitext-var' => 'new_wikitext',
294                'article' => $page,
295                'contextUserIdentity' => $userIdentity
296            ] );
297
298        return $this;
299    }
300}