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