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