<?php
/**
 * Input functions
 *
 * @package RosarioSIS
 * @subpackage functions
 */

/**
 * Date Input
 *
 * @example DateInput( DBDate(), '_values[CATEGORY_' . $category['ID'] . ']' )
 *
 * @uses PrepareDate() to display Month / Day / Year Select fields + JSCalendar integration
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && $value != '' && $div )
 *
 * @param  string         $value    Input value.
 * @param  string         $name     Input name.
 * @param  string         $title    Input title (optional). Defaults to ''.
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 * @param  boolean        $allow_na Allow N/A (empty value) (optional). Defaults to true.
 * @param  boolean        $required Required date fields (optional). Defaults to false.
 *
 * @return string         Input HTML
 */
function DateInput( $value, $name, $title = '', $div = true, $allow_na = true, $required = false )
{
	$id = GetInputID( $name );

	$ftitle = FormatInputTitle( $title, '', $value == '' && $required );

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return ( $value != '' ? ProperDate( $value ) : '-' ) . FormatInputTitle( $title );
	}

	$options = [
		'required' => $required,
	];

	if ( $value == ''
		|| ! $div )
	{
		return PrepareDate( $value, '_' . $name, $allow_na, $options ) . $ftitle;
	}

	$options = $options + [ 'Y' => 1, 'M' => 1, 'D' => 1 ];

	$input = PrepareDate( $value, '_' . $name, $allow_na, $options ) . $ftitle;

	return InputDivOnclick(
		$id,
		$input,
		( $value != '' ? ProperDate( $value ) : '-' ),
		FormatInputTitle( $title )
	);
}


/**
 * Text Input
 *
 * @example TextInput( Config( 'NAME' ), 'values[config][NAME]', _( 'Program Name' ), 'required' )
 *
 * @since 12.0 HTML Omit value attribute when empty
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && $value != '' && $div )
 *
 * @param  string  $value Input value.
 * @param  string  $name  Input name.
 * @param  string  $title Input title (optional). Defaults to ''.
 * @param  string  $extra Extra HTML attributes added to the input.
 * @param  boolean $div   Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string  Input HTML
 */
function TextInput( $value, $name, $title = '', $extra = '', $div = true )
{
	$id = GetInputID( $name );

	// mab - support array style $option values
	$display_val = is_array( $value ) ? $value[1] : $value;

	$value = is_array( $value ) ? $value[0] : $value;

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return ( $value != '' ? $display_val : '-' ) . FormatInputTitle( $title );
	}

	// Input size / length based on value number of chars
	if ( mb_strpos( $extra, 'size=' ) === false )
	{
		// Max size is 32 (more or less 300px)
		$size = min( mb_strlen( (string) $value ), 32 );

		// Min size is 2 (more or less 35px)
		$size = max( $size, 2 );

		$extra .= $value != '' ? ' size="' . $size . '"' : ' size="12"';
	}

	// Specify input type via $extra (email,...).
	$type = mb_strpos( $extra, 'type=' ) === false ? ' type="text"' : '';

	$input = '<input' . $type . ' id="' . $id . '" name="' . AttrEscape( $name ) .
		'"' . ( $value != '' ? ' value="' . AttrEscape( $value ) . '"' : '' ) . ' ' . $extra . '>' .
		FormatInputTitle( $title, $id, $required );

	if ( is_null( $value )
		|| trim( $value ) == ''
		|| ! $div )
	{
		return $input;
	}

	return InputDivOnclick(
		$id,
		$input,
		( $value != '' ? $display_val : '-' ),
		FormatInputTitle( $title )
	);
}


/**
 * Password Input
 *
 * @example echo PasswordInput( '********', 'PASSWORD', _( 'Password' ), 'required strength' );
 *
 * @since 4.4
 * @since 5.5.1 Fill input value if $value != '********'.
 * @since 11.1 Prevent using App name, username, or email in the password
 *
 * @global $_ROSARIO['PasswordInput']['user_inputs'] used in PasswordReset.php, FirstLogin.fnc.php & Preferences.php
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && $value != '' && $div )
 * @uses TextInput
 *
 * @param  string  $value Input value.
 * @param  string  $name  Input name.
 * @param  string  $title Input title (optional). Defaults to ''.
 * @param  string  $extra Extra HTML attributes. Pass 'strength' to display strength indicator.
 * @param  boolean $div   Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string  Input HTML
 */
function PasswordInput( $value, $name, $title = '', $extra = '', $div = true )
{
	global $_ROSARIO;

	static $zxcvbn_included = false,
		$js_included = false;

	$id = GetInputID( $name );

	$strength = ( mb_strpos( $extra, 'strength' ) !== false );

	// mab - support array style $option values
	$display_val = is_array( $value ) ? $value[1] : $value;

	$value = is_array( $value ) ? $value[0] : $value;

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return ( $value != '' ? $display_val : '-' ) . FormatInputTitle( $title );
	}

	// Default input size.
	if ( $value == ''
		&& mb_strpos( $extra, 'size=' ) === false )
	{
		$extra .= ' size="17"';
	}
	elseif ( mb_strpos( $extra, 'size=' ) === false )
	{
		$extra .= ' size="' . ( strlen( $value ) + 5 ) . '"';
	}

	$extra .= ' type="password" autocomplete="new-password"';

	$js = '';

	$min_required_strength = $strength ? Config( 'PASSWORD_STRENGTH' ) : 0;

	if ( $min_required_strength )
	{
		// @since 12.5 CSP remove unsafe-inline Javascript
		$extra .= ' data-min-strength="' . AttrEscape( $min_required_strength ) . '"';

		// Error message when trying to submit the form.
		$extra .= ' data-error="' . AttrEscape( _( 'Your password must be stronger.' ) ) . '"';

		// @since 11.1 Prevent using App name, username, or email in the password
		$user_inputs = array_merge(
			[ Config( 'NAME' ) ],
			// Add username & email to this global var before calling PasswordInput().
			issetVal( $_ROSARIO['PasswordInput']['user_inputs'], [] )
		);

		$extra .= ' data-user-inputs="' . AttrEscape( json_encode( $user_inputs ) ) . '"';

		if ( ! $zxcvbn_included )
		{
			// @since 12.5 CSP remove unsafe inline Javascript
			$js .= '<script src="assets/js/zxcvbn/zxcvbn.js"></script>';

			$zxcvbn_included = true;
		}
	}

	if ( ! $js_included )
	{
		// Call our jQuery PasswordStrength plugin based on zxcvbn.
		$js .= '<script src="assets/js/csp/functions/PasswordInput.js?v=12.5"></script>';

		$js_included = true;
	}

	$input = TextInput( ( $value !== str_repeat( '*', 8 ) ? $value : '' ), $name, '', $extra, false );

	$lock_icons = button( 'unlocked', '', '', 'password-toggle password-show' ) .
		button( 'locked', '', '', 'password-toggle password-hide' );

	$password_strength_bars = '';

	if ( $min_required_strength )
	{
		$password_strength_bars = '<div class="password-strength-bars">
			<span class="score0"></span>
			<span class="score1"></span>
			<span class="score2"></span>
			<span class="score3"></span>
			<span class="score4"></span>
		</div>';
	}

	$input .= $lock_icons . $password_strength_bars .
		FormatInputTitle( $title, $id, $required ) . $js;

	$input = '<div class="password-input-wrapper">' . $input . '</div>';

	if ( is_null( $value )
		|| trim( $value ) == ''
		|| ! $div )
	{
		return $input;
	}

	return InputDivOnclick(
		$id,
		$input,
		( $value != '' ? $display_val : '-' ),
		FormatInputTitle( $title )
	);
}

/**
 * Multi Languages Text Input
 *
 * @since 5.5.2 Fix save first language in ML fields if not en_US.utf8.
 * @since 12.3 Fix use of 'required' in $extra
 *
 * @example MLTextInput( Config( 'TITLE' ), 'values[config][TITLE]', _( 'Program Title' ) )
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 *
 * @uses ParseMLField() to get localized options
 *
 * @global $RosarioLocales Returns simple TextInput() if only 1 locale set
 *
 * @param  string  $value Input value.
 * @param  string  $name  Input name.
 * @param  string  $title Input title (optional). Defaults to ''.
 * @param  string  $extra Extra HTML attributes added to the input.
 * @param  boolean $div   Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string  Input HTML
 */
function MLTextInput( $value, $name, $title = '', $extra = '', $div = true )
{
	global $RosarioLocales;

	$value = is_array( $value ) ? $value[0] : $value;

	if ( count( $RosarioLocales ) < 2 )
	{
		return TextInput( ParseMLField( $value, $_SESSION['locale'] ), $name, $title, $extra, $div );
	}

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	// Remove 'required' from $extra. Manually added for first locale input only, see below.
	$extra = str_replace( 'required', '', $extra );

	$id = GetInputID( $name );

	if ( AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		// Ng - foreach possible language.
		// @since 12.5 CSP remove unsafe-inline Javascript
		$return = '<script src="assets/js/csp/functions/MLTextInput.js?12.5"></script>';

		$return .= '<div class="ml-text-input"><input type="hidden" id="' . $id . '" name="' .
			AttrEscape( $name ) . '" value="' . AttrEscape( $value ) . '" autocomplete="off">';

		if ( mb_strpos( $extra, 'size=' ) === false
			&& $value != '' )
		{
			// MLInput size based on current locale value length.
			$extra .=  ' size="' . mb_strlen( ParseMLField( $value ) ) . '"';
		}

		foreach ( (array) $RosarioLocales as $key => $loc )
		{
			$language = function_exists( 'locale_get_display_language' ) ?
				ucfirst( locale_get_display_language( $loc, $_SESSION['locale'] ) ) :
				str_replace( '.utf8', '', $loc );

			$return .= '<label><img src="locale/' . $loc . '/flag.png" class="button bigger" alt="' . AttrEscape( $language ) . '" title="' . AttrEscape( $language ) . '"> ';

			$return .= TextInput(
				ParseMLField( $value, $loc ),
				'ML_' . $name . '[' . $loc . ']',
				'',
				$extra . ( $key == 0 && $required ? ' required' : '' ) .
					' data-id="' . AttrEscape( $id ) . '" data-loc="' . AttrEscape( $loc ) . '"',
				false
			);

			$return .= '</label><br>';
		}

		$return .= '</div>';

		$title_break = '';
	}
	else
	{
		$return = ParseMLField( $value );

		$title_break = '<br>';
	}

	return $return . FormatInputTitle( $title, '', $required, $title_break );
}


/**
 * Textarea Input
 *
 * @example TextAreaInput( $RET[1]['DESCRIPTION'], 'values[DESCRIPTION]' )
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *
 * @uses MarkDownInputPreview()
 *
 * @uses ShowDown jQuery plugin for MarkDown rendering called using the .markdown-to-html CSS class
 *
 * @param  string  $value    Input value.
 * @param  string  $name     Input name.
 * @param  string  $title    Input title (optional). Defaults to ''.
 * @param  string  $extra    Extra HTML attributes added to the input.
 * @param  boolean $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 * @param  string  $type     markdown|tinymce|text Text Type (optional). Defaults to 'markdown'.
 *
 * @return string  Input HTML
 */
function TextAreaInput( $value, $name, $title = '', $extra = '', $div = true, $type = 'markdown' )
{
	$id = GetInputID( $name );

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	$ftitle = FormatInputTitle( $title, $id, $required );

	$ftitle_nobr = FormatInputTitle( $title, $id, $required, '' );

	if ( $type === 'tinymce'
		&& mb_strpos( (string) $extra, 'required' ) !== false )
	{
		// Remove required attribute, TinyMCE bug.
		$extra = str_replace( 'required', '', $extra );
	}

	$display_val = '-';

	if ( $value != '' )
	{
		$display_val = nl2br( $value );

		if ( $type === 'markdown' )
		{
			// Convert MarkDown to HTML.
			$display_val = '<div class="markdown-to-html">' . $value . '</div>';
		}
		elseif ( $type === 'tinymce' )
		{
			$display_val = '<div class="tinymce-html">' . $value . '</div>';
		}
	}

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $display_val . ( $type !== 'text' && $display_val !== '-' ?
			FormatInputTitle( $title, '', false, '' ) :
			FormatInputTitle( $title ) );
	}

	// Columns.
	/*if ( mb_strpos( $extra, 'cols' ) === false )
	{
		$extra .= ' cols=30';
		$cols = 30;
	}
	else
		$cols = mb_substr( $extra, mb_strpos( $extra, 'cols' ) + 5, 2 ) *1;*/

	// Rows.
	if ( mb_strpos( $extra, 'rows' ) === false )
	{
		$extra .= ' rows=5';
	}

	$textarea =  ( $type === 'markdown' ? MarkDownInputPreview( $id ) : '' ) .
		'<textarea id="' . $id . '" name="' . AttrEscape( $name ) . '" ' . $extra . '>' .
		// Fix Stored XSS security issue: escape textarea HTML.
		htmlspecialchars(
			// Prevent double encoding single quote (&#039;), was encoded by SanitizeHTML() or SanitizeMarkDown()
			str_replace( '&#039;', "'", (string) $value )
		) .
		'</textarea>' . ( $type === 'tinymce' ? $ftitle_nobr : $ftitle );

	if ( $value == ''
		|| ! $div )
	{
		return $textarea;
	}

	return InputDivOnclick(
		$id,
		$textarea,
		$display_val,
		FormatInputTitle( $title )
	);
}



/**
 * TinyMCE Input (HTML editor)
 *
 * Note: if you will pass additional CSS classes in the `$extra` parameter
 * Add the `tinymce-horizontal` class for Horizontal PDF.
 * Do not forget the `tinymce` class required to trigger TinyMCE.
 *
 * @todo Fix <label>, see http://stackoverflow.com/questions/4258701/tinymce-accessibility-label-for
 * @todo Allow passing options to TinyMCE (plugins, ...)
 *
 * @example TinyMCEInput( $RET[1]['TEMPLATE'], 'tinymce_textarea' )
 * @example TinyMCEInput( $html, 'tinymce_horizontal', _( 'Horizontal PDF' ), 'class="tinymce-horizontal"' )
 * @example TinyMCEInput( $html, 'letter_text', _( 'Letter Text' ), 'class="tinymce-document"' )
 *
 * @uses TextAreaInput()
 *
 * @see TinyMCE Javascript plugin for HTML edition in assets/js/tinymce/
 *
 * @since 2.9
 * @since 12.5 Fix add .tinymce CSS class when class already set in $extra param
 *
 * @global $locale Locale to translate TinyMCE interface.
 *
 * @param  string  $value    Input value.
 * @param  string  $name     Input name.
 * @param  string  $title    Input title (optional). Defaults to ''.
 * @param  string  $extra    Extra HTML attributes added to the input (optional). Defaults to ''.
 *
 * @return string  Input HTML
 */
function TinyMCEInput( $value, $name, $title = '', $extra = '' )
{
	static $js_included = false;

	$div = false;

	$wrapper = '';

	if ( ! $extra
		|| mb_strpos( $extra, 'class=' ) === false )
	{
		$extra .= ' class="tinymce"';
	}
	elseif ( mb_strpos( $extra, 'class=' ) !== false )
	{
		$extra = str_replace(
			[ 'class="', "class='" ],
			[ 'class="tinymce ', "class='tinymce " ],
			$extra
		);
	}

	if ( mb_strpos( (string) $extra, 'class=' ) !== false )
	{
		// If has .tinymce-horizontal class, add wrapper, needed here.
		if ( mb_strpos( (string) $extra, 'tinymce-horizontal' ) !== false )
		{
			$extra = str_replace(
				'tinymce-horizontal',
				'',
				$extra
			);

			$wrapper = '<div class="tinymce-horizontal">';
		}
	}

	$textarea = TextAreaInput( $value, $name, $title, $extra, $div, 'tinymce' );

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $textarea;
	}

	if ( $wrapper )
	{
		$textarea = $wrapper . $textarea . '</div>';
	}

	$tinymce_js = '';

	if ( ! $js_included )
	{
		$language = '';

		if ( file_exists( 'assets/js/tinymce/langs/' . mb_substr( $_SESSION['locale'], 0, 2 ) . '.js' ) )
		{
			// For example: es (Spanish).
			$language = mb_substr( $_SESSION['locale'], 0, 2 );
		}
		elseif ( file_exists( 'assets/js/tinymce/langs/' . mb_substr( $_SESSION['locale'], 0, 5 ) . '.js' ) )
		{
			// For example: fr_FR (French).
			$language = mb_substr( $_SESSION['locale'], 0, 5 );
		}

		// Include main TinyMCE javascript
		// and its configuration (plugin, language...).
		ob_start(); ?>

		<script src="assets/js/tinymce/tinymce.min.js?v=4.9.8"></script>
		<input type="hidden" disabled id="tinymce_language" value="<?php echo AttrEscape( $language ); ?>" />
		<script src="assets/js/csp/functions/TinyMCEInput.js?v=12.5"></script>
		<?php
		/**
		 * TinyMCE before init action hook
		 *
		 * @since 5.3
		 */
		do_action( 'functions/Inputs.php|tinymce_before_init' );

		$tinymce_js = ob_get_clean();

		$js_included = true;
	}

	return $tinymce_js . $textarea;
}


/**
 * Adds MarkDown preview to <textarea> input fields
 * Adds Write & Preview tabs + MD button above the field
 *
 * @uses   MarkDownInputPreview() Javascript function
 * @see    warehouse.js, and below for AJAX calls handling
 * @since  2.9
 *
 * @param  string $input_id input ID attribute value.
 *
 * @return HTML   preview link & preview DIV
 */
function MarkDownInputPreview( $input_id )
{
	static $js_included = false;

	if ( ! $input_id )
	{
		return false;
	}

	if ( ! $js_included )
	{
		// @since 12.5 CSP remove unsafe-inline Javascript
		?>
		<script src="assets/js/csp/functions/MarkDownInputPreview.js?v=12.5"></script>
		<?php

		$js_included = true;
	}

	ob_start();

	?>
	<div class="md-preview">
		<a href="#!" data-id="<?php echo AttrEscape( $input_id ); ?>" class="tab disabled"><?php echo _( 'Write' ); ?></a>

		<a href="#!" data-id="<?php echo AttrEscape( $input_id ); ?>" class="tab"><?php echo _( 'Preview' ); ?></a>

		<a href="https://gitlab.com/francoisjacquet/rosariosis/wikis/Markdown-Cheatsheet" title="<?php echo AttrEscape( _( 'Mastering MarkDown' ) ); ?>" target="_blank" class="md-link">
			<img class="button" src="assets/themes/<?php echo Preferences( 'THEME' ); ?>/btn/md_button.png" alt="<?php echo AttrEscape( _( 'Mastering MarkDown' ) ); ?>">
		</a>
		<div class="markdown-to-html" id="<?php echo GetInputID( 'divMDPreview' . $input_id ); ?>"></div>
	</div>
	<?php

	return ob_get_clean();
}


/**
 * Checkbox Input
 *
 * @example CheckboxInput( $value, 'values[' . $id . '][' . $name . ']', '', $value, $id == 'new', button( 'check' ), button( 'x' ) );
 *
 * @uses GetInputID() to generate ID from name
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && ! $new && $div )
 *
 * @param  string  $value   Input value.
 * @param  string  $name    Input name.
 * @param  string  $title   Input title (optional). Defaults to ''.
 * @param  string  $checked Deprecated.
 * @param  boolean $new     New input (optional). Defaults to false.
 * @param  string  $yes     Checked value text (optional). Defaults to 'Yes'.
 * @param  string  $yes     Not checked value text (optional). Defaults to 'No'.
 * @param  string  $extra   Extra HTML attributes added to the input.
 * @param  boolean $div     Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string  Input HTML
 */
function CheckboxInput( $value, $name, $title = '', $checked = '', $new = false, $yes = 'Yes', $no = 'No', $div = true, $extra = '' )
{
	$checked = '';

	// $checked has been deprecated -- it remains only as a placeholder.
	if ( $value
		&& $value !== 'N' )
	{
		$checked = ' checked';
	}

	if ( AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		$id = GetInputID( $name );

		$checkbox = '<input type="hidden" name="' . AttrEscape( $name ) . '" value="">' . // Save unchecked value!
			'<label class="checkbox-label">
			<input type="checkbox" name="' . AttrEscape( $name ) . '" id="' . $id . '" value="Y"' . $checked . ' ' . $extra . '>&nbsp;' .
			$title . '</label>';

		if ( $new
			|| ! $div )
		{
			return $checkbox;
		}

		return InputDivOnclick(
			$id,
			$checkbox,
			( $value ?
				( $yes === 'Yes' ? _( 'Yes' ) : $yes ) :
				( $no === 'No' ? _( 'No' ) : $no ) ),
			'&nbsp;<span class="checkbox-label">' . $title . '</span>'
		);
	}

	return ( $value ?
		( $yes === 'Yes' || isset( $_REQUEST['LO_save'] ) ? _( 'Yes' ) : $yes ) :
		( $no === 'No' || isset( $_REQUEST['LO_save'] ) ? _( 'No' ) : $no ) ) .
			( $title !== '' ? ' ' . $title : '' );
}


/**
 * Multiple Checkbox Input
 * Do not forget to add '[]' (array) after your input name.
 *
 * @since 4.2
 * @since 4.5 Allow associative $options array.
 * @since 6.1 Allow numeric key (ID) in associative $options array.
 * @since 10.8.1 Fix save all unchecked, add hidden empty checkbox
 * @since 12.4.1 Fix display value text when associative $options array
 * @since 12.5 JS required group of checkboxes
 *
 * @example MultipleCheckboxInput( $value, 'values[' . $id . '][' . $name . '][]' );
 *
 * @uses GetInputID() to generate ID from name
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && ! $new && $div )
 *
 * @param  string  $value   Input value(s), delimited by 2 pipes. For example: '||Value1||Value2||'.
 * @param  string  $name    Input name.
 * @param  string  $title   Input title (optional). Defaults to ''.
 * @param  array   $options Input options: [ 'option_value' => 'option_text' ].
 * @param  string  $extra   Extra HTML attributes added to the input. 'required' means at least 1 checked (optional).
 * @param  boolean $div     Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string  Inputs HTML
 */
function MultipleCheckboxInput( $value, $name, $title, $options, $extra = '', $div = true )
{
	static $js_included = false;

	$id = GetInputID( $name );

	$required = mb_strpos( $extra, 'required' ) !== false; // Note: do not check for $value == '' yet.

	/**
	 * Remove 'required' from $extra.
	 * Want don't want all checkboxes checked. We likely want at least 1 checked.
	 *
	 * @since 12.5
	 */
	$extra = str_replace( 'required', '', $extra );

	$associative_array = $options !== array_values( $options );

	if ( $associative_array
		&& $value != '' )
	{
		$values = explode( '||', trim( $value, '|' ) );

		$multiple_value = array_intersect_key( $options, array_flip( $values ) );

		$multiple_value = implode( ', ', $multiple_value );
	}
	else
	{
		$multiple_value = ( $value != '' ) ?
			str_replace( '||', ', ', mb_substr( $value, 2, -2 ) ) :
			'-';
	}

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $multiple_value . FormatInputTitle( $title );
	}

	$js = '';

	if ( $required
		&& count( $options ) > 1
		&& ! $js_included )
	{
		/**
		 * JS required group of checkboxes
		 * Alert user on form submit if no checkboxes in the group are checked.
		 *
		 * @link https://stackoverflow.com/questions/6218494/using-the-html5-required-attribute-for-a-group-of-checkboxes
		 *
		 * @since 12.5
		 */
		$js = '<script src="assets/js/csp/functions/MultipleCheckboxInput.js?v12.5"></script>';

		$alert = _( 'Please check at least one checkbox in the group.' );

		// Disabled input value used by JS.
		$js .= '<input type="hidden" disabled id="multiple_checkbox_input_required_alert" value="' .
			AttrEscape( $alert ) . '" />';

		$js_included = true;
	}

	$multiple_html = '<table class="cellspacing-0 cellpadding-5 multiple-checkbox-input ' .
		( $required ? 'required' : '' ) . '"><tr class="st">';

	$i = 0;

	foreach ( (array) $options as $option_value => $option )
	{
		if ( $i++ % 3 == 0 && $i > 1 )
		{
			$multiple_html .= '</tr><tr class="st">';
		}

		if ( ! $associative_array )
		{
			// Not an associative array, use Text as value.
			$option_value = $option;
		}

		$multiple_html .= '<td><label>
			<input type="checkbox" name="' . AttrEscape( $name ) . '"
				value="' . AttrEscape( $option_value ) . '" ' . $extra . ' ' .
				( $option != '' && mb_strpos( (string) $value, '||' . $option_value . '||' ) !== false ? ' checked' : '' ) . '>&nbsp;' .
			( $option != '' ? $option : '-' ) .
		'</label></td>';
	}

	$multiple_html .= '</tr></table>' . FormatInputTitle(
		$title,
		'',
		( $required && $value == '' ),
		''
	);

	$multiple_html .= '<input type="hidden" name="' . AttrEscape( $name ) . '" value="">';

	if ( trim( (string) $value, '|' ) == ''
		|| ! $div )
	{
		return $multiple_html . $js;
	}

	return InputDivOnclick(
		$id,
		$multiple_html,
		$multiple_value,
		FormatInputTitle( $title )
	) . $js;
}


/**
 * Select Input
 *
 * @since 5.6 Support option groups (`<optgroup>`) by adding 'group' to $extra.
 * @since 6.0 Support multiple values.
 * @since 10.8.4 Fix save multiple SelectInput() when none selected, add hidden empty input (only if $allow_na)
 *
 * @example SelectInput( $value, 'values[' . $id . '][' . $name . ']', '', $options, 'N/A', $extra )
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && $value != '' && $div )
 *
 * @param  string         $values   Input value(s).
 * @param  string         $name     Input name.
 * @param  string         $title    Input title (optional). Defaults to ''.
 * @param  array          $options  Input options: array( option_value => option_text ) or with groups: array( group_name => array( option_value => option_text ) ).
 * @param  string|boolean $allow_na Allow N/A (empty value); set to false to disallow (optional). Defaults to N/A.
 * @param  string         $extra    Extra HTML attributes added to the input. Add 'group' to enable options grouping.
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string         Input HTML
 */
function SelectInput( $values, $name, $title = '', $options = [], $allow_na = 'N/A', $extra = '', $div = true )
{
	$id = GetInputID( $name );

	$required = $values == '' && mb_strpos( (string) $extra, 'required' ) !== false;

	$is_multiple = is_array( $options ) && mb_strpos( (string) $extra, 'multiple' ) !== false;

	$values = $is_multiple ?
		(array) $values :
		// Mab - support array style $option values.
		( is_array( $values ) ? [ $values[0] ] : [ $values ] );

	$make_display_val = function( $values, $options )
	{
		$display_val = [];

		foreach ( (array) $values as $value )
		{
			if ( is_null( $value ) )
			{
				// Fix PHP8.5 deprecated using null as an array offset, use an empty string instead
				$value = '';
			}

			if ( isset( $options[ $value ] ) )
			{
				$display_val[] = is_array( $options[ $value ] ) ? $options[ $value ][1] : $options[ $value ];
			}
		}

		return implode( ', ', $display_val );
	};

	$is_group = is_array( $options ) && is_array( reset( $options ) ) && mb_strpos( $extra, 'group' ) !== false;

	if ( $is_group )
	{
		$display_val = [];

		foreach ( (array) $options as $group_options )
		{
			$display_value = $make_display_val( $values, $group_options );

			if ( $display_value )
			{
				$display_val[] = $display_value;
			}
		}

		$display_val = implode( ', ', $display_val );
	}
	else
	{
		$display_val = $make_display_val( $values, $options );
	}

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		if ( $display_val == '' )
		{
			$display_val = '-';

			if ( $allow_na !== false )
			{
				$display_val = $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na;
			}
		}

		return $display_val . FormatInputTitle( $title );
	}

	$select = '<select name="' . AttrEscape( $name ) . '" id="' . $id . '" ' . $extra . '>';

	if ( $allow_na !== false )
	{
		$select .= '<option value="">' . ( $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na ) . '</option>';
	}

	$make_option = function( $values, $key, $val )
	{
		$selected = '';

		$key .= '';

		foreach ( (array) $values as $value )
		{
			if ( $value == $key
				&& ( !( $value == false && $value !== $key )
					|| ( $value === '0' && $key === 0 ) ) )
			{
				$selected = ' selected';

				break;
			}
		}

		return '<option value="' . AttrEscape( $key ) . '"' .
			$selected . '>' . ( is_array( $val ) ? $val[0] : $val ) . '</option>';
	};

	if ( $is_group )
	{
		foreach ( (array) $options as $group => $group_options )
		{
			$select .= '<optgroup label="' . AttrEscape( $group ) . '">';

			foreach ( (array) $group_options as $key => $val )
			{
				$select .= $make_option( $values, $key, $val );
			}

			$select .= '</optgroup>';
		}
	}
	else
	{
		// Mab - append current val to select list if not in list.
		if ( ! $is_multiple
			&& $values[0] != ''
			&& ( ! is_array( $options )
				|| ! array_key_exists( $values[0], $options ) ) )
		{
			$options[ $values[0] ] = [ $values[0], '<span style="color:red">' . $values[0] . '</span>' ];

			$display_val = '<span style="color:red">' . $values[0] . '</span>';
		}

		foreach ( (array) $options as $key => $val )
		{
			$select .= $make_option( $values, $key, $val );
		}
	}

	$select .= '</select>' . FormatInputTitle( $title, $id, $required );

	if ( $is_multiple
		&& $allow_na !== false )
	{
		// Fix save multiple SelectInput() when none selected, add hidden empty input (only if $allow_na)
		$select .= '<input type="hidden" name="' . AttrEscape( $name ) . '" value="">';
	}

	if ( ! isset( $values[0] )
		|| $values[0] == ''
		|| ! $div )
	{
		return $select;
	}

	return InputDivOnclick(
		$id,
		$select,
		$display_val,
		FormatInputTitle( $title )
	);
}


/**
 * Multi Languages Select Input
 *
 * @example MLSelectInput(
 *          	$RET['CATEGORY_ID'],
 *          	'tables[' . $_REQUEST['id'] . '][CATEGORY_ID]',
 *          	_( 'User Field Category' ),
 *          	$categories_options,
 *          	false
 *          );
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && $value != '' && $div )
 *
 * @uses ParseMLField() to get localized options
 *
 * @global $RosarioLocales Returns simple SelectInput() if only 1 locale set
 * @global $locale         Get current locale
 *
 * @param  string         $value    Input value.
 * @param  string         $name     Input name.
 * @param  string         $title    Input title.
 * @param  array          $options  Input options: array( option_value => option_text ).
 * @param  string|boolean $allow_na Allow N/A (empty value); set to false to disallow (optional). Defaults to N/A.
 * @param  string         $extra    Extra HTML attributes added to the input.
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string         Input HTML
 */
function MLSelectInput( $value, $name, $title, $options, $allow_na = 'N/A', $extra = '', $div = true )
{
	global $RosarioLocales,
		$locale;

	// Mab - support array style $option values.
	$value = is_array( $value ) ? $value[0] : $value;

	if ( count( $RosarioLocales ) < 2 )
	{
		return SelectInput( ParseMLField( $value, $locale ), $name, $title, $options, $div );
	}

	$id = GetInputID( $name );

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	// Mab - append current val to select list if not in list.
	if ( $value != ''
		&& ( ! is_array( $options )
			|| !array_key_exists( $value, $options ) ) )
	{
		$options[ $value ] = [ $value, '<span style="color:red">' . $value . '</span>' ];
	}

	if ( AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		$select = '<select name="' . AttrEscape( $name ) . '" id="' . $id . '" '.$extra.'>';

		if ( $allow_na !== false )
		{
			$select .= '<option value="">' . ( $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na ) . '</option>';
		}

		foreach ( (array) $options as $key => $val )
		{
			$selected = '';

			$key .= '';

			if ( $value == $key
				&& ( ! ( $value == false && $value !== $key )
					|| ( $value === '0' && $key === 0 ) ) )
			{
				$selected = ' selected';
			}

			$val_locale = ParseMLField( ( is_array( $val ) ? $val[0] : $val ), $locale );

			$select .= '<option value="' . AttrEscape( $key ) . '"' .
				$selected . '>' . $val_locale . '</option>';
		}

		$select .= '</select>' . FormatInputTitle( $title, $id, $required );

		if ( $value == ''
			|| ! $div )
		{
			return $select;
		}

		return InputDivOnclick(
			$id,
			$select,
			ParseMLField(
				( is_array( $options[ $value ] ) ? $options[ $value ][1] : $options[ $value ] ),
				$locale
			),
			FormatInputTitle( $title )
		);
	}

	$display_val = is_array( $options[ $value ] ) ? $options[ $value ][1] : $options[ $value ];

	if ( $display_val == '' )
	{
		$display_val = '-';

		if ( $allow_na !== false )
		{
			$display_val = $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na;
		}
	}
	else
	{
		$display_val = ParseMLField( $display_val, $locale );
	}

	return $display_val . FormatInputTitle( $title );
}


/**
 * SelectInput() wrapper which adds jQuery Chosen.
 *
 * Chosen is a library for making long, unwieldy select boxes more friendly.
 * @link https://github.com/harvesthq/chosen
 *
 * @since 2.9.5
 * @deprecated since 10.7 Use Select2Input() instead. Fixes the overflow issue.
 *
 * @example ChosenSelectInput( $value, 'values[' . $id . '][' . $name . ']', '', $options, 'N/A', $extra )
 *
 * @uses Select2Input() to generate the Select input
 *
 * @param  string         $value    Input value.
 * @param  string         $name     Input name.
 * @param  string         $title    Input title (optional). Defaults to ''.
 * @param  array          $options  Input options: array( option_value => option_text ).
 * @param  string|boolean $allow_na Allow N/A (empty value); set to false to disallow (optional). Defaults to N/A.
 * @param  string         $extra    Extra HTML attributes added to the input.
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string         Input HTML
 */
function ChosenSelectInput( $value, $name, $title = '', $options = [], $allow_na = 'N/A', $extra = '', $div = true )
{
	// Raise deprecation notice.
	trigger_error(
		'ChosenSelectInput() function is deprecated since RosarioSIS 10.7. Use Select2Input() instead.',
		E_USER_DEPRECATED
	);

	return Select2Input( $value, $name, $title, $options, $allow_na, $extra, $div );
}


/**
 * SelectInput() wrapper which adds jQuery Select2.
 *
 * Select2 gives you a customizable select box with support for searching, tagging, remote data sets, infinite scrolling, and many other highly used options.
 * @link https://select2.org/
 *
 * @since 10.7
 * @since 12.5 Select2 AJAX results: add `data-ajax-url="my_ajax_results_url"` param to $extra
 *
 * @example Select2Input( $value, 'values[' . $id . '][' . $name . ']', '', $options, 'N/A', $extra )
 *
 * @uses SelectInput() to generate the Select input
 *
 * @param  string         $value    Input value.
 * @param  string         $name     Input name.
 * @param  string         $title    Input title (optional). Defaults to ''.
 * @param  array          $options  Input options: array( option_value => option_text ).
 * @param  string|boolean $allow_na Allow N/A (empty value); set to false to disallow (optional). Defaults to N/A.
 * @param  string         $extra    Extra HTML attributes added to the input.
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true.
 *
 * @return string         Input HTML
 */
function Select2Input( $value, $name, $title = '', $options = [], $allow_na = 'N/A', $extra = '', $div = true )
{
	static $select2_included = false;

	$js = '';

	if ( ! $select2_included
		&& AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		ob_start();	?>
		<!-- Select2 -->
		<script src="assets/js/jquery-select2/select2.min.js"></script>
		<link rel="stylesheet" href="assets/js/jquery-select2/select2.min.css">
		<script src="assets/js/csp/functions/Select2Input.js?v=12.5"></script>
		<?php $select2_included = true;

		$js = ob_get_clean();
	}

	if ( ! $extra
		|| mb_strpos( $extra, 'class=' ) === false )
	{
		$extra .= ' class="select2-select"';
	}
	elseif ( mb_strpos( $extra, 'class=' ) !== false )
	{
		$extra = str_replace(
			[ 'class="', "class='" ],
			[ 'class="select2-select ', "class='select2-select " ],
			$extra
		);
	}

	// Translate default "Select Some Options" multiple placeholder.
	if ( mb_strpos( $extra, 'multiple' ) !== false
		&& mb_strpos( $extra, 'data-placeholder=' ) === false )
	{
		$extra .= ' data-placeholder="' . AttrEscape( _( 'Select some Options' ) ) . '"';
	}

	$return = $js . SelectInput(
		$value,
		$name,
		$title,
		$options,
		$allow_na,
		$extra,
		$div
	);

	if ( $value != ''
		&& $div
		&& AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		$id = GetInputID( $name );
	}

	return $return;
}


/**
 * Radio Input
 *
 * @example RadioInput( $value, 'values[' . $id . '][COLOR]', '', $color_options );
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 * @uses InputDivOnclick()
 *       if ( AllowEdit() && !isset( $_REQUEST['_ROSARIO_PDF'] ) && ! $new && $div )
 *
 * @param  string         $value    Input value
 * @param  string         $name     Input name
 * @param  string         $title    Input title.
 * @param  array          $options  Input options: array( option_value => option_text )
 * @param  string|boolean $allow_na Allow N/A (empty value); set to false to disallow (optional). Defaults to N/A
 * @param  string         $extra    Extra HTML attributes added to the input (optional).
 * @param  boolean        $div      Is input wrapped into <div onclick>? (optional). Defaults to true
 *
 * @return string         Input HTML
 */
function RadioInput( $value, $name, $title, $options, $allow_na = 'N/A', $extra = '', $div = true )
{
	$id = GetInputID( $name );

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	// mab - append current val to select list if not in list
	if ( $value != ''
		&& ( ! is_array( $options )
			|| ! array_key_exists( $value, $options ) ) )
	{
		$options[ $value ] = [ $value, '<span style="color:red">' . $value . '</span>' ];
	}

	if ( AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		$table = '<table class="cellspacing-0 cellpadding-5"><tr class="st">';

		$i = 0;

		if ( $allow_na !== false )
		{
			$table .= '<td><label><input type="radio" name="' . AttrEscape( $name ) . '" value=""' .
				( $value == '' ? ' checked' : '' ) . ' ' . $extra . '> ' .
				( $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na ) . '</label></td>';

			$i++;
		}

		foreach ( (array) $options as $key => $val )
		{
			if ( $i++ % 3 == 0 && $i > 1 )
			{
				$table .= '</tr><tr class="st">';
			}

			$checked = '';

			$key .= '';

			if ( $value == $key
				&& ( !( $value == false && $value !== $key )
					|| ( $value === '0' && $key === 0 ) ) )
			{
				$checked = ' checked';
			}

			$table .= '<td><label><input type="radio" name="' . AttrEscape( $name ) . '" value="' .
				AttrEscape( $key ) . '"' . $checked . ' ' . $extra . '> ' .
				( is_array( $val ) ? $val[0] : $val ) . '</label></td>';
		}

		$table .= '</tr></table>';

		$table .= FormatInputTitle( $title, '', $required, '' );

		if ( $value == ''
			|| ! $div )
		{
			return $table;
		}

		return InputDivOnclick(
			$id,
			$table,
			is_array( $options[ $value ] ) ? $options[ $value ][1] : $options[ $value ],
			FormatInputTitle( $title )
		);
	}

	$display_val = ! isset( $options[ (string) $value ] ) ? '' :
		( is_array( $options[ $value ] ) ? $options[ $value ][1] : $options[ $value ] );

	if ( $display_val == '' )
	{
		$display_val = '-';

		if ( $allow_na !== false )
		{
			$display_val = $allow_na === 'N/A' ? _( 'N/A' ) : $allow_na;
		}
	}

	return $display_val . FormatInputTitle( $title );
}


/**
 * Color Picker Input
 *
 * @example ColorInput( $value, 'values[' . $id . '][' . $column . ']', _( 'Color' ) );
 *
 * @since 2.9
 * @since 5.4 Remove $type param.
 *
 * @param  string  $value Color value
 * @param  string  $name  Input name attribute
 * @param  string  $title Input title (label)
 * @param  string  $extra Extra HTML attributes added to the input (optional).
 * @param  boolean $div   Is input wrapped into <div onclick>? (optional). Defaults to true
 *
 * @return string  Color Picker Input HTML
 */
function ColorInput( $value, $name, $title = '', $extra = '', $div = true )
{
	if ( strpos( $_SERVER['HTTP_USER_AGENT'], 'MSIE' )
		|| strpos( $_SERVER['HTTP_USER_AGENT'], 'Trident/7' ) )
	{
		// Is Internet Explorer: not compatible with color input.
		return ColorInputMiniColors( $value, $name, $title, 'hidden', $extra, $div );
	}

	$id = GetInputID( $name );

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	$color_rect = '<div class="color-input-value" style="background-color:' . $value . ';"></div>';

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $color_rect . FormatInputTitle( $title, '', '', '' );
	}

	$input = '<input type="color" name="' . AttrEscape( $name ) . '" id="' . $id . '" value="' .
		AttrEscape( $value ) . '"' . $extra . '>';

	$input .= FormatInputTitle( $title, $id, $required );

	if ( $value == ''
		|| ! $div )
	{
		return $input;
	}

	return InputDivOnclick(
		$id,
		$input,
		$color_rect,
		FormatInputTitle( $title )
	);
}


/**
 * Color Picker Input for browsers not supporting HTML5 color input
 * @link http://caniuse.com/#search=input%20type
 *
 * @example ColorInputMiniColors( $value, 'values[' . $id . '][' . $column . ']', '', 'hidden', 'data-position="bottom right"' );
 *
 * @deprecated
 *
 * @since 5.4
 *
 * @uses jQuery MiniColors plugin
 *
 * @see assets/js/jquery-minicolors/ for plugin files
 *
 * @link https://github.com/claviska/jquery-minicolors/
 *
 * @param  string  $value Color value
 * @param  string  $name  Input name attribute
 * @param  string  $title Input title (label)
 * @param  string  $type  hidden|text Input type attribute (optional). Defaults to 'hidden'
 * @param  string  $extra Extra HTML attributes added to the input (optional).
 * @param  boolean $div   Is input wrapped into <div onclick>? (optional). Defaults to true
 *
 * @return string  Color Picker Input HTML
 */
function ColorInputMiniColors( $value, $name, $title = '', $type = 'hidden', $extra = '', $div = true )
{
	static $included = false;

	$id = GetInputID( $name );

	$required = $value == '' && mb_strpos( $extra, 'required' ) !== false;

	$color_rect = '<div class="color-input-value" style="background-color:' . $value . ';"></div>';

	if ( ! AllowEdit()
		|| isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $color_rect . FormatInputTitle( $title, '', '', '' );
	}

	$js = '';

	if ( ! $included )
	{
		ob_start();
		?>
		<!-- MiniColors -->
		<link rel="stylesheet" href="assets/js/jquery-minicolors/jquery.minicolors.css">
		<script src="assets/js/jquery-minicolors/jquery.minicolors.js"></script>
		<script>$(document).ready(function(){
			$('.minicolors').each(function(){
				$(this).minicolors({
					position: $(this).attr('data-position') || 'bottom left'
				});
			});
		});</script>
		<?php
		$js = ob_get_clean();

		$included = true;
	}

	ob_start();
	?>
	<input type="<?php echo AttrEscape( $type ); ?>" name="<?php echo AttrEscape( $name ); ?>" id="<?php echo $id; ?>"
		class="minicolors" value="<?php echo AttrEscape( $value ); ?>" <?php echo $extra; ?>>
	<?php

	$color = ob_get_clean() . FormatInputTitle( $title, $id, $required );

	if ( $value == ''
		|| ! $div )
	{
		return $js . $color;
	}

	return $js . InputDivOnclick(
		$id,
		$color,
		$color_rect,
		FormatInputTitle( $title ) .
		'<script>$("#div' . $id . '").on("click", function(){
			$("#' . $id . '").minicolors({
				position: $("#' . $id . '").attr("data-position") || "bottom left"
			});
		});</script>'
	);
}


/**
 * Captcha Input
 *
 * @example CaptchaInput( 'captcha' . rand( 100, 9999 ), _( 'Captcha' ) );
 *
 * @since 3.5
 *
 * @see assets/js/warehouse.js for captcha JS functions.
 *
 * @uses $_SESSION['CaptchaInput']
 * Places input name in session
 * so we can retrieve & check the input & answer values before processing form
 *
 * @param  string  $name  Input name attribute (array: 'input' & 'answer' indexes available).
 * @param  string  $title Input title (label)
 * @param  string  $extra Extra HTML attributes added to the input (optional).
 *
 * @return string  Captcha Input HTML
 */
function CaptchaInput( $name, $title, $extra = '' )
{
	// Place input name in session
	// so we can retrieve & check the input & answer values before processing form.
	$_SESSION['CaptchaInput'] = $name;

	$id_base = GetInputID( $name );

	$required = true;

	// @since 12.5 CSP remove unsafe-inline Javascript
	ob_start(); ?>
	<div class="captcha" id="<?php echo $id_base; ?>">
		<span id="<?php echo $id_base; ?>-n1"></span> + <span id="<?php echo $id_base; ?>-n2"></span> = <input id="<?php echo $id_base; ?>-input" name="<?php echo AttrEscape( $name ); ?>[input]" type="number" required>
		<input id="<?php echo $id_base; ?>-answer" name="<?php echo AttrEscape( $name ); ?>[answer]" type="hidden" <?php echo $extra; ?>>
		<?php echo FormatInputTitle( $title, $id_base . '-input', $required ); ?>
	</div>
	<script src="assets/js/csp/functions/CaptchaInput.js?v=12.5"></script>
	<?php

	return ob_get_clean();
}


/**
 * Check Captcha
 * Compare captcha input with answer.
 *
 * @since 3.5
 *
 * @uses $_SESSION['CaptchaInput'] where captcha name is stored.
 *
 * @see Create User / Student forms.
 *
 * @example if ( ! CheckCaptcha() )	$error[] = _( 'Captcha' );
 *
 * @return boolean True if no captcha or if captcha passed, else false.
 */
function CheckCaptcha()
{
	if ( ! isset( $_SESSION['CaptchaInput'] )
		|| ! $_SESSION['CaptchaInput'] )
	{
		return true;
	}

	// Get submitted captcha using captcha name saved in session.
	$captcha = isset( $_REQUEST[ $_SESSION['CaptchaInput'] ] ) ?
		$_REQUEST[ $_SESSION['CaptchaInput'] ] :
		[];

	// Compare input & answer.
	return $captcha && $captcha['input'] === $captcha['answer'];
}


/**
 * File Input
 * Warning: contrary to other *Input() functions, there is no AllowEdit() check
 * so the file input will also be visible to students, parents & teachers.
 *
 * @since 3.8.1
 * @since 5.2 Add $max_file_size param & max file size validation.
 * @since 7.8 Handle `multiple` files attribute. See FileUploadMultiple().
 *
 * @example FileInput( 'file', _( 'File' ), 'required' )
 * @example FileInput( 'files[]', _( 'Files' ), 'multiple' )
 *
 * @uses GetInputID() to generate ID from name
 * @uses FormatInputTitle() to format title
 *
 * @param  string  $name          Input name.
 * @param  string  $title         Input title (optional). Defaults to ''.
 * @param  string  $extra         Extra HTML attributes added to the input. Defaults to ''.
 * @param  int     $max_file_size Maximum File Size, in Mb. Optional. Defaults to FileUploadMaxSize().
 *
 * @return string  Input HTML
 */
function FileInput( $name, $title = '', $extra = '', $max_file_size = 0 )
{
	static $js_included = false;

	require_once 'ProgramFunctions/FileUpload.fnc.php';

	$id = GetInputID( $name );

	$required = mb_strpos( $extra, 'required' ) !== false;

	$ftitle = FormatInputTitle( $title, $id, $required );

	// Input size / length based on value number of chars.
	if ( mb_strpos( $extra, 'size=' ) === false )
	{
		$extra .= ' size="10"';
	}

	$max_file_size = $max_file_size > 0 && $max_file_size < FileUploadMaxSize() ? $max_file_size : FileUploadMaxSize();

	// Input title indicating Maximum file size.
	if ( mb_strpos( $extra, 'title=' ) === false )
	{
		$extra .= ' title="' . AttrEscape( sprintf( _( 'Maximum file size: %01.0fMb' ), $max_file_size ) ) . '"';
	}

	$js = '';

	if ( ! $js_included )
	{
		// @since 12.5 CSP remove unsafe-inline Javascript
		$js = '<script src="assets/js/csp/functions/FileInput.js?v=12.5"></script>';
	}

	return '<input type="file" id="' . $id . '" name="' . AttrEscape( $name ) . '" ' . $extra .
		' class="onchange-file-input" data-max-size="' . (float) $max_file_size . '"><span class="loading"></span>' .
		$ftitle . $js;
}


/**
 * No Input
 * Simulate Input formatting for a non-editable Input / Value
 *
 * @example NoInput( $person[1]['PHONE'], _( 'Home Phone' ) )
 *
 * @uses FormatInputTitle() to format title
 *
 * @param  string $value Input value
 * @param  string $title Input title (optional). Defaults to ''
 *
 * @return string Value + Formatted Title
 */
function NoInput( $value, $title = '' )
{
	$ftitle = FormatInputTitle( $title );

	$value = ( $value != '' ? $value : '-' );

	if ( AllowEdit()
		&& ! isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return '<span class="no-input-value">' .
			$value .
			'</span>' . $ftitle;
	}

	return $value . $ftitle;
}


/**
 * Checkbox onclick
 * When clicked the checkbox value is submitted & the URL is opened
 *
 * @example DrawHeader( CheckBoxOnclick( 'by_name', _( 'Sort by Name' ) );
 *
 * @param  string $name  Input name
 * @param  string $title Title
 *
 * @return string Checkbox onclick HTML
 */
function CheckBoxOnclick( $name, $title = '' )
{
	$onclick_URL = PreparePHP_SELF(
		$_REQUEST,
		[],
		isset( $_REQUEST[ $name ] ) && $_REQUEST[ $name ] == 'Y' ? [ $name => '' ] : [ $name => 'Y' ]
	);

	$input = '<input type="checkbox" name="' . AttrEscape( $name ) . '" value="Y"' .
		( isset( $_REQUEST[ $name ] ) && $_REQUEST[ $name ] == 'Y' ? ' checked' : '' ) .
		' class="onclick-ajax-link" data-link="' . $onclick_URL . '">';

	if ( $title != '' )
	{
		$input = '<label>' . $input . '&nbsp;' . $title . '</label>';
	}

	return $input;
}


/**
 * Get Javascript friendly input HTML ID attribute
 * From name attribute value
 *
 * @since 2.9
 * @since 12.0 Breaking change: Dots `.` are now removed from ID
 *
 * @example GetInputID( 'cust[CUSTOM_1]' ); // will return "custCUSTOM_1"
 *
 * @param  string $name Input name attribute
 *
 * @return string Input ID attribute
 */
function GetInputID( $name )
{
	if ( empty( $name ) )
	{
		return $name;
	}

	// @link https://stackoverflow.com/questions/4724732/preg-replace-in-php-regular-expression-for-not-condition#answer-4724767
	return preg_replace( '/\W/', '', $name );
}


/**
 * Format Input Title
 * Adds line break, <label> & .legend-gray CSS class to Title string
 *
 * @since 2.9
 *
 * @example $id = GetInputID( $name );
 *          $required = $value == '' && mb_strpos( $extra, 'required' ) !== false;
 *          $ftitle = FormatInputTitle( $title, $id, $required );
 *
 * @uses Use it if your *Field type (ie. password) is not supported to get a standardized Title
 *       or as an InputDivOnclick() argument
 *
 * @param  string  $title    Input Title
 * @param  string  $id       Input id attribute (optional). Defaults to ''
 * @param  boolean $required Required Input & AllowEdit() ? CSS class is .legend-red
 * @param  string  $break    Break before title (optional). Defaults to '<br>'
 *
 * @return string  Formatted Input Title
 */
function FormatInputTitle( $title, $id = '', $required = false, $break = '<br>' )
{
	if ( $title === '' )
	{
		return '';
	}

	if ( mb_strpos( (string) $title, 'a11y-hidden' ) !== false )
	{
		// Accessibility hidden title: force break to empty string.
		$break = '';
	}
	else
	{
		// Not hidden, add legend class color.
		$class = $required && AllowEdit() ? 'legend-red' : 'legend-gray';

		$title = '<span class="' . AttrEscape( $class ) . '">' . $title . '</span>';
	}

	// Add label only if id attribute given
	if ( $id !== '' )
	{
		$title = '<label for="' . $id . '">' . $title . '</label>';
	}

	return $break . $title;
}


/**
 * Input <div> onclick
 * Wraps the Input HTML inside a <div> with Value & Input Formatted Title below
 * The <div> makes the Input HTML appear and editable when clicked
 *
 * @todo Fix JS error var is not defined when InputDivOnclick() called twice!
 *
 * @since 2.9
 * @since 12.5 CSP remove unsafe-inline Javascript: use `<template>`
 *
 * @see JS ajaxPrepare() set tabindex="0" Make onclick div focusable when accessed by tab key
 * @uses JS inputAddHTML() function
 *
 * @example $return = InputDivOnclick( $id,	$textarea_html,	$display_val, $ftitle );
 *
 * @uses Use it if you would like to wrap various fields
 *       (ie. Student / User Name => First Name, Middle Name, Last Name)
 *
 * @param  string $id           Input ID
 * @param  string $input_html   Input HTML
 * @param  string $value        Input value
 * @param  string $input_ftitle Input Formatted Title
 *
 * @return string Wrapped Input HTML inside a <div> with Value & Input Formatted Title below
 */
function InputDivOnclick( $id, $input_html, $value, $input_ftitle )
{
	$id = GetInputID( $id );

	// @link https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots
	$template = '<template id="html' . $id . '">' . $input_html . '</template>';

	$value = $value == '' ? '-' : $value;

	$div_onclick = '<div id="div' . $id . '">
		<div class="onclick">' .
		// @since 12.0 CSS Add .uld class, alias of .underline-dots
		( mb_stripos( $value, '<div' ) === 0 ?
			'<div class="uld">' . $value . '</div>' :
			'<span class="uld">' . $value . '</span>' ) .
		$input_ftitle . '</div></div>';

	return $template . $div_onclick;
}


/**
 * Make Choose Checkbox
 *
 * Use in extra columns for ListOutput
 *
 * DBGet() callback
 * AND use first, to set controller checkbox & default checked value & $THIS_RET column if need be.
 *
 * @since 4.3
 * @since 10.0 Fix remove parent link to sort column, see ListOutput.fnc.php
 * @since 11.5 Add 'required' option to $value param. Prevent submitting form if no checkboxes are checked.
 *
 * First, set controller:
 * @example $extra['columns_before'] = [ 'CHECKBOX' => MakeChooseCheckbox( '', 'STUDENT_ID', 'student' ) ];
 * @example $extra['columns_before'] = [ 'CHECKBOX' => MakeChooseCheckbox( 'Y_required', '', 'st' ) ];
 * Then, use as DBGet() callback:
 * @example $extra['functions'] = [ 'CHECKBOX' => 'MakeChooseCheckbox' ];
 *
 * @param string $value           DB value or checked ('Y') or 'required'|'Y_required' to prevent submitting form if none checked.
 * @param string $column          Current DBGet column or $THIS_RET column to use (optional).
 * @param string $controller_name Controller name (set first only), ie. 'student' will give 'student[]' (optional).
 *
 * @return string Checkbox HTML or checked state if PDF/export
 */
function MakeChooseCheckbox( $value, $column = '', $controller_name = '' )
{
	global $THIS_RET;

	static $controller_column,
		$name,
		$checked,
		$controller_i = 1; // Handle multiple controllers on the same page.

	if ( ! empty( $controller_name ) )
	{
		$controller_column = $column;

		$name = $controller_name;

		$checked = mb_substr( $value, 0, 1 ) === 'Y';

		$required = mb_strpos( $value, 'required' ) !== false;

		if ( isset( $_REQUEST['_ROSARIO_PDF'] ) )
		{
			return $checked ? '✔️' : '';
		}

		$id = 'controller' . $controller_i;

		// @since 12.5 CSP remove unsafe-inline Javascript
		$return = '<input type="checkbox" value="Y" name="' . $id . '" id="' . $id . '"
			class="onclick-checkall" data-name-like="' . AttrEscape( $name . '[]' ) . '"' .
			( $required ? ' data-error="' . AttrEscape( _( 'You must choose at least one element from the list.' ) ) . '"' : '' ) .
			( $checked ? ' checked' : '' ) . '>
			<label for="' . $id . '" class="a11y-hidden">' . _( 'Check All' ) . '</label>';

		return $return;
	}

	if ( isset( $_REQUEST['_ROSARIO_PDF'] ) )
	{
		return $checked ? '✔️' : '';
	}

	if ( ! empty( $controller_column ) )
	{
		$value = issetVal( $THIS_RET[ $controller_column ], '' );
	}

	return '<label><input type="checkbox" name="' . AttrEscape( $name ) . '[]" value="' . AttrEscape( $value ) . '"' .
		( $checked ? ' checked' : '' ) . '><span class="a11y-hidden">' .
		_( 'Select' ) . '</span></label>';
}

/**
 * Escape HTML attribute
 * Protects against XSS (Javascript execution)
 *
 * @see URLEscape() to escape href, action & src attributes
 * @see GetInputID() to escape id attribute
 *
 * @example $html = '<span title="' . AttrEscape( $value ) . '">' . _( 'Text' ) . '</span>';
 * @example <span title="<?php echo AttrEscape( $value ); ?\>"><?php echo _( 'Text' ); ?\></span>
 *
 * @uses htmlspecialchars
 *
 * @since 9.0
 *
 * @param string $value Text to be escaped.
 *
 * @return string Escaped string.
 */
function AttrEscape( $value )
{
	return htmlspecialchars( (string) $value, ENT_QUOTES, 'UTF-8', false );
}
