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