[ '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(
'',
'' === $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(
'',
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(
'';
}
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
);
}
}