Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 457
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
InputBox
0.00% covered (danger)
0.00%
0 / 457
0.00% covered (danger)
0.00%
0 / 18
11556
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 render
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
110
 getEditActionArgs
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getFormLinebreakClasses
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getLinebreakClasses
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchForm
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 1
600
 getSearchForm2
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
72
 getCreateForm
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
272
 getMoveForm
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
20
 getCommentForm
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
90
 extractOptions
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
272
 isValidColor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildTextBox
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 buildCheckboxInput
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 buildSubmitInput
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 bgColorStyle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 shouldUseVE
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 languageConvert
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Classes for InputBox extension
4 *
5 * @file
6 * @ingroup Extensions
7 */
8
9namespace MediaWiki\Extension\InputBox;
10
11use MediaWiki\Config\Config;
12use MediaWiki\Html\Html;
13use MediaWiki\MainConfigNames;
14use MediaWiki\Parser\Parser;
15use MediaWiki\Parser\Sanitizer;
16use MediaWiki\Registration\ExtensionRegistry;
17use MediaWiki\SpecialPage\SpecialPage;
18
19/**
20 * InputBox class
21 */
22class InputBox {
23
24    /* Fields */
25
26    /** @var Config */
27    private $config;
28    /** @var Parser */
29    private $mParser;
30    /** @var string */
31    private $mType = '';
32    /** @var int */
33    private $mWidth = 50;
34    /** @var ?string */
35    private $mPreload = null;
36    /** @var ?array */
37    private $mPreloadparams = null;
38    /** @var ?string */
39    private $mEditIntro = null;
40    /** @var ?string */
41    private $mUseVE = null;
42    /** @var ?string */
43    private $mUseDT = null;
44    /** @var ?string */
45    private $mSummary = null;
46    /** @var ?string */
47    private $mNosummary = null;
48    /** @var ?string */
49    private $mMinor = null;
50    /** @var string */
51    private $mPage = '';
52    /** @var string */
53    private $mBR = 'yes';
54    /** @var string */
55    private $mDefaultText = '';
56    /** @var string */
57    private $mPlaceholderText = '';
58    /** @var string */
59    private $mBGColor = 'transparent';
60    /** @var string */
61    private $mButtonLabel = '';
62    /** @var string */
63    private $mSearchButtonLabel = '';
64    /** @var string */
65    private $mFullTextButton = '';
66    /** @var string */
67    private $mLabelText = '';
68    /** @var ?string */
69    private $mHidden = '';
70    /** @var string */
71    private $mNamespaces = '';
72    /** @var string */
73    private $mID = '';
74    /** @var ?string */
75    private $mInline = null;
76    /** @var string */
77    private $mPrefix = '';
78    /** @var string */
79    private $mDir = '';
80    /** @var string */
81    private $mSearchFilter = '';
82    /** @var string */
83    private $mTour = '';
84    /** @var string */
85    private $mTextBoxAriaLabel = '';
86
87    /* Functions */
88
89    /**
90     * @param Config $config
91     * @param Parser $parser
92     */
93    public function __construct(
94        Config $config,
95        $parser
96    ) {
97        $this->config = $config;
98        $this->mParser = $parser;
99        // Default value for dir taken from the page language (bug 37018)
100        $this->mDir = $this->mParser->getTargetLanguage()->getDir();
101        // Split caches by language, to make sure visitors do not see a cached
102        // version in a random language (since labels are in the user language)
103        $this->mParser->getOptions()->getUserLangObj();
104        $this->mParser->getOutput()->addModuleStyles( [
105            'ext.inputBox.styles',
106        ] );
107    }
108
109    public function render() {
110        // Handle various types
111        switch ( $this->mType ) {
112            case 'create':
113            case 'comment':
114                return $this->getCreateForm();
115            case 'move':
116                return $this->getMoveForm();
117            case 'commenttitle':
118                return $this->getCommentForm();
119            case 'search':
120                return $this->getSearchForm( 'search' );
121            case 'fulltext':
122                return $this->getSearchForm( 'fulltext' );
123            case 'search2':
124                return $this->getSearchForm2();
125            default:
126                $key = $this->mType === '' ? 'inputbox-error-no-type' : 'inputbox-error-bad-type';
127                return Html::rawElement( 'div', [],
128                    Html::element( 'strong',
129                        [ 'class' => 'error' ],
130                        wfMessage( $key, $this->mType )->text()
131                    )
132                );
133        }
134    }
135
136    /**
137     * Returns the action name and value to use in inputboxes which redirects to edit pages.
138     * Decides, if the link should redirect to VE edit page (veaction=edit) or to wikitext editor
139     * (action=edit).
140     *
141     * @return array Array with name and value data
142     */
143    private function getEditActionArgs() {
144        // default is wikitext editor
145        $args = [
146            'name' => 'action',
147            'value' => 'edit',
148        ];
149        // check, if VE is installed and VE editor is requested
150        if ( $this->shouldUseVE() ) {
151            $args = [
152                'name' => 'veaction',
153                'value' => 'edit',
154            ];
155        }
156        return $args;
157    }
158
159    /**
160     * Get common classes, that could be added and depend on, if
161     * a line break between a button and an input field is added or not.
162     *
163     * @return string
164     */
165    private function getFormLinebreakClasses() {
166        return strtolower( $this->mBR ) === '<br />' ? ' mw-inputbox-form' : ' mw-inputbox-form-inline';
167    }
168
169    /**
170     * Get common classes, that could be added and depend on, if
171     * a line break between a button and an input field is added or not.
172     *
173     * @return string
174     */
175    private function getLinebreakClasses() {
176        return strtolower( $this->mBR ) === '<br />' ? 'mw-inputbox-input ' : '';
177    }
178
179    /**
180     * Generate search form
181     * @param string $type
182     * @return string HTML
183     */
184    public function getSearchForm( $type ) {
185        // Use button label fallbacks
186        if ( !$this->mButtonLabel ) {
187            $this->mButtonLabel = wfMessage( 'inputbox-tryexact' )->text();
188        }
189        if ( !$this->mSearchButtonLabel ) {
190            $this->mSearchButtonLabel = wfMessage( 'inputbox-searchfulltext' )->text();
191        }
192        if ( $this->mID !== '' ) {
193            $idArray = [ 'id' => Sanitizer::escapeIdForAttribute( $this->mID ) ];
194        } else {
195            $idArray = [];
196        }
197        // We need a unqiue id to link <label> to checkboxes, but also
198        // want multiple <inputbox>'s to not be invalid html
199        $idRandStr = Sanitizer::escapeIdForAttribute( '-' . $this->mID . wfRandom() );
200
201        // Build HTML
202        $htmlOut = Html::openElement( 'div',
203            [
204                'class' => 'mw-inputbox-centered',
205                'style' => $this->bgColorStyle(),
206            ]
207        );
208        $htmlOut .= Html::openElement( 'form',
209            [
210                'name' => 'searchbox',
211                'class' => 'searchbox' . $this->getFormLinebreakClasses(),
212                'action' => SpecialPage::getTitleFor( 'Search' )->getLocalUrl(),
213            ] + $idArray
214        );
215
216        $htmlOut .= $this->buildTextBox( [
217            // enable SearchSuggest with mw-searchInput class
218            'class' => $this->getLinebreakClasses() . 'mw-searchInput searchboxInput',
219            'name' => 'search',
220            'type' => $this->mHidden ? 'hidden' : 'text',
221            'value' => $this->mDefaultText,
222            'placeholder' => $this->mPlaceholderText,
223            'size' => $this->mWidth,
224            'dir' => $this->mDir
225        ] );
226
227        if ( $this->mPrefix !== '' ) {
228            $htmlOut .= Html::hidden( 'prefix', $this->mPrefix );
229        }
230
231        if ( $this->mSearchFilter !== '' ) {
232            $htmlOut .= Html::hidden( 'searchfilter', $this->mSearchFilter );
233        }
234
235        if ( $this->mTour !== '' ) {
236            $htmlOut .= Html::hidden( 'tour', $this->mTour );
237        }
238
239        $htmlOut .= $this->mBR;
240
241        // Determine namespace checkboxes
242        $namespacesArray = explode( ',', $this->mNamespaces );
243        if ( $this->mNamespaces ) {
244            $contLang = $this->mParser->getContentLanguage();
245            $namespaces = $contLang->getNamespaces();
246            $nsAliases = array_merge(
247                $contLang->getNamespaceAliases(),
248                $this->config->get( MainConfigNames::NamespaceAliases )
249            );
250            $showNamespaces = [];
251            $checkedNS = [];
252            // Check for valid namespaces
253            foreach ( $namespacesArray as $userNS ) {
254                // no whitespace
255                $userNS = trim( $userNS );
256
257                // Namespace needs to be checked if flagged with "**"
258                if ( strpos( $userNS, '**' ) ) {
259                    $userNS = str_replace( '**', '', $userNS );
260                    $checkedNS[$userNS] = true;
261                }
262
263                $mainMsg = wfMessage( 'inputbox-ns-main' )->inContentLanguage()->text();
264                if ( $userNS === 'Main' || $userNS === $mainMsg ) {
265                    $i = 0;
266                } elseif ( array_search( $userNS, $namespaces ) ) {
267                    $i = array_search( $userNS, $namespaces );
268                } elseif ( isset( $nsAliases[$userNS] ) ) {
269                    $i = $nsAliases[$userNS];
270                } else {
271                    // Namespace not recognized, skip
272                    continue;
273                }
274                $showNamespaces[$i] = $userNS;
275                if ( isset( $checkedNS[$userNS] ) && $checkedNS[$userNS] ) {
276                    $checkedNS[$i] = true;
277                }
278            }
279
280            // Show valid namespaces
281            foreach ( $showNamespaces as $i => $name ) {
282                $checked = [];
283                // Namespace flagged with "**" or if it's the only one
284                if ( ( isset( $checkedNS[$i] ) && $checkedNS[$i] ) || count( $showNamespaces ) === 1 ) {
285                    $checked = [ 'checked' => 'checked' ];
286                }
287
288                if ( count( $showNamespaces ) === 1 ) {
289                    // Checkbox
290                    $htmlOut .= Html::element( 'input',
291                        [
292                            'type' => 'hidden',
293                            'name' => 'ns' . $i,
294                            'value' => 1,
295                            'id' => 'mw-inputbox-ns' . $i . $idRandStr
296                        ] + $checked
297                    );
298                } else {
299                    // Checkbox
300                    $htmlOut .= $this->buildCheckboxInput(
301                        $name, 'ns' . $i, 'mw-inputbox-ns' . $i . $idRandStr, "1", $checked
302                    );
303                }
304            }
305
306            // Line break
307            $htmlOut .= $this->mBR;
308        } elseif ( $type === 'search' ) {
309            // Go button
310            $htmlOut .= $this->buildSubmitInput(
311                [
312                    'type' => 'submit',
313                    'name' => 'go',
314                    'value' => $this->mButtonLabel
315                ]
316            );
317            $htmlOut .= "\u{00A0}";
318        }
319
320        // Search button
321        $htmlOut .= $this->buildSubmitInput(
322            [
323                'type' => 'submit',
324                'name' => 'fulltext',
325                'value' => $this->mSearchButtonLabel
326            ]
327        );
328
329        // Hidden fulltext param for IE (bug 17161)
330        if ( $type === 'fulltext' ) {
331            $htmlOut .= Html::hidden( 'fulltext', 'Search' );
332        }
333
334        $htmlOut .= Html::closeElement( 'form' );
335        $htmlOut .= Html::closeElement( 'div' );
336
337        // Return HTML
338        return $htmlOut;
339    }
340
341    /**
342     * Generate search form version 2
343     * @return string
344     */
345    public function getSearchForm2() {
346        // Use button label fallbacks
347        if ( !$this->mButtonLabel ) {
348            $this->mButtonLabel = wfMessage( 'inputbox-tryexact' )->text();
349        }
350
351        if ( $this->mID !== '' ) {
352            $unescapedID = $this->mID;
353        } else {
354            // The label element needs a unique id, use
355            // random number to avoid multiple input boxes
356            // having conflicts.
357            $unescapedID = wfRandom();
358        }
359        $id = Sanitizer::escapeIdForAttribute( $unescapedID );
360        $htmlLabel = '';
361        if ( strlen( trim( $this->mLabelText ) ) ) {
362            $htmlLabel = Html::openElement( 'label', [ 'for' => 'bodySearchInput' . $id,
363                'class' => 'mw-inputbox-label'
364            ] );
365            $htmlLabel .= $this->mParser->recursiveTagParse( $this->mLabelText );
366            $htmlLabel .= Html::closeElement( 'label' );
367        }
368        $htmlOut = Html::openElement( 'form',
369            [
370                'name' => 'bodySearch' . $id,
371                'id' => 'bodySearch' . $id,
372                'class' => 'bodySearch' .
373                    ( $this->mInline ? ' mw-inputbox-inline' : '' ) . $this->getFormLinebreakClasses(),
374                'action' => SpecialPage::getTitleFor( 'Search' )->getLocalUrl(),
375            ]
376        );
377        $htmlOut .= Html::openElement( 'div',
378            [
379                'class' => 'bodySearchWrap' . ( $this->mInline ? ' mw-inputbox-inline' : '' ),
380                'style' => $this->bgColorStyle(),
381            ]
382        );
383        $htmlOut .= $htmlLabel;
384
385        $htmlOut .= $this->buildTextBox( [
386            'type' => $this->mHidden ? 'hidden' : 'text',
387            'name' => 'search',
388            // enable SearchSuggest with mw-searchInput class
389            'class' => 'mw-searchInput',
390            'size' => $this->mWidth,
391            'id' => 'bodySearchInput' . $id,
392            'dir' => $this->mDir,
393            'placeholder' => $this->mPlaceholderText
394        ] );
395
396        $htmlOut .= "\u{00A0}" . $this->buildSubmitInput(
397            [
398                'type' => 'submit',
399                'name' => 'go',
400                'value' => $this->mButtonLabel,
401            ]
402        );
403
404        // Better testing needed here!
405        if ( $this->mFullTextButton !== '' ) {
406            $htmlOut .= $this->buildSubmitInput(
407                [
408                    'type' => 'submit',
409                    'name' => 'fulltext',
410                    'value' => $this->mSearchButtonLabel
411                ]
412            );
413        }
414
415        $htmlOut .= Html::closeElement( 'div' );
416        $htmlOut .= Html::closeElement( 'form' );
417
418        // Return HTML
419        return $htmlOut;
420    }
421
422    /**
423     * Generate create page form
424     * @return string
425     */
426    public function getCreateForm() {
427        if ( $this->mType === 'comment' ) {
428            if ( !$this->mButtonLabel ) {
429                $this->mButtonLabel = wfMessage( 'inputbox-postcomment' )->text();
430            }
431        } else {
432            if ( !$this->mButtonLabel ) {
433                $this->mButtonLabel = wfMessage( 'inputbox-createarticle' )->text();
434            }
435        }
436
437        $htmlOut = Html::openElement( 'div',
438            [
439                'class' => 'mw-inputbox-centered',
440                'style' => $this->bgColorStyle(),
441            ]
442        );
443        $createBoxParams = [
444            'name' => 'createbox',
445            'class' => 'createbox' . $this->getFormLinebreakClasses(),
446            'action' => $this->config->get( MainConfigNames::Script ),
447            'method' => 'get'
448        ];
449        if ( $this->mID !== '' ) {
450            $createBoxParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
451        }
452        $htmlOut .= Html::openElement( 'form', $createBoxParams );
453        $editArgs = $this->getEditActionArgs();
454        $htmlOut .= Html::hidden( $editArgs['name'], $editArgs['value'] );
455        if ( $this->mPreload !== null ) {
456            $htmlOut .= Html::hidden( 'preload', $this->mPreload );
457        }
458        if ( is_array( $this->mPreloadparams ) ) {
459            foreach ( $this->mPreloadparams as $preloadparams ) {
460                $htmlOut .= Html::hidden( 'preloadparams[]', $preloadparams );
461            }
462        }
463        if ( $this->mEditIntro !== null ) {
464            $htmlOut .= Html::hidden( 'editintro', $this->mEditIntro );
465        }
466        if ( $this->mSummary !== null ) {
467            $htmlOut .= Html::hidden( 'summary', $this->mSummary );
468        }
469        if ( $this->mNosummary !== null ) {
470            $htmlOut .= Html::hidden( 'nosummary', $this->mNosummary );
471        }
472        if ( $this->mPrefix !== '' ) {
473            $htmlOut .= Html::hidden( 'prefix', $this->mPrefix );
474        }
475        if ( $this->mMinor !== null ) {
476            $htmlOut .= Html::hidden( 'minor', $this->mMinor );
477        }
478        // @phan-suppress-next-line PhanSuspiciousValueComparison False positive
479        if ( $this->mType === 'comment' ) {
480            $htmlOut .= Html::hidden( 'section', 'new' );
481            if ( $this->mUseDT ) {
482                $htmlOut .= Html::hidden( 'dtpreload', '1' );
483            }
484        }
485
486        $htmlOut .= $this->buildTextBox( [
487            'type' => $this->mHidden ? 'hidden' : 'text',
488            'name' => 'title',
489            'class' => $this->getLinebreakClasses() .
490                'mw-inputbox-createbox',
491            'value' => $this->mDefaultText,
492            'placeholder' => $this->mPlaceholderText,
493            // For visible input fields, use required so that the form will not
494            // submit without a value
495            'required' => !$this->mHidden,
496            'size' => $this->mWidth,
497            'dir' => $this->mDir
498        ] );
499
500        $htmlOut .= $this->mBR;
501        $htmlOut .= $this->buildSubmitInput(
502            [
503                'type' => 'submit',
504                'name' => 'create',
505                'value' => $this->mButtonLabel
506            ],
507            true
508        );
509        $htmlOut .= Html::closeElement( 'form' );
510        $htmlOut .= Html::closeElement( 'div' );
511
512        // Return HTML
513        return $htmlOut;
514    }
515
516    /**
517     * Generate move page form
518     * @return string
519     */
520    public function getMoveForm() {
521        if ( !$this->mButtonLabel ) {
522            $this->mButtonLabel = wfMessage( 'inputbox-movearticle' )->text();
523        }
524
525        $htmlOut = Html::openElement( 'div',
526            [
527                'class' => 'mw-inputbox-centered',
528                'style' => $this->bgColorStyle(),
529            ]
530        );
531        $moveBoxParams = [
532            'name' => 'movebox',
533            'class' => 'mw-movebox' . $this->getFormLinebreakClasses(),
534            'action' => $this->config->get( MainConfigNames::Script ),
535            'method' => 'get'
536        ];
537        if ( $this->mID !== '' ) {
538            $moveBoxParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
539        }
540        $htmlOut .= Html::openElement( 'form', $moveBoxParams );
541        $htmlOut .= Html::hidden( 'title',
542            SpecialPage::getTitleFor( 'Movepage', $this->mPage )->getPrefixedText() );
543        $htmlOut .= Html::hidden( 'wpReason', $this->mSummary );
544        $htmlOut .= Html::hidden( 'prefix', $this->mPrefix );
545
546        $htmlOut .= $this->buildTextBox( [
547            'type' => $this->mHidden ? 'hidden' : 'text',
548            'name' => 'wpNewTitle',
549            'class' => $this->getLinebreakClasses() . 'mw-moveboxInput',
550            'value' => $this->mDefaultText,
551            'placeholder' => $this->mPlaceholderText,
552            'size' => $this->mWidth,
553            'dir' => $this->mDir
554        ] );
555
556        $htmlOut .= $this->mBR;
557        $htmlOut .= $this->buildSubmitInput(
558            [
559                'type' => 'submit',
560                'value' => $this->mButtonLabel
561            ],
562            true
563        );
564        $htmlOut .= Html::closeElement( 'form' );
565        $htmlOut .= Html::closeElement( 'div' );
566
567        // Return HTML
568        return $htmlOut;
569    }
570
571    /**
572     * Generate new section form
573     * @return string
574     */
575    public function getCommentForm() {
576        if ( !$this->mButtonLabel ) {
577                $this->mButtonLabel = wfMessage( 'inputbox-postcommenttitle' )->text();
578        }
579
580        $htmlOut = Html::openElement( 'div',
581            [
582                'class' => 'mw-inputbox-centered',
583                'style' => $this->bgColorStyle(),
584            ]
585        );
586        $commentFormParams = [
587            'name' => 'commentbox',
588            'class' => 'commentbox' . $this->getFormLinebreakClasses(),
589            'action' => $this->config->get( MainConfigNames::Script ),
590            'method' => 'get'
591        ];
592        if ( $this->mID !== '' ) {
593            $commentFormParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
594        }
595        $htmlOut .= Html::openElement( 'form', $commentFormParams );
596        $editArgs = $this->getEditActionArgs();
597        $htmlOut .= Html::hidden( $editArgs['name'], $editArgs['value'] );
598        if ( $this->mPreload !== null ) {
599            $htmlOut .= Html::hidden( 'preload', $this->mPreload );
600        }
601        if ( is_array( $this->mPreloadparams ) ) {
602            foreach ( $this->mPreloadparams as $preloadparams ) {
603                $htmlOut .= Html::hidden( 'preloadparams[]', $preloadparams );
604            }
605        }
606        if ( $this->mEditIntro !== null ) {
607            $htmlOut .= Html::hidden( 'editintro', $this->mEditIntro );
608        }
609
610        $htmlOut .= $this->buildTextBox( [
611            'type' => $this->mHidden ? 'hidden' : 'text',
612            'name' => 'preloadtitle',
613            'class' => $this->getLinebreakClasses() . 'commentboxInput',
614            'value' => $this->mDefaultText,
615            'placeholder' => $this->mPlaceholderText,
616            'size' => $this->mWidth,
617            'dir' => $this->mDir
618        ] );
619
620        $htmlOut .= Html::hidden( 'section', 'new' );
621        if ( $this->mUseDT ) {
622            $htmlOut .= Html::hidden( 'dtpreload', '1' );
623        }
624        $htmlOut .= Html::hidden( 'title', $this->mPage );
625        $htmlOut .= $this->mBR;
626        $htmlOut .= $this->buildSubmitInput(
627            [
628                'type' => 'submit',
629                'name' => 'create',
630                'value' => $this->mButtonLabel
631            ],
632            true
633        );
634        $htmlOut .= Html::closeElement( 'form' );
635        $htmlOut .= Html::closeElement( 'div' );
636
637        // Return HTML
638        return $htmlOut;
639    }
640
641    /**
642     * Extract options from a blob of text
643     *
644     * @param string $text Tag contents
645     */
646    public function extractOptions( $text ) {
647        // Parse all possible options
648        $values = [];
649        foreach ( explode( "\n", $text ) as $line ) {
650            if ( strpos( $line, '=' ) === false ) {
651                continue;
652            }
653            [ $name, $value ] = explode( '=', $line, 2 );
654            $name = strtolower( trim( $name ) );
655            $value = Sanitizer::decodeCharReferences( trim( $value ) );
656            if ( $name === 'preloadparams[]' ) {
657                // We have to special-case this one because it's valid for it to appear more than once.
658                $this->mPreloadparams[] = $value;
659            } else {
660                $values[ $name ] = $value;
661            }
662        }
663
664        // Validate the dir value.
665        if ( isset( $values['dir'] ) && !in_array( $values['dir'], [ 'ltr', 'rtl' ] ) ) {
666            unset( $values['dir'] );
667        }
668
669        // Build list of options, with local member names
670        $options = [
671            'type' => 'mType',
672            'width' => 'mWidth',
673            'preload' => 'mPreload',
674            'page' => 'mPage',
675            'editintro' => 'mEditIntro',
676            'useve' => 'mUseVE',
677            'usedt' => 'mUseDT',
678            'summary' => 'mSummary',
679            'nosummary' => 'mNosummary',
680            'minor' => 'mMinor',
681            'break' => 'mBR',
682            'default' => 'mDefaultText',
683            'placeholder' => 'mPlaceholderText',
684            'bgcolor' => 'mBGColor',
685            'buttonlabel' => 'mButtonLabel',
686            'searchbuttonlabel' => 'mSearchButtonLabel',
687            'fulltextbutton' => 'mFullTextButton',
688            'namespaces' => 'mNamespaces',
689            'labeltext' => 'mLabelText',
690            'hidden' => 'mHidden',
691            'id' => 'mID',
692            'inline' => 'mInline',
693            'prefix' => 'mPrefix',
694            'dir' => 'mDir',
695            'searchfilter' => 'mSearchFilter',
696            'tour' => 'mTour',
697            'arialabel' => 'mTextBoxAriaLabel'
698        ];
699        // Options we should maybe run through lang converter.
700        $convertOptions = [
701            'default' => true,
702            'buttonlabel' => true,
703            'searchbuttonlabel' => true,
704            'placeholder' => true,
705            'arialabel' => true
706        ];
707        foreach ( $options as $name => $var ) {
708            if ( isset( $values[$name] ) ) {
709                $this->$var = $values[$name];
710                if ( isset( $convertOptions[$name] ) ) {
711                    $this->$var = $this->languageConvert( $this->$var );
712                }
713            }
714        }
715
716        // Insert a line break if configured to do so
717        $this->mBR = ( strtolower( $this->mBR ) === 'no' ) ? ' ' : '<br />';
718
719        // Validate the width; make sure it's a valid, positive integer
720        $this->mWidth = intval( $this->mWidth <= 0 ? 50 : $this->mWidth );
721
722        // Validate background color
723        if ( !$this->isValidColor( $this->mBGColor ) ) {
724            $this->mBGColor = 'transparent';
725        }
726
727        // T297725: De-obfuscate attempts to trick people into making edits to .js pages
728        $target = $this->mType === 'commenttitle' ? $this->mPage : $this->mDefaultText;
729        if ( $this->mHidden && $this->mPreload && substr( $target, -3 ) === '.js' ) {
730            $this->mHidden = null;
731        }
732    }
733
734    /**
735     * Do a security check on the bgcolor parameter
736     * @param string $color
737     * @return bool
738     */
739    public function isValidColor( $color ) {
740        $regex = <<<REGEX
741            /^ (
742                [a-zA-Z]* |       # color names
743                \# [0-9a-f]{3} |  # short hexadecimal
744                \# [0-9a-f]{6} |  # long hexadecimal
745                rgb \s* \( \s* (
746                    \d+ \s* , \s* \d+ \s* , \s* \d+ |    # rgb integer
747                    [0-9.]+% \s* , \s* [0-9.]+% \s* , \s* [0-9.]+%   # rgb percent
748                ) \s* \)
749            ) $ /xi
750REGEX;
751        return (bool)preg_match( $regex, $color );
752    }
753
754    /**
755     * Factory method to help build the textbox widget.
756     *
757     * @param array $defaultAttr
758     * @return string
759     */
760    private function buildTextBox( $defaultAttr ) {
761        if ( $this->mTextBoxAriaLabel ) {
762            $defaultAttr[ 'aria-label' ] = $this->mTextBoxAriaLabel;
763        }
764
765        $class = $defaultAttr[ 'class' ] ?? '';
766        $class .= ' cdx-text-input__input';
767        $defaultAttr[ 'class' ] = $class;
768        return Html::openElement( 'div', [
769            'class' => 'cdx-text-input',
770        ] )
771            . Html::element( 'input', $defaultAttr )
772            . Html::closeElement( 'div' );
773    }
774
775    /**
776     * Factory method to help build checkbox input.
777     *
778     * @param string $label text displayed next to checkbox (label)
779     * @param string $name name of input
780     * @param string $id id of input
781     * @param string $value value of input
782     * @param array $defaultAttr (optional)
783     * @return string
784     */
785    private function buildCheckboxInput( $label, $name, $id, $value, $defaultAttr = [] ) {
786        $htmlOut = ' <span class="cdx-checkbox cdx-checkbox--inline">';
787        $htmlOut .= Html::element( 'input',
788            [
789                'type' => 'checkbox',
790                'name' => $name,
791                'value' => $value,
792                'id' => $id,
793                'class' => 'cdx-checkbox__input',
794            ] + $defaultAttr
795        );
796        $htmlOut .= '<span class="cdx-checkbox__icon"></span>';
797        // Label
798        $htmlOut .= Html::label( $label, $id, [
799            'class' => 'cdx-checkbox__label',
800        ] );
801        $htmlOut .= '</span> ';
802        return $htmlOut;
803    }
804
805    /**
806     * Factory method to help build submit button.
807     *
808     * @param array $defaultAttr
809     * @param bool $isProgressive (optional)
810     * @return string
811     */
812    private function buildSubmitInput( $defaultAttr, $isProgressive = false ) {
813        $defaultAttr[ 'class' ] ??= '';
814        $defaultAttr[ 'class' ] .= ' cdx-button';
815        if ( $isProgressive ) {
816            $defaultAttr[ 'class' ] .= ' cdx-button--action-progressive cdx-button--weight-primary';
817        }
818        $defaultAttr[ 'class' ] = trim( $defaultAttr[ 'class' ] );
819        return Html::element( 'input', $defaultAttr );
820    }
821
822    private function bgColorStyle() {
823        if ( $this->mBGColor !== 'transparent' ) {
824            // Define color to avoid flagging linting warnings.
825            // https://phabricator.wikimedia.org/T369619
826            // Editor is assumed to know what they are doing here,
827            // and choosing a color compatible with dark and light themes...
828            return 'background-color: ' . $this->mBGColor . '; color: inherit;';
829        }
830        return '';
831    }
832
833    /**
834     * Returns true, if the VisualEditor is requested from the inputbox wikitext definition and
835     * if the VisualEditor extension is actually installed or not, false otherwise.
836     *
837     * @return bool
838     */
839    private function shouldUseVE() {
840        return ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) && $this->mUseVE !== null;
841    }
842
843    /**
844     * For compatability with pre T119158 behaviour
845     *
846     * If a field that is going to be used as an attribute
847     * and it contains "-{" in it, run it through language
848     * converter.
849     *
850     * Its not really clear if it would make more sense to
851     * always convert instead of only if -{ is present. This
852     * function just more or less restores the previous
853     * accidental behaviour.
854     *
855     * @see https://phabricator.wikimedia.org/T180485
856     * @param string $text
857     * @return string
858     */
859    private function languageConvert( $text ) {
860        $langConv = $this->mParser->getTargetLanguageConverter();
861        if ( $langConv->hasVariants() && strpos( $text, '-{' ) !== false ) {
862            $text = $langConv->convert( $text );
863        }
864        return $text;
865    }
866}