Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.77% covered (success)
98.77%
80 / 81
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
EditBoxBuilder
98.77% covered (success)
98.77%
80 / 81
80.00% covered (warning)
80.00%
4 / 5
11
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
 buildEditBox
97.50% covered (success)
97.50%
39 / 40
0.00% covered (danger)
0.00%
0 / 1
5
 getDropdownOptions
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
3
 getSuggestionsDropdown
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getEditorControls
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getEditBox
n/a
0 / 0
n/a
0 / 0
0
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\EditBox;
4
5use MediaWiki\Extension\AbuseFilter\AbuseFilterPermissionManager;
6use MediaWiki\Extension\AbuseFilter\KeywordsManager;
7use MediaWiki\Html\Html;
8use MediaWiki\Language\MessageLocalizer;
9use MediaWiki\Output\OutputPage;
10use MediaWiki\Permissions\Authority;
11use OOUI\ButtonWidget;
12use OOUI\DropdownInputWidget;
13use OOUI\FieldLayout;
14use OOUI\FieldsetLayout;
15use OOUI\HtmlSnippet;
16use OOUI\Widget;
17use Wikimedia\Codex\Localization\MediaWikiLocalization;
18use Wikimedia\Codex\Utility\Codex;
19
20/**
21 * Base class for classes responsible for building filter edit boxes
22 */
23abstract class EditBoxBuilder {
24    /** @var AbuseFilterPermissionManager */
25    protected $afPermManager;
26
27    /** @var KeywordsManager */
28    protected $keywordsManager;
29
30    /** @var MessageLocalizer */
31    protected $localizer;
32
33    /** @var Authority */
34    protected $authority;
35
36    /** @var OutputPage */
37    protected $output;
38
39    /** @var array */
40    private $dropdownOptions;
41
42    /**
43     * @param AbuseFilterPermissionManager $afPermManager
44     * @param KeywordsManager $keywordsManager
45     * @param MessageLocalizer $messageLocalizer
46     * @param Authority $authority
47     * @param OutputPage $output
48     */
49    public function __construct(
50        AbuseFilterPermissionManager $afPermManager,
51        KeywordsManager $keywordsManager,
52        MessageLocalizer $messageLocalizer,
53        Authority $authority,
54        OutputPage $output
55    ) {
56        $this->afPermManager = $afPermManager;
57        $this->keywordsManager = $keywordsManager;
58        $this->localizer = $messageLocalizer;
59        $this->authority = $authority;
60        $this->output = $output;
61    }
62
63    /**
64     * @param string $rules
65     * @param bool $addResultDiv
66     * @param bool $externalForm
67     * @param bool $needsModifyRights
68     * @param-taint $rules none
69     * @return string
70     */
71    public function buildEditBox(
72        string $rules,
73        bool $addResultDiv = true,
74        bool $externalForm = false,
75        bool $needsModifyRights = true
76    ): string {
77        $this->output->addModules( 'ext.abuseFilter.edit' );
78        $this->output->enableOOUI();
79
80        $isUserAllowed = $needsModifyRights ?
81            $this->afPermManager->canEdit( $this->authority ) :
82            $this->afPermManager->canUseTestTools( $this->authority );
83        if ( !$isUserAllowed ) {
84            $addResultDiv = false;
85        }
86
87        $output = $this->getEditBox( $rules, $isUserAllowed, $externalForm );
88
89        if ( $isUserAllowed ) {
90            // Load the styles up front so the placeholder input below never appears unstyled.
91            $this->output->addModuleStyles( 'ext.abuseFilter.conditionBuilder.styles' );
92            $this->output->addModules( 'ext.abuseFilter.conditionBuilder' );
93
94            // Shown until the picker loads: same markup as the picker's own input so
95            // nothing shifts, disabled so typed text can't get lost in the swap.
96            $codex = new Codex( new MediaWikiLocalization( $this->localizer ) );
97            $pickerPlaceholder = $codex->textInput()
98                ->setPlaceholder( $this->localizer->msg( 'abusefilter-edit-builder-select' )->text() )
99                ->setDisabled( true )
100                ->build()
101                ->getHtml();
102            $conditionBuilder = new Widget( [
103                'content' => new HtmlSnippet( $pickerPlaceholder ),
104                'classes' => [ 'mw-abusefilter-condition-builder' ]
105            ] );
106
107            $dropdown = $this->getSuggestionsDropdown();
108
109            $formElements = [
110                new FieldLayout( $conditionBuilder ),
111                new FieldLayout( $dropdown, [
112                    'classes' => [ 'mw-abusefilter-condition-builder-fallback' ]
113                ] ),
114                new FieldLayout( $this->getEditorControls() )
115            ];
116
117            $fieldSet = new FieldsetLayout( [
118                'items' => $formElements,
119                'classes' => [ 'mw-abusefilter-edit-buttons', 'mw-abusefilter-javascript-tools' ]
120            ] );
121
122            $output .= $fieldSet;
123        }
124
125        if ( $addResultDiv ) {
126            $output .= Html::element(
127                'div',
128                [ 'id' => 'mw-abusefilter-syntaxresult', 'style' => 'display: none;' ]
129            );
130        }
131
132        return $output;
133    }
134
135    protected function getDropdownOptions(): array {
136        if ( $this->dropdownOptions === null ) {
137            $rawDropdown = $this->keywordsManager->getBuilderValues();
138
139            // The array needs to be rearranged to be understood by OOUI. It comes with the format
140            // [ group-msg-key => [ text-to-add => text-msg-key ] ] and we need it as
141            // [ group-msg => [ text-msg => text-to-add ] ]
142            // Also, the 'other' element must be the first one.
143            $dropdownOptions = [ $this->localizer->msg( 'abusefilter-edit-builder-select' )->text() => 'other' ];
144            foreach ( $rawDropdown as $group => $values ) {
145                // Give grep a chance to find the usages:
146                // abusefilter-edit-builder-group-op-arithmetic
147                // abusefilter-edit-builder-group-op-comparison
148                // abusefilter-edit-builder-group-op-bool
149                // abusefilter-edit-builder-group-misc
150                // abusefilter-edit-builder-group-funcs
151                // abusefilter-edit-builder-group-vars
152                $localisedGroup = $this->localizer->msg( "abusefilter-edit-builder-group-$group" )->text();
153                $dropdownOptions[ $localisedGroup ] = array_flip( $values );
154                $newKeys = array_map(
155                    function ( $key ) use ( $group, $dropdownOptions, $localisedGroup ) {
156                        // Force all operators and functions to be always shown as left to right text
157                        // with the help of control characters:
158                        // * 202A is LEFT-TO-RIGHT EMBEDDING (LRE)
159                        // * 202C is POP DIRECTIONAL FORMATTING (PDF)
160                        // This has to be done with control characters because
161                        // markup cannot be used within <option> elements.
162                        $operatorExample = "\u{202A}" .
163                            $dropdownOptions[ $localisedGroup ][ $key ] .
164                            "\u{202C}";
165                        return $this->localizer->msg(
166                            "abusefilter-edit-builder-$group-$key",
167                            $operatorExample
168                        )->text();
169                    },
170                    array_keys( $dropdownOptions[ $localisedGroup ] )
171                );
172                $dropdownOptions[ $localisedGroup ] = array_combine(
173                    $newKeys,
174                    $dropdownOptions[ $localisedGroup ]
175                );
176            }
177
178            $this->dropdownOptions = $dropdownOptions;
179        }
180        return $this->dropdownOptions;
181    }
182
183    private function getSuggestionsDropdown(): DropdownInputWidget {
184        $dropdownList = Html::listDropdownOptionsOoui( $this->getDropdownOptions() );
185        return new DropdownInputWidget( [
186            'name' => 'wpFilterBuilder',
187            'inputId' => 'wpFilterBuilder',
188            'options' => $dropdownList
189        ] );
190    }
191
192    /**
193     * Get an additional widget that "controls" the editor, and is placed next to it
194     * Precondition: the user has full rights.
195     */
196    protected function getEditorControls(): Widget {
197        return new ButtonWidget(
198            [
199                'label' => $this->localizer->msg( 'abusefilter-edit-check' )->text(),
200                'id' => 'mw-abusefilter-syntaxcheck'
201            ]
202        );
203    }
204
205    /**
206     * Generate the HTML for the actual edit box
207     *
208     * @param string $rules
209     * @param bool $isUserAllowed
210     * @param bool $externalForm
211     * @return string
212     */
213    abstract protected function getEditBox( string $rules, bool $isUserAllowed, bool $externalForm ): string;
214
215}