Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.69% |
151 / 153 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
VariableGenerator | |
98.69% |
151 / 153 |
|
75.00% |
6 / 8 |
15 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getVariableHolder | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addGenericVars | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
2.01 | |||
addUserVars | |
100.00% |
48 / 48 |
|
100.00% |
1 / 1 |
2 | |||
addTitleVars | |
96.77% |
30 / 31 |
|
0.00% |
0 / 1 |
5 | |||
addDerivedEditVars | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
1 | |||
addEditVarsFromUpdate | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
addEditVars | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\VariableGenerator; |
4 | |
5 | use MediaWiki\Extension\AbuseFilter\Hooks\AbuseFilterHookRunner; |
6 | use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder; |
7 | use MediaWiki\Page\WikiPage; |
8 | use MediaWiki\RecentChanges\RecentChange; |
9 | use MediaWiki\Storage\PreparedUpdate; |
10 | use MediaWiki\Title\Title; |
11 | use MediaWiki\User\User; |
12 | use MediaWiki\User\UserFactory; |
13 | use MediaWiki\User\UserIdentity; |
14 | use MediaWiki\Utils\MWTimestamp; |
15 | |
16 | /** |
17 | * Class used to generate variables, for instance related to a given user or title. |
18 | */ |
19 | class 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 | } |