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