<?php
/**
 * Classes for InputBox extension
 *
 * @file
 * @ingroup Extensions
 */

namespace MediaWiki\Extension\InputBox;

use MediaWiki\Config\Config;
use MediaWiki\Html\Html;
use MediaWiki\MainConfigNames;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\Sanitizer;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\SpecialPage\SpecialPage;

/**
 * InputBox class
 */
class InputBox {

	/* Fields */

	/** @var Config */
	private $config;
	/** @var Parser */
	private $mParser;
	/** @var string */
	private $mType = '';
	/** @var int */
	private $mWidth = 50;
	/** @var ?string */
	private $mPreload = null;
	/** @var ?array */
	private $mPreloadparams = null;
	/** @var ?string */
	private $mEditIntro = null;
	/** @var ?string */
	private $mUseVE = null;
	/** @var ?string */
	private $mUseDT = null;
	/** @var ?string */
	private $mSummary = null;
	/** @var ?string */
	private $mNosummary = null;
	/** @var ?string */
	private $mMinor = null;
	/** @var string */
	private $mPage = '';
	/** @var string */
	private $mBR = 'yes';
	/** @var string */
	private $mDefaultText = '';
	/** @var string */
	private $mPlaceholderText = '';
	/** @var string */
	private $mBGColor = 'transparent';
	/** @var string */
	private $mButtonLabel = '';
	/** @var string */
	private $mSearchButtonLabel = '';
	/** @var string */
	private $mFullTextButton = '';
	/** @var string */
	private $mLabelText = '';
	/** @var ?string */
	private $mHidden = '';
	/** @var string */
	private $mNamespaces = '';
	/** @var string */
	private $mID = '';
	/** @var ?string */
	private $mInline = null;
	/** @var string */
	private $mPrefix = '';
	/** @var string */
	private $mDir = '';
	/** @var string */
	private $mSearchFilter = '';
	/** @var string */
	private $mTour = '';
	/** @var string */
	private $mTextBoxAriaLabel = '';

	/* Functions */

	/**
	 * @param Config $config
	 * @param Parser $parser
	 */
	public function __construct(
		Config $config,
		$parser
	) {
		$this->config = $config;
		$this->mParser = $parser;
		// Default value for dir taken from the page language (bug 37018)
		$this->mDir = $this->mParser->getTargetLanguage()->getDir();
		// Split caches by language, to make sure visitors do not see a cached
		// version in a random language (since labels are in the user language)
		$this->mParser->getOptions()->getUserLangObj();
		$this->mParser->getOutput()->addModuleStyles( [
			'ext.inputBox.styles',
		] );
	}

	public function render() {
		// Handle various types
		switch ( $this->mType ) {
			case 'create':
			case 'comment':
				return $this->getCreateForm();
			case 'move':
				return $this->getMoveForm();
			case 'commenttitle':
				return $this->getCommentForm();
			case 'search':
				return $this->getSearchForm( 'search' );
			case 'fulltext':
				return $this->getSearchForm( 'fulltext' );
			case 'search2':
				return $this->getSearchForm2();
			default:
				$key = $this->mType === '' ? 'inputbox-error-no-type' : 'inputbox-error-bad-type';
				return Html::rawElement( 'div', [],
					Html::element( 'strong',
						[ 'class' => 'error' ],
						wfMessage( $key, $this->mType )->text()
					)
				);
		}
	}

	/**
	 * Returns the action name and value to use in inputboxes which redirects to edit pages.
	 * Decides, if the link should redirect to VE edit page (veaction=edit) or to wikitext editor
	 * (action=edit).
	 *
	 * @return array Array with name and value data
	 */
	private function getEditActionArgs() {
		// default is wikitext editor
		$args = [
			'name' => 'action',
			'value' => 'edit',
		];
		// check, if VE is installed and VE editor is requested
		if ( $this->shouldUseVE() ) {
			$args = [
				'name' => 'veaction',
				'value' => 'edit',
			];
		}
		return $args;
	}

	/**
	 * Get common classes, that could be added and depend on, if
	 * a line break between a button and an input field is added or not.
	 *
	 * @return string
	 */
	private function getFormLinebreakClasses() {
		return strtolower( $this->mBR ) === '<br />' ? ' mw-inputbox-form' : ' mw-inputbox-form-inline';
	}

	/**
	 * Get common classes, that could be added and depend on, if
	 * a line break between a button and an input field is added or not.
	 *
	 * @return string
	 */
	private function getLinebreakClasses() {
		return strtolower( $this->mBR ) === '<br />' ? 'mw-inputbox-input ' : '';
	}

	/**
	 * Generate search form
	 * @param string $type
	 * @return string HTML
	 */
	public function getSearchForm( $type ) {
		// Use button label fallbacks
		if ( !$this->mButtonLabel ) {
			$this->mButtonLabel = wfMessage( 'inputbox-tryexact' )->text();
		}
		if ( !$this->mSearchButtonLabel ) {
			$this->mSearchButtonLabel = wfMessage( 'inputbox-searchfulltext' )->text();
		}
		if ( $this->mID !== '' ) {
			$idArray = [ 'id' => Sanitizer::escapeIdForAttribute( $this->mID ) ];
		} else {
			$idArray = [];
		}
		// We need a unqiue id to link <label> to checkboxes, but also
		// want multiple <inputbox>'s to not be invalid html
		$idRandStr = Sanitizer::escapeIdForAttribute( '-' . $this->mID . wfRandom() );

		// Build HTML
		$htmlOut = Html::openElement( 'div',
			[
				'class' => 'mw-inputbox-centered',
				'style' => $this->bgColorStyle(),
			]
		);
		$htmlOut .= Html::openElement( 'form',
			[
				'name' => 'searchbox',
				'class' => 'searchbox' . $this->getFormLinebreakClasses(),
				'action' => SpecialPage::getTitleFor( 'Search' )->getLocalUrl(),
			] + $idArray
		);

		$htmlOut .= $this->buildTextBox( [
			// enable SearchSuggest with mw-searchInput class
			'class' => $this->getLinebreakClasses() . 'mw-searchInput searchboxInput',
			'name' => 'search',
			'type' => $this->mHidden ? 'hidden' : 'text',
			'value' => $this->mDefaultText,
			'placeholder' => $this->mPlaceholderText,
			'size' => $this->mWidth,
			'dir' => $this->mDir
		] );

		if ( $this->mPrefix !== '' ) {
			$htmlOut .= Html::hidden( 'prefix', $this->mPrefix );
		}

		if ( $this->mSearchFilter !== '' ) {
			$htmlOut .= Html::hidden( 'searchfilter', $this->mSearchFilter );
		}

		if ( $this->mTour !== '' ) {
			$htmlOut .= Html::hidden( 'tour', $this->mTour );
		}

		$htmlOut .= $this->mBR;

		// Determine namespace checkboxes
		$namespacesArray = explode( ',', $this->mNamespaces );
		if ( $this->mNamespaces ) {
			$contLang = $this->mParser->getContentLanguage();
			$namespaces = $contLang->getNamespaces();
			$nsAliases = array_merge(
				$contLang->getNamespaceAliases(),
				$this->config->get( MainConfigNames::NamespaceAliases )
			);
			$showNamespaces = [];
			$checkedNS = [];
			// Check for valid namespaces
			foreach ( $namespacesArray as $userNS ) {
				// no whitespace
				$userNS = trim( $userNS );

				// Namespace needs to be checked if flagged with "**"
				if ( strpos( $userNS, '**' ) ) {
					$userNS = str_replace( '**', '', $userNS );
					$checkedNS[$userNS] = true;
				}

				$mainMsg = wfMessage( 'inputbox-ns-main' )->inContentLanguage()->text();
				if ( $userNS === 'Main' || $userNS === $mainMsg ) {
					$i = 0;
				} elseif ( array_search( $userNS, $namespaces ) ) {
					$i = array_search( $userNS, $namespaces );
				} elseif ( isset( $nsAliases[$userNS] ) ) {
					$i = $nsAliases[$userNS];
				} else {
					// Namespace not recognized, skip
					continue;
				}
				$showNamespaces[$i] = $userNS;
				if ( isset( $checkedNS[$userNS] ) && $checkedNS[$userNS] ) {
					$checkedNS[$i] = true;
				}
			}

			// Show valid namespaces
			foreach ( $showNamespaces as $i => $name ) {
				$checked = [];
				// Namespace flagged with "**" or if it's the only one
				if ( ( isset( $checkedNS[$i] ) && $checkedNS[$i] ) || count( $showNamespaces ) === 1 ) {
					$checked = [ 'checked' => 'checked' ];
				}

				if ( count( $showNamespaces ) === 1 ) {
					// Checkbox
					$htmlOut .= Html::element( 'input',
						[
							'type' => 'hidden',
							'name' => 'ns' . $i,
							'value' => 1,
							'id' => 'mw-inputbox-ns' . $i . $idRandStr
						] + $checked
					);
				} else {
					// Checkbox
					$htmlOut .= $this->buildCheckboxInput(
						$name, 'ns' . $i, 'mw-inputbox-ns' . $i . $idRandStr, "1", $checked
					);
				}
			}

			// Line break
			$htmlOut .= $this->mBR;
		} elseif ( $type === 'search' ) {
			// Go button
			$htmlOut .= $this->buildSubmitInput(
				[
					'type' => 'submit',
					'name' => 'go',
					'value' => $this->mButtonLabel
				]
			);
			$htmlOut .= "\u{00A0}";
		}

		// Search button
		$htmlOut .= $this->buildSubmitInput(
			[
				'type' => 'submit',
				'name' => 'fulltext',
				'value' => $this->mSearchButtonLabel
			]
		);

		// Hidden fulltext param for IE (bug 17161)
		if ( $type === 'fulltext' ) {
			$htmlOut .= Html::hidden( 'fulltext', 'Search' );
		}

		$htmlOut .= Html::closeElement( 'form' );
		$htmlOut .= Html::closeElement( 'div' );

		// Return HTML
		return $htmlOut;
	}

	/**
	 * Generate search form version 2
	 * @return string
	 */
	public function getSearchForm2() {
		// Use button label fallbacks
		if ( !$this->mButtonLabel ) {
			$this->mButtonLabel = wfMessage( 'inputbox-tryexact' )->text();
		}

		if ( $this->mID !== '' ) {
			$unescapedID = $this->mID;
		} else {
			// The label element needs a unique id, use
			// random number to avoid multiple input boxes
			// having conflicts.
			$unescapedID = wfRandom();
		}
		$id = Sanitizer::escapeIdForAttribute( $unescapedID );
		$htmlLabel = '';
		if ( strlen( trim( $this->mLabelText ) ) ) {
			$htmlLabel = Html::openElement( 'label', [ 'for' => 'bodySearchInput' . $id,
				'class' => 'mw-inputbox-label'
			] );
			$htmlLabel .= $this->mParser->recursiveTagParse( $this->mLabelText );
			$htmlLabel .= Html::closeElement( 'label' );
		}
		$htmlOut = Html::openElement( 'form',
			[
				'name' => 'bodySearch' . $id,
				'id' => 'bodySearch' . $id,
				'class' => 'bodySearch' .
					( $this->mInline ? ' mw-inputbox-inline' : '' ) . $this->getFormLinebreakClasses(),
				'action' => SpecialPage::getTitleFor( 'Search' )->getLocalUrl(),
			]
		);
		$htmlOut .= Html::openElement( 'div',
			[
				'class' => 'bodySearchWrap' . ( $this->mInline ? ' mw-inputbox-inline' : '' ),
				'style' => $this->bgColorStyle(),
			]
		);
		$htmlOut .= $htmlLabel;

		$htmlOut .= $this->buildTextBox( [
			'type' => $this->mHidden ? 'hidden' : 'text',
			'name' => 'search',
			// enable SearchSuggest with mw-searchInput class
			'class' => 'mw-searchInput',
			'size' => $this->mWidth,
			'id' => 'bodySearchInput' . $id,
			'dir' => $this->mDir,
			'placeholder' => $this->mPlaceholderText
		] );

		$htmlOut .= "\u{00A0}" . $this->buildSubmitInput(
			[
				'type' => 'submit',
				'name' => 'go',
				'value' => $this->mButtonLabel,
			]
		);

		// Better testing needed here!
		if ( $this->mFullTextButton !== '' ) {
			$htmlOut .= $this->buildSubmitInput(
				[
					'type' => 'submit',
					'name' => 'fulltext',
					'value' => $this->mSearchButtonLabel
				]
			);
		}

		$htmlOut .= Html::closeElement( 'div' );
		$htmlOut .= Html::closeElement( 'form' );

		// Return HTML
		return $htmlOut;
	}

	/**
	 * Generate create page form
	 * @return string
	 */
	public function getCreateForm() {
		if ( $this->mType === 'comment' ) {
			if ( !$this->mButtonLabel ) {
				$this->mButtonLabel = wfMessage( 'inputbox-postcomment' )->text();
			}
		} else {
			if ( !$this->mButtonLabel ) {
				$this->mButtonLabel = wfMessage( 'inputbox-createarticle' )->text();
			}
		}

		$htmlOut = Html::openElement( 'div',
			[
				'class' => 'mw-inputbox-centered',
				'style' => $this->bgColorStyle(),
			]
		);
		$createBoxParams = [
			'name' => 'createbox',
			'class' => 'createbox' . $this->getFormLinebreakClasses(),
			'action' => $this->config->get( MainConfigNames::Script ),
			'method' => 'get'
		];
		if ( $this->mID !== '' ) {
			$createBoxParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
		}
		$htmlOut .= Html::openElement( 'form', $createBoxParams );
		$editArgs = $this->getEditActionArgs();
		$htmlOut .= Html::hidden( $editArgs['name'], $editArgs['value'] );
		if ( $this->mPreload !== null ) {
			$htmlOut .= Html::hidden( 'preload', $this->mPreload );
		}
		if ( is_array( $this->mPreloadparams ) ) {
			foreach ( $this->mPreloadparams as $preloadparams ) {
				$htmlOut .= Html::hidden( 'preloadparams[]', $preloadparams );
			}
		}
		if ( $this->mEditIntro !== null ) {
			$htmlOut .= Html::hidden( 'editintro', $this->mEditIntro );
		}
		if ( $this->mSummary !== null ) {
			$htmlOut .= Html::hidden( 'summary', $this->mSummary );
		}
		if ( $this->mNosummary !== null ) {
			$htmlOut .= Html::hidden( 'nosummary', $this->mNosummary );
		}
		if ( $this->mPrefix !== '' ) {
			$htmlOut .= Html::hidden( 'prefix', $this->mPrefix );
		}
		if ( $this->mMinor !== null ) {
			$htmlOut .= Html::hidden( 'minor', $this->mMinor );
		}
		// @phan-suppress-next-line PhanSuspiciousValueComparison False positive
		if ( $this->mType === 'comment' ) {
			$htmlOut .= Html::hidden( 'section', 'new' );
			if ( $this->mUseDT ) {
				$htmlOut .= Html::hidden( 'dtpreload', '1' );
			}
		}

		$htmlOut .= $this->buildTextBox( [
			'type' => $this->mHidden ? 'hidden' : 'text',
			'name' => 'title',
			'class' => $this->getLinebreakClasses() .
				'mw-inputbox-createbox',
			'value' => $this->mDefaultText,
			'placeholder' => $this->mPlaceholderText,
			// For visible input fields, use required so that the form will not
			// submit without a value
			'required' => !$this->mHidden,
			'size' => $this->mWidth,
			'dir' => $this->mDir
		] );

		$htmlOut .= $this->mBR;
		$htmlOut .= $this->buildSubmitInput(
			[
				'type' => 'submit',
				'name' => 'create',
				'value' => $this->mButtonLabel
			],
			true
		);
		$htmlOut .= Html::closeElement( 'form' );
		$htmlOut .= Html::closeElement( 'div' );

		// Return HTML
		return $htmlOut;
	}

	/**
	 * Generate move page form
	 * @return string
	 */
	public function getMoveForm() {
		if ( !$this->mButtonLabel ) {
			$this->mButtonLabel = wfMessage( 'inputbox-movearticle' )->text();
		}

		$htmlOut = Html::openElement( 'div',
			[
				'class' => 'mw-inputbox-centered',
				'style' => $this->bgColorStyle(),
			]
		);
		$moveBoxParams = [
			'name' => 'movebox',
			'class' => 'mw-movebox' . $this->getFormLinebreakClasses(),
			'action' => $this->config->get( MainConfigNames::Script ),
			'method' => 'get'
		];
		if ( $this->mID !== '' ) {
			$moveBoxParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
		}
		$htmlOut .= Html::openElement( 'form', $moveBoxParams );
		$htmlOut .= Html::hidden( 'title',
			SpecialPage::getTitleFor( 'Movepage', $this->mPage )->getPrefixedText() );
		$htmlOut .= Html::hidden( 'wpReason', $this->mSummary );
		$htmlOut .= Html::hidden( 'prefix', $this->mPrefix );

		$htmlOut .= $this->buildTextBox( [
			'type' => $this->mHidden ? 'hidden' : 'text',
			'name' => 'wpNewTitle',
			'class' => $this->getLinebreakClasses() . 'mw-moveboxInput',
			'value' => $this->mDefaultText,
			'placeholder' => $this->mPlaceholderText,
			'size' => $this->mWidth,
			'dir' => $this->mDir
		] );

		$htmlOut .= $this->mBR;
		$htmlOut .= $this->buildSubmitInput(
			[
				'type' => 'submit',
				'value' => $this->mButtonLabel
			],
			true
		);
		$htmlOut .= Html::closeElement( 'form' );
		$htmlOut .= Html::closeElement( 'div' );

		// Return HTML
		return $htmlOut;
	}

	/**
	 * Generate new section form
	 * @return string
	 */
	public function getCommentForm() {
		if ( !$this->mButtonLabel ) {
				$this->mButtonLabel = wfMessage( 'inputbox-postcommenttitle' )->text();
		}

		$htmlOut = Html::openElement( 'div',
			[
				'class' => 'mw-inputbox-centered',
				'style' => $this->bgColorStyle(),
			]
		);
		$commentFormParams = [
			'name' => 'commentbox',
			'class' => 'commentbox' . $this->getFormLinebreakClasses(),
			'action' => $this->config->get( MainConfigNames::Script ),
			'method' => 'get'
		];
		if ( $this->mID !== '' ) {
			$commentFormParams['id'] = Sanitizer::escapeIdForAttribute( $this->mID );
		}
		$htmlOut .= Html::openElement( 'form', $commentFormParams );
		$editArgs = $this->getEditActionArgs();
		$htmlOut .= Html::hidden( $editArgs['name'], $editArgs['value'] );
		if ( $this->mPreload !== null ) {
			$htmlOut .= Html::hidden( 'preload', $this->mPreload );
		}
		if ( is_array( $this->mPreloadparams ) ) {
			foreach ( $this->mPreloadparams as $preloadparams ) {
				$htmlOut .= Html::hidden( 'preloadparams[]', $preloadparams );
			}
		}
		if ( $this->mEditIntro !== null ) {
			$htmlOut .= Html::hidden( 'editintro', $this->mEditIntro );
		}

		$htmlOut .= $this->buildTextBox( [
			'type' => $this->mHidden ? 'hidden' : 'text',
			'name' => 'preloadtitle',
			'class' => $this->getLinebreakClasses() . 'commentboxInput',
			'value' => $this->mDefaultText,
			'placeholder' => $this->mPlaceholderText,
			'size' => $this->mWidth,
			'dir' => $this->mDir
		] );

		$htmlOut .= Html::hidden( 'section', 'new' );
		if ( $this->mUseDT ) {
			$htmlOut .= Html::hidden( 'dtpreload', '1' );
		}
		$htmlOut .= Html::hidden( 'title', $this->mPage );
		$htmlOut .= $this->mBR;
		$htmlOut .= $this->buildSubmitInput(
			[
				'type' => 'submit',
				'name' => 'create',
				'value' => $this->mButtonLabel
			],
			true
		);
		$htmlOut .= Html::closeElement( 'form' );
		$htmlOut .= Html::closeElement( 'div' );

		// Return HTML
		return $htmlOut;
	}

	/**
	 * Extract options from a blob of text
	 *
	 * @param string $text Tag contents
	 */
	public function extractOptions( $text ) {
		// Parse all possible options
		$values = [];
		foreach ( explode( "\n", $text ) as $line ) {
			if ( strpos( $line, '=' ) === false ) {
				continue;
			}
			[ $name, $value ] = explode( '=', $line, 2 );
			$name = strtolower( trim( $name ) );
			$value = Sanitizer::decodeCharReferences( trim( $value ) );
			if ( $name === 'preloadparams[]' ) {
				// We have to special-case this one because it's valid for it to appear more than once.
				$this->mPreloadparams[] = $value;
			} else {
				$values[ $name ] = $value;
			}
		}

		// Validate the dir value.
		if ( isset( $values['dir'] ) && !in_array( $values['dir'], [ 'ltr', 'rtl' ] ) ) {
			unset( $values['dir'] );
		}

		// Build list of options, with local member names
		$options = [
			'type' => 'mType',
			'width' => 'mWidth',
			'preload' => 'mPreload',
			'page' => 'mPage',
			'editintro' => 'mEditIntro',
			'useve' => 'mUseVE',
			'usedt' => 'mUseDT',
			'summary' => 'mSummary',
			'nosummary' => 'mNosummary',
			'minor' => 'mMinor',
			'break' => 'mBR',
			'default' => 'mDefaultText',
			'placeholder' => 'mPlaceholderText',
			'bgcolor' => 'mBGColor',
			'buttonlabel' => 'mButtonLabel',
			'searchbuttonlabel' => 'mSearchButtonLabel',
			'fulltextbutton' => 'mFullTextButton',
			'namespaces' => 'mNamespaces',
			'labeltext' => 'mLabelText',
			'hidden' => 'mHidden',
			'id' => 'mID',
			'inline' => 'mInline',
			'prefix' => 'mPrefix',
			'dir' => 'mDir',
			'searchfilter' => 'mSearchFilter',
			'tour' => 'mTour',
			'arialabel' => 'mTextBoxAriaLabel'
		];
		// Options we should maybe run through lang converter.
		$convertOptions = [
			'default' => true,
			'buttonlabel' => true,
			'searchbuttonlabel' => true,
			'placeholder' => true,
			'arialabel' => true
		];
		foreach ( $options as $name => $var ) {
			if ( isset( $values[$name] ) ) {
				$this->$var = $values[$name];
				if ( isset( $convertOptions[$name] ) ) {
					$this->$var = $this->languageConvert( $this->$var );
				}
			}
		}

		// Insert a line break if configured to do so
		$this->mBR = ( strtolower( $this->mBR ) === 'no' ) ? ' ' : '<br />';

		// Validate the width; make sure it's a valid, positive integer
		$this->mWidth = intval( $this->mWidth <= 0 ? 50 : $this->mWidth );

		// Validate background color
		if ( !$this->isValidColor( $this->mBGColor ) ) {
			$this->mBGColor = 'transparent';
		}

		// T297725: De-obfuscate attempts to trick people into making edits to .js pages
		$target = $this->mType === 'commenttitle' ? $this->mPage : $this->mDefaultText;
		if ( $this->mHidden && $this->mPreload && substr( $target, -3 ) === '.js' ) {
			$this->mHidden = null;
		}
	}

	/**
	 * Do a security check on the bgcolor parameter
	 * @param string $color
	 * @return bool
	 */
	public function isValidColor( $color ) {
		$regex = <<<REGEX
			/^ (
				[a-zA-Z]* |       # color names
				\# [0-9a-f]{3} |  # short hexadecimal
				\# [0-9a-f]{6} |  # long hexadecimal
				rgb \s* \( \s* (
					\d+ \s* , \s* \d+ \s* , \s* \d+ |    # rgb integer
					[0-9.]+% \s* , \s* [0-9.]+% \s* , \s* [0-9.]+%   # rgb percent
				) \s* \)
			) $ /xi
REGEX;
		return (bool)preg_match( $regex, $color );
	}

	/**
	 * Factory method to help build the textbox widget.
	 *
	 * @param array $defaultAttr
	 * @return string
	 */
	private function buildTextBox( $defaultAttr ) {
		if ( $this->mTextBoxAriaLabel ) {
			$defaultAttr[ 'aria-label' ] = $this->mTextBoxAriaLabel;
		}

		$class = $defaultAttr[ 'class' ] ?? '';
		$class .= ' cdx-text-input__input';
		$defaultAttr[ 'class' ] = $class;
		return Html::openElement( 'div', [
			'class' => 'cdx-text-input',
		] )
			. Html::element( 'input', $defaultAttr )
			. Html::closeElement( 'div' );
	}

	/**
	 * Factory method to help build checkbox input.
	 *
	 * @param string $label text displayed next to checkbox (label)
	 * @param string $name name of input
	 * @param string $id id of input
	 * @param string $value value of input
	 * @param array $defaultAttr (optional)
	 * @return string
	 */
	private function buildCheckboxInput( $label, $name, $id, $value, $defaultAttr = [] ) {
		$htmlOut = ' <span class="cdx-checkbox cdx-checkbox--inline">';
		$htmlOut .= Html::element( 'input',
			[
				'type' => 'checkbox',
				'name' => $name,
				'value' => $value,
				'id' => $id,
				'class' => 'cdx-checkbox__input',
			] + $defaultAttr
		);
		$htmlOut .= '<span class="cdx-checkbox__icon"></span>';
		// Label
		$htmlOut .= Html::label( $label, $id, [
			'class' => 'cdx-checkbox__label',
		] );
		$htmlOut .= '</span> ';
		return $htmlOut;
	}

	/**
	 * Factory method to help build submit button.
	 *
	 * @param array $defaultAttr
	 * @param bool $isProgressive (optional)
	 * @return string
	 */
	private function buildSubmitInput( $defaultAttr, $isProgressive = false ) {
		$defaultAttr[ 'class' ] ??= '';
		$defaultAttr[ 'class' ] .= ' cdx-button';
		if ( $isProgressive ) {
			$defaultAttr[ 'class' ] .= ' cdx-button--action-progressive cdx-button--weight-primary';
		}
		$defaultAttr[ 'class' ] = trim( $defaultAttr[ 'class' ] );
		return Html::element( 'input', $defaultAttr );
	}

	private function bgColorStyle() {
		if ( $this->mBGColor !== 'transparent' ) {
			// Define color to avoid flagging linting warnings.
			// https://phabricator.wikimedia.org/T369619
			// Editor is assumed to know what they are doing here,
			// and choosing a color compatible with dark and light themes...
			return 'background-color: ' . $this->mBGColor . '; color: inherit;';
		}
		return '';
	}

	/**
	 * Returns true, if the VisualEditor is requested from the inputbox wikitext definition and
	 * if the VisualEditor extension is actually installed or not, false otherwise.
	 *
	 * @return bool
	 */
	private function shouldUseVE() {
		return ExtensionRegistry::getInstance()->isLoaded( 'VisualEditor' ) && $this->mUseVE !== null;
	}

	/**
	 * For compatability with pre T119158 behaviour
	 *
	 * If a field that is going to be used as an attribute
	 * and it contains "-{" in it, run it through language
	 * converter.
	 *
	 * Its not really clear if it would make more sense to
	 * always convert instead of only if -{ is present. This
	 * function just more or less restores the previous
	 * accidental behaviour.
	 *
	 * @see https://phabricator.wikimedia.org/T180485
	 * @param string $text
	 * @return string
	 */
	private function languageConvert( $text ) {
		$langConv = $this->mParser->getTargetLanguageConverter();
		if ( $langConv->hasVariants() && strpos( $text, '-{' ) !== false ) {
			$text = $langConv->convert( $text );
		}
		return $text;
	}
}
