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