[ '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', tag_escape( $tag ), $attr_string, $content ); } }