Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.58% |
139 / 141 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
VariableGenerator | |
98.58% |
139 / 141 |
|
71.43% |
5 / 7 |
18 | |
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% |
40 / 40 |
|
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 | |||
addEditVars | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
6 |
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\Storage\PreparedUpdate; |
8 | use MediaWiki\Title\Title; |
9 | use MediaWiki\User\UserFactory; |
10 | use MediaWiki\User\UserIdentity; |
11 | use MediaWiki\Utils\MWTimestamp; |
12 | use RecentChange; |
13 | use WikiPage; |
14 | |
15 | /** |
16 | * Class used to generate variables, for instance related to a given user or title. |
17 | */ |
18 | class 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 | } |