input type attribute. * * @var array */ private $input_types = array( 'text' => 'text', 'number' => 'number', 'email' => 'email', 'url' => 'url', 'phone' => 'tel', 'date' => 'date', 'time' => 'time', 'color' => 'color', 'range' => 'range', ); /** * Render. * * @param array $field Field config. * @param string $name Input name attribute. */ public function render( $field, $name ) { $this->render_label( $field ); $id = $this->field_id( $field ); $required = ! empty( $field['required'] ) ? ' required aria-required="true"' : ''; $prefill = $field['value'] ?? ''; $values = array( is_scalar( $prefill ) ? (string) $prefill : '' ); $price_attr = $this->price_attr( $field ); if ( 'textarea' === $field['type'] ) { foreach ( $values as $i => $value ) { printf( '', 0 === $i ? 'id="' . esc_attr( $id ) . '"' : '', esc_attr( $name ), esc_attr( $field['placeholder'] ?? '' ), $price_attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in price_attr(). $required, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static string. esc_textarea( $value ) ); } return; } $type = isset( $this->input_types[ $field['type'] ] ) ? $this->input_types[ $field['type'] ] : 'text'; $extra = ''; if ( in_array( $type, array( 'number', 'range' ), true ) ) { $extra .= '' !== ( $field['min'] ?? '' ) ? ' min="' . esc_attr( $field['min'] ) . '"' : ''; $extra .= '' !== ( $field['max'] ?? '' ) ? ' max="' . esc_attr( $field['max'] ) . '"' : ''; $extra .= '' !== ( $field['step'] ?? '' ) ? ' step="' . esc_attr( $field['step'] ) . '"' : ''; } if ( ! empty( $field['maxlength'] ) ) { $extra .= ' maxlength="' . absint( $field['maxlength'] ) . '"'; } foreach ( $values as $i => $value ) { printf( '', esc_attr( $type ), 0 === $i ? 'id="' . esc_attr( $id ) . '"' : '', esc_attr( $name ), esc_attr( $value ), esc_attr( $field['placeholder'] ?? '' ), $extra, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped above. $price_attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in price_attr(). $required // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static string. ); } if ( 'range' === $field['type'] ) { echo ''; } } /** * Type-aware sanitization. An array value (a crafted POST, or a field an * add-on renders as multiple inputs) is cleaned entry by entry, with * blanks dropped. * * @param mixed $value Raw value. * @param array $field Field config. * @return string|string[] */ public function sanitize( $value, $field ) { if ( is_array( $value ) ) { $clean = array(); foreach ( $value as $single ) { $single = is_scalar( $single ) ? $this->sanitize_single( $single, $field ) : ''; if ( '' !== $single ) { $clean[] = $single; } } return $clean; } return $this->sanitize_single( $value, $field ); } /** * Sanitize one scalar entry. * * @param mixed $value Raw scalar. * @param array $field Field config. * @return string */ private function sanitize_single( $value, $field ) { $value = is_scalar( $value ) ? (string) $value : ''; switch ( $field['type'] ) { case 'email': return sanitize_email( $value ); case 'url': return esc_url_raw( $value ); case 'textarea': return sanitize_textarea_field( $value ); case 'number': case 'range': return '' === $value ? '' : (string) floatval( $value ); default: return sanitize_text_field( $value ); } } /** * Extra validation beyond required. Arrays validate every entry. * * @param mixed $value Sanitized value. * @param array $field Field config. * @return true|WP_Error */ public function validate( $value, $field ) { $result = parent::validate( $value, $field ); if ( is_wp_error( $result ) || '' === $value || array() === $value ) { return $result; } /** * Whether a field may post several values. Core fields render one * input, so an array is a crafted POST that would multiply the fee; * an add-on that renders repeated inputs opts in here. * * @param bool $accepts Default false (true for checkbox groups). * @param array $field Field config. */ if ( is_array( $value ) && ! apply_filters( 'shapeoptions/field_accepts_multiple', false, $field ) ) { /* translators: %s: field label */ return new WP_Error( 'shapeoptions_invalid', sprintf( __( 'Invalid value for "%s".', 'shapeoptions' ), $field['label'] ) ); } foreach ( is_array( $value ) ? $value : array( $value ) as $single ) { $check = $this->validate_single( $single, $field ); if ( is_wp_error( $check ) ) { return $check; } } return true; } /** * Fallback caps for the types whose natural length is not the single-line * default: textarea is the one input meant to hold prose (a gift message, * care instructions), and a URL carrying tracking parameters runs long * enough that the browser's own ~2000 character ceiling is the sensible * bound. Everything else — a name, an email, a date, a colour — keeps the * base default, which is already far past anything a customer would type. * * @param array $field Field config. * @return int */ protected function default_max_length( $field ) { switch ( $field['type'] ) { case 'textarea': return 5000; case 'url': return 2048; default: return parent::default_max_length( $field ); } } /** * Validate one scalar entry. * * @param string $value Sanitized scalar. * @param array $field Field config. * @return true|WP_Error */ private function validate_single( $value, $field ) { if ( 'email' === $field['type'] && ! is_email( $value ) ) { /* translators: %s: field label */ return new WP_Error( 'shapeoptions_invalid', sprintf( __( '"%s" must be a valid email address.', 'shapeoptions' ), $field['label'] ) ); } if ( in_array( $field['type'], array( 'number', 'range' ), true ) ) { $num = (float) $value; if ( ( '' !== ( $field['min'] ?? '' ) && $num < (float) $field['min'] ) || ( '' !== ( $field['max'] ?? '' ) && $num > (float) $field['max'] ) ) { /* translators: %s: field label */ return new WP_Error( 'shapeoptions_invalid', sprintf( __( '"%s" is out of the allowed range.', 'shapeoptions' ), $field['label'] ) ); } } return true; } /** * An array value (from an add-on rendering multiple inputs) charges the * flat fee once per entry. * * @param mixed $value Chosen value. * @param array $field Field config. * @return float */ public function get_price( $value, $field ) { if ( is_array( $value ) ) { return count( $value ) * (float) ( $field['price'] ?? 0 ); } return parent::get_price( $value, $field ); } } endif;