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