Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.15% covered (danger)
47.15%
58 / 123
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLRadioField
47.54% covered (danger)
47.54%
58 / 122
12.50% covered (danger)
12.50%
1 / 8
179.83
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 validate
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getInputHTML
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getInputOOUI
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 getInputCodex
100.00% covered (success)
100.00%
53 / 53
100.00% covered (success)
100.00%
1 / 1
6
 formatOptions
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 getOptionDescriptions
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
12.41
 needsLabel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\HTMLForm\Field;
4
5use InvalidArgumentException;
6use MediaWiki\Html\Html;
7use MediaWiki\HTMLForm\HTMLFormField;
8use MediaWiki\Parser\Sanitizer;
9use MediaWiki\Xml\Xml;
10
11/**
12 * Radio checkbox fields.
13 *
14 * @stable to extend
15 */
16class HTMLRadioField extends HTMLFormField {
17    /**
18     * @stable to call
19     * @param array $params
20     *   In addition to the usual HTMLFormField parameters, this can take the following fields:
21     *   - 'flatlist': If given, the options will be displayed on a single line (wrapping to following
22     *     lines if necessary), rather than each one on a line of its own. This is desirable mostly
23     *     for very short lists of concisely labelled options.
24     *   - 'option-descriptions': Associative array mapping values to raw HTML descriptions. These
25     *     descriptions are displayed below their respective options. Note that the key-value
26     *     relationship is reversed compared to 'options' and friends. Only supported by the Codex
27     *     display format; if this is set when another display format is used, an exception is thrown.
28     *   - 'option-descriptions-messages': Associative array mapping values to message keys.
29     *     Overwrites 'option-descriptions'.
30     *   - 'option-descriptions-messages-parse': If true, parse the messages in
31     *     'option-descriptions-messages'.
32     */
33    public function __construct( $params ) {
34        parent::__construct( $params );
35
36        if ( isset( $params['flatlist'] ) ) {
37            $this->mClass .= ' mw-htmlform-flatlist';
38        }
39    }
40
41    public function validate( $value, $alldata ) {
42        $p = parent::validate( $value, $alldata );
43
44        if ( $p !== true ) {
45            return $p;
46        }
47
48        if ( !is_string( $value ) && !is_int( $value ) ) {
49            return $this->msg( 'htmlform-required' );
50        }
51
52        $validOptions = HTMLFormField::flattenOptions( $this->getOptions() );
53
54        if ( in_array( strval( $value ), $validOptions, true ) ) {
55            return true;
56        } else {
57            return $this->msg( 'htmlform-select-badoption' );
58        }
59    }
60
61    /**
62     * This returns a block of all the radio options, in one cell.
63     * @see includes/HTMLFormField#getInputHTML()
64     *
65     * @param string $value
66     *
67     * @return string
68     */
69    public function getInputHTML( $value ) {
70        if (
71            isset( $this->mParams['option-descriptions'] ) ||
72            isset( $this->mParams['option-descriptions-messages'] ) ) {
73            throw new InvalidArgumentException(
74                "Non-Codex HTMLForms do not support the 'option-descriptions' parameter for radio buttons"
75            );
76        }
77
78        $html = $this->formatOptions( $this->getOptions(), strval( $value ) );
79
80        return $html;
81    }
82
83    public function getInputOOUI( $value ) {
84        if (
85            isset( $this->mParams['option-descriptions'] ) ||
86            isset( $this->mParams['option-descriptions-messages'] ) ) {
87            throw new InvalidArgumentException(
88                "Non-Codex HTMLForms do not support the 'option-descriptions' parameter for radio buttons"
89            );
90        }
91
92        $options = [];
93        foreach ( $this->getOptions() as $label => $data ) {
94            if ( is_int( $label ) ) {
95                $label = strval( $label );
96            }
97            $options[] = [
98                'data' => $data,
99                // @phan-suppress-next-line SecurityCheck-XSS Labels are raw when not from message
100                'label' => $this->mOptionsLabelsNotFromMessage ? new \OOUI\HtmlSnippet( $label ) : $label,
101            ];
102        }
103
104        return new \OOUI\RadioSelectInputWidget( [
105            'name' => $this->mName,
106            'id' => $this->mID,
107            'value' => $value,
108            'options' => $options,
109        ] + \OOUI\Element::configFromHtmlAttributes(
110            $this->getAttributes( [ 'disabled', 'tabindex' ] )
111        ) );
112    }
113
114    public function getInputCodex( $value, $hasErrors ) {
115        $optionDescriptions = $this->getOptionDescriptions();
116        $html = '';
117
118        // Iterate over an array of options and return the HTML markup.
119        foreach ( $this->getOptions() as $label => $radioValue ) {
120            $description = $optionDescriptions[$radioValue] ?? '';
121            $descriptionID = Sanitizer::escapeIdForAttribute(
122                $this->mID . "-$radioValue-description"
123            );
124
125            // Attributes for the radio input.
126            $radioInputClasses = [ 'cdx-radio__input' ];
127            $radioInputAttribs = [
128                'id' => Sanitizer::escapeIdForAttribute( $this->mID . "-$radioValue" ),
129                'type' => 'radio',
130                'name' => $this->mName,
131                'class' => $radioInputClasses,
132                'value' => $radioValue
133            ];
134            $radioInputAttribs += $this->getAttributes( [ 'disabled', 'tabindex' ] );
135            if ( $description ) {
136                $radioInputAttribs['aria-describedby'] = $descriptionID;
137            }
138
139            // Set the selected value as "checked".
140            if ( $radioValue === $value ) {
141                $radioInputAttribs['checked'] = true;
142            }
143
144            // Attributes for the radio icon.
145            $radioIconClasses = [ 'cdx-radio__icon' ];
146            $radioIconAttribs = [
147                'class' => $radioIconClasses,
148            ];
149
150            // Attributes for the radio label.
151            $radioLabelClasses = [ 'cdx-label__label' ];
152            $radioLabelAttribs = [
153                'class' => $radioLabelClasses,
154                'for' => $radioInputAttribs['id']
155            ];
156
157            // HTML markup for radio input, radio icon, and radio label elements.
158            $radioInput = Html::element( 'input', $radioInputAttribs );
159            $radioIcon = Html::element( 'span', $radioIconAttribs );
160            $radioLabel = $this->mOptionsLabelsNotFromMessage
161                ? Html::rawElement( 'label', $radioLabelAttribs, $label )
162                : Html::element( 'label', $radioLabelAttribs, $label );
163
164            $radioDescription = '';
165            if ( isset( $optionDescriptions[$radioValue] ) ) {
166                $radioDescription = Html::rawElement(
167                    'span',
168                    [ 'id' => $descriptionID, 'class' => 'cdx-label__description' ],
169                    $optionDescriptions[$radioValue]
170                );
171            }
172            $radioLabelWrapper = Html::rawElement(
173                'div',
174                [ 'class' => 'cdx-radio__label cdx-label' ],
175                $radioLabel . $radioDescription
176            );
177
178            // HTML markup for CSS-only Codex Radio.
179            $radio = Html::rawElement(
180                'span',
181                [ 'class' => 'cdx-radio' ],
182                $radioInput . $radioIcon . $radioLabelWrapper
183            );
184
185            // Append the Codex Radio HTML markup to the initialized empty string variable.
186            $html .= $radio;
187        }
188
189        return $html;
190    }
191
192    public function formatOptions( $options, $value ) {
193        $html = '';
194
195        $attribs = $this->getAttributes( [ 'disabled', 'tabindex' ] );
196        $elementFunc = [ Html::class, $this->mOptionsLabelsNotFromMessage ? 'rawElement' : 'element' ];
197
198        # @todo Should this produce an unordered list perhaps?
199        foreach ( $options as $label => $info ) {
200            if ( is_array( $info ) ) {
201                $html .= Html::rawElement( 'h1', [], $label ) . "\n";
202                $html .= $this->formatOptions( $info, $value );
203            } else {
204                $id = Sanitizer::escapeIdForAttribute( $this->mID . "-$info" );
205                $classes = [ 'mw-htmlform-flatlist-item' ];
206                $radio = Xml::radio(
207                    $this->mName, $info, $info === $value, $attribs + [ 'id' => $id ]
208                );
209                $radio .= "\u{00A0}" . call_user_func( $elementFunc, 'label', [ 'for' => $id ], $label );
210
211                $html .= ' ' . Html::rawElement(
212                    'div',
213                    [ 'class' => $classes ],
214                    $radio
215                );
216            }
217        }
218
219        return $html;
220    }
221
222    /**
223     * Fetch the array of option descriptions from the field's parameters. This checks
224     * 'option-descriptions-messages' first, then 'option-descriptions'.
225     *
226     * @return array|null Array mapping option values to raw HTML descriptions
227     */
228    protected function getOptionDescriptions() {
229        if ( array_key_exists( 'option-descriptions-messages', $this->mParams ) ) {
230            $needsParse = $this->mParams['option-descriptions-messages-parse'] ?? false;
231            $optionDescriptions = [];
232            foreach ( $this->mParams['option-descriptions-messages'] as $value => $msgKey ) {
233                $msg = $this->msg( $msgKey );
234                $optionDescriptions[$value] = $needsParse ? $msg->parse() : $msg->escaped();
235            }
236            return $optionDescriptions;
237        } elseif ( array_key_exists( 'option-descriptions', $this->mParams ) ) {
238            return $this->mParams['option-descriptions'];
239        }
240    }
241
242    protected function needsLabel() {
243        return false;
244    }
245}
246
247/** @deprecated class alias since 1.42 */
248class_alias( HTMLRadioField::class, 'HTMLRadioField' );