628 lines
17 KiB
PHP
628 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Handles the Form Field Control block.
|
|
*
|
|
* @package GenerateBlocksPro
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Form field control block class.
|
|
*/
|
|
class GenerateBlocks_Block_Form_Field_Control extends GenerateBlocks_Block {
|
|
|
|
/**
|
|
* Keep track of all blocks of this type on the page.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected static $block_ids = [];
|
|
|
|
/**
|
|
* Block name.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static $block_name = 'generateblocks-pro/form-field-control';
|
|
|
|
/**
|
|
* Maximum textarea rows accepted from block attributes.
|
|
*
|
|
* @var int
|
|
*/
|
|
const MAX_TEXTAREA_ROWS = 100;
|
|
|
|
/**
|
|
* Default allow-list for custom form control HTML attributes.
|
|
*
|
|
* Prefix entries ending in `-*` match all attributes with that prefix.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $allowed_html_attributes = [
|
|
'aria-*',
|
|
'data-*',
|
|
'autocapitalize',
|
|
'autocomplete',
|
|
'enterkeyhint',
|
|
'inputmode',
|
|
'max',
|
|
'maxlength',
|
|
'min',
|
|
'minlength',
|
|
'pattern',
|
|
'spellcheck',
|
|
'step',
|
|
'title',
|
|
];
|
|
|
|
/**
|
|
* Attributes generated by the renderer or controlled by runtime behavior.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $reserved_html_attributes = [
|
|
'aria-invalid',
|
|
'aria-required',
|
|
'checked',
|
|
'class',
|
|
'disabled',
|
|
'id',
|
|
'multiple',
|
|
'name',
|
|
'placeholder',
|
|
'readonly',
|
|
'required',
|
|
'rows',
|
|
'selected',
|
|
'style',
|
|
'type',
|
|
'value',
|
|
];
|
|
|
|
/**
|
|
* Field types where custom HTML attributes apply to one rendered control.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $custom_html_attribute_field_types = [
|
|
'text',
|
|
'email',
|
|
'url',
|
|
'tel',
|
|
'number',
|
|
'textarea',
|
|
'select',
|
|
'checkbox',
|
|
];
|
|
|
|
/**
|
|
* Field-type support for known default attributes.
|
|
*
|
|
* @var array
|
|
*/
|
|
private static $html_attribute_field_type_support = [
|
|
'autocapitalize' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
|
|
'enterkeyhint' => [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ],
|
|
'inputmode' => [ 'text', 'email', 'url', 'tel', 'number', 'textarea' ],
|
|
'max' => [ 'number' ],
|
|
'maxlength' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
|
|
'min' => [ 'number' ],
|
|
'minlength' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
|
|
'pattern' => [ 'text', 'email', 'url', 'tel', 'number' ],
|
|
'spellcheck' => [ 'text', 'email', 'url', 'tel', 'textarea' ],
|
|
'step' => [ 'number' ],
|
|
];
|
|
|
|
/**
|
|
* Get the allowed custom form control HTML attributes.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function get_allowed_html_attribute_names() {
|
|
/**
|
|
* Filter custom HTML attributes allowed on generated form controls.
|
|
*
|
|
* Prefix entries ending in `-*` match all attributes with that prefix.
|
|
* The renderer still blocks generated/runtime attributes such as
|
|
* `name`, `id`, `type`, `value`, `required`, `aria-required`,
|
|
* `aria-invalid`, `style`, event handlers, `form*`, and `data-gb-*`.
|
|
*
|
|
* @since 2.6.0
|
|
* @param array $allowed_attributes Allowed attribute names and prefix markers.
|
|
*/
|
|
$allowed_attributes = apply_filters(
|
|
'generateblocks_form_control_html_attributes',
|
|
self::$allowed_html_attributes
|
|
);
|
|
|
|
if ( ! is_array( $allowed_attributes ) ) {
|
|
return self::$allowed_html_attributes;
|
|
}
|
|
|
|
$normalized = [];
|
|
|
|
foreach ( $allowed_attributes as $attribute_name ) {
|
|
$attribute_name = self::normalize_html_attribute_name( $attribute_name, true );
|
|
|
|
if ( '' !== $attribute_name ) {
|
|
$normalized[] = $attribute_name;
|
|
}
|
|
}
|
|
|
|
return array_values( array_unique( $normalized ) );
|
|
}
|
|
|
|
/**
|
|
* Sanitize custom HTML attributes before rendering generated controls.
|
|
*
|
|
* @param mixed $html_attributes Raw htmlAttributes block attribute.
|
|
* @param string $field_type Normalized field type.
|
|
* @return array
|
|
*/
|
|
public static function sanitize_html_attributes( $html_attributes, $field_type = '' ) {
|
|
$field_type = is_string( $field_type ) ? $field_type : '';
|
|
|
|
if (
|
|
! is_array( $html_attributes ) ||
|
|
! in_array( $field_type, self::$custom_html_attribute_field_types, true )
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
$sanitized = [];
|
|
$allowed_attributes = self::get_allowed_html_attribute_names();
|
|
|
|
foreach ( $html_attributes as $attribute_name => $attribute_value ) {
|
|
$attribute_name = self::normalize_html_attribute_name( $attribute_name );
|
|
|
|
if ( ! self::is_allowed_html_attribute( $attribute_name, $field_type, $allowed_attributes ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( is_array( $attribute_value ) || is_object( $attribute_value ) || null === $attribute_value ) {
|
|
continue;
|
|
}
|
|
|
|
$attribute_value = (string) $attribute_value;
|
|
|
|
if ( '' === $attribute_value && 0 !== strpos( $attribute_name, 'data-' ) ) {
|
|
continue;
|
|
}
|
|
|
|
$sanitized[ $attribute_name ] = $attribute_value;
|
|
}
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Normalize an HTML attribute name for allow-list checks.
|
|
*
|
|
* @param mixed $attribute_name Raw attribute name.
|
|
* @param bool $allow_prefix Whether `aria-*` style prefix markers are valid.
|
|
* @return string
|
|
*/
|
|
private static function normalize_html_attribute_name( $attribute_name, $allow_prefix = false ) {
|
|
if ( ! is_scalar( $attribute_name ) ) {
|
|
return '';
|
|
}
|
|
|
|
$attribute_name = strtolower( trim( (string) $attribute_name ) );
|
|
$is_prefix = $allow_prefix && '-*' === substr( $attribute_name, -2 );
|
|
$attribute_name = preg_replace( '/[^a-z0-9._:-]/', '', $attribute_name );
|
|
|
|
if ( $is_prefix ) {
|
|
if ( ! is_string( $attribute_name ) || ! preg_match( '/^[a-z][a-z0-9._:-]*-$/', $attribute_name ) ) {
|
|
return '';
|
|
}
|
|
|
|
return $attribute_name . '*';
|
|
}
|
|
|
|
if ( ! is_string( $attribute_name ) || ! preg_match( '/^[a-z][a-z0-9._:-]*$/', $attribute_name ) ) {
|
|
return '';
|
|
}
|
|
|
|
return $attribute_name;
|
|
}
|
|
|
|
/**
|
|
* Whether an attribute name can be rendered for a field type.
|
|
*
|
|
* @param string $attribute_name Normalized attribute name.
|
|
* @param string $field_type Normalized field type.
|
|
* @param array $allowed_attributes Allowed attribute names and prefix markers.
|
|
* @return bool
|
|
*/
|
|
private static function is_allowed_html_attribute( $attribute_name, $field_type, $allowed_attributes ) {
|
|
if ( '' === $attribute_name ) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
in_array( $attribute_name, self::$reserved_html_attributes, true ) ||
|
|
0 === strpos( $attribute_name, 'on' ) ||
|
|
0 === strpos( $attribute_name, 'form' ) ||
|
|
0 === strpos( $attribute_name, 'data-gb-' )
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
$is_allowed = false;
|
|
|
|
foreach ( $allowed_attributes as $allowed_attribute ) {
|
|
if ( '-*' === substr( $allowed_attribute, -2 ) ) {
|
|
$prefix = substr( $allowed_attribute, 0, -1 );
|
|
|
|
if ( 0 === strpos( $attribute_name, $prefix ) ) {
|
|
$is_allowed = true;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ( $attribute_name === $allowed_attribute ) {
|
|
$is_allowed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! $is_allowed ) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
isset( self::$html_attribute_field_type_support[ $attribute_name ] ) &&
|
|
! in_array( $field_type, self::$html_attribute_field_type_support[ $attribute_name ], true )
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get sanitized custom HTML attributes for a control block.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $field_type Normalized field type.
|
|
* @return array
|
|
*/
|
|
private static function get_custom_html_attributes( $attributes, $field_type ) {
|
|
return self::sanitize_html_attributes( $attributes['htmlAttributes'] ?? [], $field_type );
|
|
}
|
|
|
|
/**
|
|
* Render the Form Field Control block.
|
|
*
|
|
* @param array $attributes The block attributes.
|
|
* @param string $block_content The block content.
|
|
* @param WP_Block $block The block instance.
|
|
* @return string
|
|
*/
|
|
public static function render_block( $attributes, $block_content, $block ) {
|
|
unset( $block_content );
|
|
|
|
$field_type = GenerateBlocks_Block_Form_Field::normalize_field_type(
|
|
GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldType', '' )
|
|
);
|
|
$field_name = sanitize_key(
|
|
GenerateBlocks_Block_Form_Field::get_context_value( $block, 'fieldName', '' )
|
|
);
|
|
$unique_id = (string) GenerateBlocks_Block_Form_Field::get_context_value( $block, 'uniqueId', '' );
|
|
$required = 'hidden' !== $field_type && ! empty( GenerateBlocks_Block_Form_Field::get_context_value( $block, 'isRequired', false ) );
|
|
$field_id = GenerateBlocks_Block_Form_Field::get_field_id( $unique_id, $field_name );
|
|
$html = self::build_control( $attributes, $field_type, $field_name, $field_id, $required, $block );
|
|
|
|
return generateblocks_maybe_add_block_css(
|
|
$html,
|
|
[
|
|
'class_name' => __CLASS__,
|
|
'attributes' => $attributes,
|
|
'block_ids' => self::$block_ids,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build the control markup.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $field_type Field type.
|
|
* @param string $field_name Field name.
|
|
* @param string $field_id Field ID.
|
|
* @param bool $required Whether required.
|
|
* @param mixed $block Block instance.
|
|
* @return string
|
|
*/
|
|
private static function build_control( $attributes, $field_type, $field_name, $field_id, $required, $block ) {
|
|
$default_value = GenerateBlocks_Block_Form_Field::resolve_default_value( $attributes['defaultValue'] ?? '', $block );
|
|
|
|
switch ( $field_type ) {
|
|
case 'textarea':
|
|
return self::build_textarea( $attributes, $field_name, $field_id, $required, $default_value );
|
|
|
|
case 'select':
|
|
return self::build_select( $attributes, $field_name, $field_id, $required, $default_value );
|
|
|
|
case 'checkbox':
|
|
return self::build_checkbox( $attributes, $field_name, $field_id, $required );
|
|
|
|
case 'radio':
|
|
return self::build_choice_group( $attributes, $field_name, $field_id, true, $required, $default_value );
|
|
|
|
case 'checkbox-group':
|
|
return self::build_choice_group( $attributes, $field_name, $field_id, false, false, '' );
|
|
|
|
default:
|
|
return self::build_input( $attributes, $field_name, $field_id, $field_type, $required, $default_value );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build base control classes.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $element Element class.
|
|
* @return string
|
|
*/
|
|
private static function get_control_classes( $attributes, $element ) {
|
|
return GenerateBlocks_Block_Form_Field::get_block_classes(
|
|
'gb-form-field-control',
|
|
$attributes,
|
|
[ $element ]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Build an input element.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $name Field name.
|
|
* @param string $id Field ID.
|
|
* @param string $type Input type.
|
|
* @param bool $required Whether required.
|
|
* @param string $default_value Default value.
|
|
* @return string
|
|
*/
|
|
private static function build_input( $attributes, $name, $id, $type, $required, $default_value ) {
|
|
$attrs = array_merge(
|
|
self::get_custom_html_attributes( $attributes, $type ),
|
|
[
|
|
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
|
|
'type' => $type,
|
|
'name' => $name,
|
|
'id' => $id,
|
|
]
|
|
);
|
|
|
|
if ( ! empty( $attributes['placeholder'] ) && 'hidden' !== $type ) {
|
|
$attrs['placeholder'] = (string) $attributes['placeholder'];
|
|
}
|
|
|
|
if ( '' !== $default_value ) {
|
|
$attrs['value'] = $default_value;
|
|
}
|
|
|
|
if ( $required ) {
|
|
$attrs['required'] = true;
|
|
$attrs['aria-required'] = 'true';
|
|
}
|
|
|
|
return self::single_tag( 'input', $attrs );
|
|
}
|
|
|
|
/**
|
|
* Build a textarea element.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $name Field name.
|
|
* @param string $id Field ID.
|
|
* @param bool $required Whether required.
|
|
* @param string $default_value Default value.
|
|
* @return string
|
|
*/
|
|
private static function build_textarea( $attributes, $name, $id, $required, $default_value ) {
|
|
$attrs = array_merge(
|
|
self::get_custom_html_attributes( $attributes, 'textarea' ),
|
|
[
|
|
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
|
|
'name' => $name,
|
|
'id' => $id,
|
|
]
|
|
);
|
|
$rows = isset( $attributes['rows'] ) ? min( self::MAX_TEXTAREA_ROWS, absint( $attributes['rows'] ) ) : 0;
|
|
|
|
if ( $rows ) {
|
|
$attrs['rows'] = $rows;
|
|
}
|
|
|
|
if ( ! empty( $attributes['placeholder'] ) ) {
|
|
$attrs['placeholder'] = (string) $attributes['placeholder'];
|
|
}
|
|
|
|
if ( $required ) {
|
|
$attrs['required'] = true;
|
|
$attrs['aria-required'] = 'true';
|
|
}
|
|
|
|
return self::wrap_tag( 'textarea', $attrs, esc_textarea( $default_value ) );
|
|
}
|
|
|
|
/**
|
|
* Build a select element.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $name Field name.
|
|
* @param string $id Field ID.
|
|
* @param bool $required Whether required.
|
|
* @param string $default_value Default selected value.
|
|
* @return string
|
|
*/
|
|
private static function build_select( $attributes, $name, $id, $required, $default_value ) {
|
|
$attrs = array_merge(
|
|
self::get_custom_html_attributes( $attributes, 'select' ),
|
|
[
|
|
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
|
|
'name' => $name,
|
|
'id' => $id,
|
|
]
|
|
);
|
|
|
|
if ( $required ) {
|
|
$attrs['required'] = true;
|
|
$attrs['aria-required'] = 'true';
|
|
}
|
|
|
|
$options_html = '';
|
|
$placeholder = $attributes['placeholder'] ?? '';
|
|
|
|
if ( '' !== $placeholder ) {
|
|
$options_html .= sprintf(
|
|
'<option value="" disabled%s>%s</option>',
|
|
'' === $default_value ? ' selected' : '',
|
|
esc_html( $placeholder )
|
|
);
|
|
}
|
|
|
|
foreach ( (array) ( $attributes['options'] ?? [] ) as $option ) {
|
|
$option = GenerateBlocks_Block_Form_Field::normalize_option( $option );
|
|
|
|
if ( ! $option ) {
|
|
continue;
|
|
}
|
|
|
|
$options_html .= sprintf(
|
|
'<option value="%s"%s>%s</option>',
|
|
esc_attr( $option['value'] ),
|
|
'' !== $default_value && $option['value'] === $default_value ? ' selected' : '',
|
|
esc_html( $option['label'] )
|
|
);
|
|
}
|
|
|
|
return self::wrap_tag( 'select', $attrs, $options_html );
|
|
}
|
|
|
|
/**
|
|
* Build a checkbox element.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $name Field name.
|
|
* @param string $id Field ID.
|
|
* @param bool $required Whether required.
|
|
* @return string
|
|
*/
|
|
private static function build_checkbox( $attributes, $name, $id, $required ) {
|
|
$checked_value = ! empty( $attributes['checkedValue'] )
|
|
? sanitize_text_field( (string) $attributes['checkedValue'] )
|
|
: 'yes';
|
|
$attrs = array_merge(
|
|
self::get_custom_html_attributes( $attributes, 'checkbox' ),
|
|
[
|
|
'class' => self::get_control_classes( $attributes, 'gb-form-field__input' ),
|
|
'type' => 'checkbox',
|
|
'name' => $name,
|
|
'id' => $id,
|
|
'value' => $checked_value,
|
|
]
|
|
);
|
|
|
|
if ( $required ) {
|
|
$attrs['required'] = true;
|
|
$attrs['aria-required'] = 'true';
|
|
}
|
|
|
|
return self::single_tag( 'input', $attrs );
|
|
}
|
|
|
|
/**
|
|
* Build radio/checkbox-group options.
|
|
*
|
|
* @param array $attributes Block attributes.
|
|
* @param string $name Field name.
|
|
* @param string $id Field ID prefix.
|
|
* @param bool $radio Whether radio buttons.
|
|
* @param bool $required Whether radio group is required.
|
|
* @param string $default_value Default selected value.
|
|
* @return string
|
|
*/
|
|
private static function build_choice_group( $attributes, $name, $id, $radio, $required, $default_value ) {
|
|
$attrs = [
|
|
'class' => self::get_control_classes( $attributes, 'gb-form-field__group' ),
|
|
];
|
|
$html = '';
|
|
|
|
foreach ( (array) ( $attributes['options'] ?? [] ) as $index => $option ) {
|
|
$option = GenerateBlocks_Block_Form_Field::normalize_option( $option );
|
|
|
|
if ( ! $option ) {
|
|
continue;
|
|
}
|
|
|
|
$option_id = $id . '-' . $index;
|
|
$html .= sprintf(
|
|
'<label class="gb-form-field__group-option" for="%s">',
|
|
esc_attr( $option_id )
|
|
);
|
|
|
|
$input_attrs = [
|
|
'type' => $radio ? 'radio' : 'checkbox',
|
|
'name' => $radio ? $name : $name . '[]',
|
|
'id' => $option_id,
|
|
'value' => $option['value'],
|
|
];
|
|
|
|
if ( $radio && '' !== $default_value && $option['value'] === $default_value ) {
|
|
$input_attrs['checked'] = true;
|
|
}
|
|
|
|
if ( $radio && $required ) {
|
|
$input_attrs['required'] = true;
|
|
}
|
|
|
|
$html .= '<input ' . GenerateBlocks_Block_Form_Field::format_attributes( $input_attrs ) . ' />';
|
|
$html .= '<span>' . esc_html( $option['label'] ) . '</span>';
|
|
$html .= '</label>';
|
|
}
|
|
|
|
return self::wrap_tag( 'div', $attrs, $html );
|
|
}
|
|
|
|
/**
|
|
* Build a single tag.
|
|
*
|
|
* @param string $tag Tag name.
|
|
* @param array $attrs Generated attributes.
|
|
* @return string
|
|
*/
|
|
private static function single_tag( $tag, $attrs ) {
|
|
$attr_string = GenerateBlocks_Block_Form_Field::format_attributes( $attrs );
|
|
|
|
return sprintf( '<%s %s />', tag_escape( $tag ), $attr_string );
|
|
}
|
|
|
|
/**
|
|
* Build an element with content.
|
|
*
|
|
* @param string $tag Tag name.
|
|
* @param array $attrs Generated attributes.
|
|
* @param string $content Inner HTML.
|
|
* @return string
|
|
*/
|
|
private static function wrap_tag( $tag, $attrs, $content ) {
|
|
$attr_string = GenerateBlocks_Block_Form_Field::format_attributes( $attrs );
|
|
|
|
return sprintf(
|
|
'<%1$s %2$s>%3$s</%1$s>',
|
|
tag_escape( $tag ),
|
|
$attr_string,
|
|
$content
|
|
);
|
|
}
|
|
}
|