Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
60.44% |
110 / 182 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
AbuseFilterViewTestBatch | |
60.44% |
110 / 182 |
|
50.00% |
2 / 4 |
76.54 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
show | |
97.85% |
91 / 93 |
|
0.00% |
0 / 1 |
4 | |||
doTest | |
0.00% |
0 / 70 |
|
0.00% |
0 / 1 |
342 | |||
loadParameters | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\AbuseFilter\View; |
4 | |
5 | use MediaWiki\Context\IContextSource; |
6 | use MediaWiki\Extension\AbuseFilter\AbuseFilterChangesList; |
7 | use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager; |
8 | use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxBuilderFactory; |
9 | use MediaWiki\Extension\AbuseFilter\EditBox\EditBoxField; |
10 | use MediaWiki\Extension\AbuseFilter\Parser\RuleCheckerFactory; |
11 | use MediaWiki\Extension\AbuseFilter\VariableGenerator\VariableGeneratorFactory; |
12 | use MediaWiki\Html\Html; |
13 | use MediaWiki\HTMLForm\HTMLForm; |
14 | use MediaWiki\Linker\LinkRenderer; |
15 | use MediaWiki\Logging\LogEventsList; |
16 | use MediaWiki\Logging\LogPage; |
17 | use MediaWiki\Message\Message; |
18 | use MediaWiki\RecentChanges\RecentChange; |
19 | use MediaWiki\Revision\RevisionRecord; |
20 | use MediaWiki\Title\Title; |
21 | use Wikimedia\Rdbms\LBFactory; |
22 | use Wikimedia\Rdbms\SelectQueryBuilder; |
23 | |
24 | class AbuseFilterViewTestBatch extends AbuseFilterView { |
25 | /** |
26 | * @var int The limit of changes to test, hard coded for now |
27 | */ |
28 | private static $mChangeLimit = 100; |
29 | |
30 | /** |
31 | * @var LBFactory |
32 | */ |
33 | private $lbFactory; |
34 | /** |
35 | * @var string The text of the rule to test changes against |
36 | */ |
37 | private $testPattern; |
38 | /** |
39 | * @var EditBoxBuilderFactory |
40 | */ |
41 | private $boxBuilderFactory; |
42 | /** |
43 | * @var RuleCheckerFactory |
44 | */ |
45 | private $ruleCheckerFactory; |
46 | /** |
47 | * @var VariableGeneratorFactory |
48 | */ |
49 | private $varGeneratorFactory; |
50 | |
51 | /** |
52 | * @param LBFactory $lbFactory |
53 | * @param AbuseFilterPermissionManager $afPermManager |
54 | * @param EditBoxBuilderFactory $boxBuilderFactory |
55 | * @param RuleCheckerFactory $ruleCheckerFactory |
56 | * @param VariableGeneratorFactory $varGeneratorFactory |
57 | * @param IContextSource $context |
58 | * @param LinkRenderer $linkRenderer |
59 | * @param string $basePageName |
60 | * @param array $params |
61 | */ |
62 | public function __construct( |
63 | LBFactory $lbFactory, |
64 | AbuseFilterPermissionManager $afPermManager, |
65 | EditBoxBuilderFactory $boxBuilderFactory, |
66 | RuleCheckerFactory $ruleCheckerFactory, |
67 | VariableGeneratorFactory $varGeneratorFactory, |
68 | IContextSource $context, |
69 | LinkRenderer $linkRenderer, |
70 | string $basePageName, |
71 | array $params |
72 | ) { |
73 | parent::__construct( $afPermManager, $context, $linkRenderer, $basePageName, $params ); |
74 | $this->lbFactory = $lbFactory; |
75 | $this->boxBuilderFactory = $boxBuilderFactory; |
76 | $this->ruleCheckerFactory = $ruleCheckerFactory; |
77 | $this->varGeneratorFactory = $varGeneratorFactory; |
78 | } |
79 | |
80 | /** |
81 | * Shows the page |
82 | */ |
83 | public function show() { |
84 | $out = $this->getOutput(); |
85 | |
86 | if ( !$this->afPermManager->canUseTestTools( $this->getAuthority() ) ) { |
87 | // TODO: the message still refers to the old rights |
88 | $out->addWikiMsg( 'abusefilter-mustviewprivateoredit' ); |
89 | return; |
90 | } |
91 | |
92 | $this->loadParameters(); |
93 | |
94 | // Check if a loaded test pattern uses protected variables and if the user has the right |
95 | // to view protected variables. If they don't and protected variables are present, unset |
96 | // the test pattern to avoid leaking PII and notify the user. |
97 | // This is done as early as possible so that a filter with PII the user cannot access is |
98 | // never loaded. |
99 | if ( $this->testPattern !== '' ) { |
100 | $ruleChecker = $this->ruleCheckerFactory->newRuleChecker(); |
101 | $usedVars = $ruleChecker->getUsedVars( $this->testPattern ); |
102 | if ( $this->afPermManager->getForbiddenVariables( $this->getAuthority(), $usedVars ) ) { |
103 | $this->testPattern = ''; |
104 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
105 | $out->addHtml( |
106 | Html::errorBox( $this->msg( 'abusefilter-test-protectedvarerr' )->parse() ) |
107 | ); |
108 | } |
109 | } |
110 | |
111 | $out->setPageTitleMsg( $this->msg( 'abusefilter-test' ) ); |
112 | $out->addHelpLink( 'Extension:AbuseFilter/Rules format' ); |
113 | $out->addWikiMsg( 'abusefilter-test-intro', Message::numParam( self::$mChangeLimit ) ); |
114 | $out->enableOOUI(); |
115 | |
116 | $boxBuilder = $this->boxBuilderFactory->newEditBoxBuilder( $this, $this->getAuthority(), $out ); |
117 | |
118 | $rulesFields = [ |
119 | 'rules' => [ |
120 | 'section' => 'abusefilter-test-rules-section', |
121 | 'class' => EditBoxField::class, |
122 | 'html' => $boxBuilder->buildEditBox( |
123 | $this->testPattern, |
124 | true, |
125 | true, |
126 | false |
127 | ) . $this->buildFilterLoader() |
128 | ] |
129 | ]; |
130 | |
131 | $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); |
132 | $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge ); |
133 | $max = wfTimestampNow(); |
134 | |
135 | $optionsFields = [ |
136 | 'TestAction' => [ |
137 | 'type' => 'select', |
138 | 'label-message' => 'abusefilter-test-action', |
139 | 'options-messages' => [ |
140 | 'abusefilter-test-search-type-all' => '0', |
141 | 'abusefilter-test-search-type-edit' => 'edit', |
142 | 'abusefilter-test-search-type-move' => 'move', |
143 | 'abusefilter-test-search-type-delete' => 'delete', |
144 | 'abusefilter-test-search-type-createaccount' => 'createaccount', |
145 | 'abusefilter-test-search-type-upload' => 'upload' |
146 | ], |
147 | ], |
148 | 'TestUser' => [ |
149 | 'type' => 'user', |
150 | 'exists' => true, |
151 | 'ipallowed' => true, |
152 | 'required' => false, |
153 | 'label-message' => 'abusefilter-test-user', |
154 | ], |
155 | 'ExcludeBots' => [ |
156 | 'type' => 'check', |
157 | 'label-message' => 'abusefilter-test-nobots', |
158 | ], |
159 | 'TestPeriodStart' => [ |
160 | 'type' => 'datetime', |
161 | 'label-message' => 'abusefilter-test-period-start', |
162 | 'min' => $min, |
163 | 'max' => $max, |
164 | ], |
165 | 'TestPeriodEnd' => [ |
166 | 'type' => 'datetime', |
167 | 'label-message' => 'abusefilter-test-period-end', |
168 | 'min' => $min, |
169 | 'max' => $max, |
170 | ], |
171 | 'TestPage' => [ |
172 | 'type' => 'title', |
173 | 'label-message' => 'abusefilter-test-page', |
174 | 'creatable' => true, |
175 | 'required' => false, |
176 | ], |
177 | 'ShowNegative' => [ |
178 | 'type' => 'check', |
179 | 'label-message' => 'abusefilter-test-shownegative', |
180 | ], |
181 | ]; |
182 | array_walk( $optionsFields, static function ( &$el ) { |
183 | $el['section'] = 'abusefilter-test-options-section'; |
184 | } ); |
185 | $allFields = array_merge( $rulesFields, $optionsFields ); |
186 | |
187 | $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' ); |
188 | HTMLForm::factory( 'ooui', $allFields, $this->getContext() ) |
189 | ->setTitle( $this->getTitle( 'test' ) ) |
190 | ->setId( 'wpFilterForm' ) |
191 | ->setWrapperLegendMsg( 'abusefilter-test-legend' ) |
192 | ->setSubmitTextMsg( 'abusefilter-test-submit' ) |
193 | ->setSubmitCallback( [ $this, 'doTest' ] ) |
194 | ->showAlways(); |
195 | } |
196 | |
197 | /** |
198 | * Loads the revisions and checks the given syntax against them |
199 | * @param array $formData |
200 | * @param HTMLForm $form |
201 | * @return bool |
202 | */ |
203 | public function doTest( array $formData, HTMLForm $form ): bool { |
204 | // Quick syntax check. |
205 | $ruleChecker = $this->ruleCheckerFactory->newRuleChecker(); |
206 | |
207 | if ( !$ruleChecker->checkSyntax( $this->testPattern )->isValid() ) { |
208 | $form->addPreHtml( |
209 | Html::errorBox( $this->msg( 'abusefilter-test-syntaxerr' )->parse() ) |
210 | ); |
211 | return true; |
212 | } |
213 | |
214 | $dbr = $this->lbFactory->getReplicaDatabase(); |
215 | $rcQuery = RecentChange::getQueryInfo(); |
216 | $conds = []; |
217 | |
218 | // Normalise username |
219 | $userTitle = Title::newFromText( $formData['TestUser'], NS_USER ); |
220 | $testUser = $userTitle ? $userTitle->getText() : ''; |
221 | if ( $testUser !== '' ) { |
222 | $conds[$rcQuery['fields']['rc_user_text']] = $testUser; |
223 | } |
224 | |
225 | $startTS = strtotime( $formData['TestPeriodStart'] ); |
226 | if ( $startTS ) { |
227 | $conds[] = $dbr->expr( 'rc_timestamp', '>=', $dbr->timestamp( $startTS ) ); |
228 | } |
229 | $endTS = strtotime( $formData['TestPeriodEnd'] ); |
230 | if ( $endTS ) { |
231 | $conds[] = $dbr->expr( 'rc_timestamp', '<=', $dbr->timestamp( $endTS ) ); |
232 | } |
233 | if ( $formData['TestPage'] !== '' ) { |
234 | // The form validates the input for us, so this shouldn't throw. |
235 | $title = Title::newFromTextThrow( $formData['TestPage'] ); |
236 | $conds['rc_namespace'] = $title->getNamespace(); |
237 | $conds['rc_title'] = $title->getDBkey(); |
238 | } |
239 | |
240 | if ( $formData['ExcludeBots'] ) { |
241 | $conds['rc_bot'] = 0; |
242 | } |
243 | |
244 | $action = $formData['TestAction'] !== '0' ? $formData['TestAction'] : false; |
245 | $conds[] = $this->buildTestConditions( $dbr, $action ); |
246 | $conds = array_merge( $conds, $this->buildVisibilityConditions( $dbr, $this->getAuthority() ) ); |
247 | |
248 | $res = $dbr->newSelectQueryBuilder() |
249 | ->tables( $rcQuery['tables'] ) |
250 | ->fields( $rcQuery['fields'] ) |
251 | ->conds( $conds ) |
252 | ->caller( __METHOD__ ) |
253 | ->limit( self::$mChangeLimit ) |
254 | ->orderBy( 'rc_timestamp', SelectQueryBuilder::SORT_DESC ) |
255 | ->joinConds( $rcQuery['joins'] ) |
256 | ->fetchResultSet(); |
257 | |
258 | // Get our ChangesList |
259 | $changesList = new AbuseFilterChangesList( $this->getContext(), $this->testPattern ); |
260 | // Note, we're initializing some rows that will later be discarded. Hopefully this won't have any overhead. |
261 | $changesList->initChangesListRows( $res ); |
262 | $output = $changesList->beginRecentChangesList(); |
263 | |
264 | $counter = 1; |
265 | |
266 | $contextUser = $this->getUser(); |
267 | $ruleChecker->toggleConditionLimit( false ); |
268 | foreach ( $res as $row ) { |
269 | $rc = RecentChange::newFromRow( $row ); |
270 | if ( !$formData['ShowNegative'] ) { |
271 | $type = (int)$rc->getAttribute( 'rc_type' ); |
272 | $deletedValue = (int)$rc->getAttribute( 'rc_deleted' ); |
273 | if ( |
274 | ( |
275 | $type === RC_LOG && |
276 | !LogEventsList::userCanBitfield( |
277 | $deletedValue, |
278 | LogPage::SUPPRESSED_ACTION | LogPage::SUPPRESSED_USER, |
279 | $contextUser |
280 | ) |
281 | ) || ( |
282 | $type !== RC_LOG && |
283 | !RevisionRecord::userCanBitfield( $deletedValue, RevisionRecord::SUPPRESSED_ALL, $contextUser ) |
284 | ) |
285 | ) { |
286 | // If the RC is deleted, the user can't see it, and we're only showing matches, |
287 | // always skip this row. If ShowNegative is true, we can still show the row |
288 | // because we won't tell whether it matches the given filter. |
289 | continue; |
290 | } |
291 | } |
292 | |
293 | $varGenerator = $this->varGeneratorFactory->newRCGenerator( $rc, $contextUser ); |
294 | $vars = $varGenerator->getVars(); |
295 | |
296 | if ( !$vars ) { |
297 | continue; |
298 | } |
299 | |
300 | $ruleChecker->setVariables( $vars ); |
301 | $result = $ruleChecker->checkConditions( $this->testPattern )->getResult(); |
302 | |
303 | if ( $result || $formData['ShowNegative'] ) { |
304 | $changesList->setRCResult( $rc, $result ); |
305 | $rc->counter = $counter++; |
306 | $output .= $changesList->recentChangesLine( $rc, false ); |
307 | } |
308 | } |
309 | |
310 | $output .= $changesList->endRecentChangesList(); |
311 | |
312 | $form->addPostHtml( $output ); |
313 | |
314 | return true; |
315 | } |
316 | |
317 | /** |
318 | * Loads parameters from request |
319 | */ |
320 | public function loadParameters() { |
321 | $request = $this->getRequest(); |
322 | |
323 | $this->testPattern = $request->getText( 'wpFilterRules' ); |
324 | |
325 | if ( $this->testPattern === '' |
326 | && count( $this->mParams ) > 1 |
327 | && is_numeric( $this->mParams[1] ) |
328 | ) { |
329 | $dbr = $this->lbFactory->getReplicaDatabase(); |
330 | $pattern = $dbr->newSelectQueryBuilder() |
331 | ->select( 'af_pattern' ) |
332 | ->from( 'abuse_filter' ) |
333 | ->where( [ 'af_id' => intval( $this->mParams[1] ) ] ) |
334 | ->caller( __METHOD__ ) |
335 | ->fetchField(); |
336 | if ( $pattern !== false ) { |
337 | $this->testPattern = $pattern; |
338 | } |
339 | } |
340 | } |
341 | } |