__( 'Text', 'frontglut' ), 'textarea' => __( 'Textarea', 'frontglut' ), 'editor' => __( 'Rich editor', 'frontglut' ), 'number' => __( 'Number', 'frontglut' ), 'email' => __( 'Email', 'frontglut' ), 'url' => __( 'URL', 'frontglut' ), 'tel' => __( 'Phone', 'frontglut' ), 'password' => __( 'Password', 'frontglut' ), 'date' => __( 'Date', 'frontglut' ), 'color' => __( 'Colour', 'frontglut' ), 'select' => __( 'Select', 'frontglut' ), 'multiselect' => __( 'Multi select', 'frontglut' ), 'radio' => __( 'Radio buttons', 'frontglut' ), 'checkbox' => __( 'Checkbox', 'frontglut' ), 'checkboxes' => __( 'Checkbox group', 'frontglut' ), 'file' => __( 'File upload', 'frontglut' ), 'image' => __( 'Image upload', 'frontglut' ), 'gallery' => __( 'Gallery', 'frontglut' ), 'table' => __( 'Table', 'frontglut' ), 'hidden' => __( 'Hidden', 'frontglut' ), 'heading' => __( 'Heading', 'frontglut' ), 'label' => __( 'Info text', 'frontglut' ), ) ); } /** Field types the form builder may offer for user-defined fields. */ public static function builder_types() { $t = self::types(); return $t; } public static function defaults() { return array( 'key' => '', 'type' => 'text', 'label' => '', 'required' => 0, 'placeholder' => '', 'help' => '', 'options' => '', 'default' => '', 'width' => '100', 'class' => '', 'storage' => 'meta', 'min' => '', 'max' => '', 'maxlength' => '', 'minlength' => '', 'logic' => array(), 'roles' => array(), 'multiple' => 0 ); } /** Parse options: array(value=>label) or "value|Label" lines or "Label" lines. */ public static function options( array $f ) { $o = isset( $f['options'] ) ? $f['options'] : array(); if ( is_array( $o ) ) { return $o; } $out = array(); foreach ( preg_split( '/\r\n|\r|\n/', (string) $o ) as $line ) { $line = trim( $line ); if ( '' === $line ) { continue; } if ( false !== strpos( $line, '|' ) ) { list( $v, $l ) = array_map( 'trim', explode( '|', $line, 2 ) ); } else { $v = $l = $line; } $out[ $v ] = $l; } return $out; } /** Visibility under conditional logic given all current values. */ public static function is_visible( array $f, array $values ) { $pre = apply_filters( 'frontglut_field_visible', null, $f, $values ); if ( null !== $pre ) { return (bool) $pre; // add-ons implement richer logic (AND/OR groups). } if ( empty( $f['logic'] ) || empty( $f['logic']['field'] ) ) { return true; } $l = $f['logic']; $cur = isset( $values[ $l['field'] ] ) ? $values[ $l['field'] ] : ''; $cur = is_array( $cur ) ? $cur : array( $cur ); $val = isset( $l['value'] ) ? (string) $l['value'] : ''; switch ( isset( $l['op'] ) ? $l['op'] : 'eq' ) { case 'neq': return ! in_array( $val, array_map( 'strval', $cur ), true ); case 'contains': foreach ( $cur as $c ) { if ( false !== stripos( (string) $c, $val ) ) { return true; } } return false; case 'empty': return '' === implode( '', array_map( 'strval', $cur ) ); case 'notempty': return '' !== implode( '', array_map( 'strval', $cur ) ); default: return in_array( $val, array_map( 'strval', $cur ), true ); } } /** Full field with wrapper, label, help. */ public static function render( array $f, $value, $name, array $ctx = array() ) { $f = array_merge( self::defaults(), $f ); $type = $f['type']; if ( 'hidden' === $type ) { return ''; } $id = 'frontglut-f-' . preg_replace( '/[^a-z0-9_-]/i', '-', $name ) . '-' . wp_rand( 100, 999 ); $attrs = ' data-frontglut-key="' . esc_attr( $f['key'] ) . '"'; if ( ! empty( $f['logic']['field'] ) ) { $lg = $f['logic']; $lg['name'] = ! empty( $ctx['prefix'] ) ? $ctx['prefix'] . '[' . $lg['field'] . ']' : $lg['field']; $attrs .= " data-frontglut-logic='" . esc_attr( wp_json_encode( $lg ) ) . "'"; } $width = in_array( (string) $f['width'], array( '25', '33', '50', '66', '75', '100' ), true ) ? $f['width'] : '100'; $cls = 'frontglut-field frontglut-w-' . $width . ' frontglut-t-' . sanitize_html_class( $type ) . ( $f['class'] ? ' ' . esc_attr( $f['class'] ) : '' ); if ( ! empty( $f['required'] ) ) { $cls .= ' is-required'; } $out = '
'; if ( 'heading' === $type ) { return $out . '

' . esc_html( $f['label'] ) . '

'; } if ( 'label' === $type ) { return $out . '
' . wp_kses_post( wpautop( $f['help'] ? $f['help'] : $f['label'] ) ) . '
'; } $bare = in_array( $type, array( 'checkbox' ), true ); if ( $f['label'] && ! $bare ) { $out .= ''; } $out .= self::control( $f, $value, $name, $id ); if ( $f['help'] ) { $out .= '

' . wp_kses_post( $f['help'] ) . '

'; } return $out . ''; } /** The bare input control. */ public static function control( array $f, $value, $name, $id = '' ) { $f = array_merge( self::defaults(), $f ); $type = $f['type']; $id = $id ? $id : 'frontglut-f-' . wp_rand( 1000, 9999 ); $n = esc_attr( $name ); $req = $f['required'] ? ' required' : ''; $ph = $f['placeholder'] ? ' placeholder="' . esc_attr( $f['placeholder'] ) . '"' : ''; $ml = $f['maxlength'] ? ' maxlength="' . (int) $f['maxlength'] . '"' : ''; $val = is_array( $value ) ? '' : (string) $value; $custom = apply_filters( 'frontglut_field_render_' . $type, null, $f, $value, $name, $id ); if ( null !== $custom ) { return $custom; } switch ( $type ) { case 'textarea': return ''; case 'editor': $mode = isset( $f['editor'] ) ? $f['editor'] : 'classic'; if ( 'lite' === $mode ) { return '
' . self::lite_toolbar() . '
' . wp_kses_post( $val ) . '
'; } if ( 'plain' === $mode ) { return ''; } ob_start(); wp_editor( $val, preg_replace( '/[^a-z0-9_]/', '_', strtolower( $id ) ), array( 'textarea_name' => $name, 'media_buttons' => false, 'textarea_rows' => 12, 'teeny' => true, 'quicktags' => true ) ); return '
' . ob_get_clean() . '
'; case 'select': $o = ''; case 'multiselect': $cur = (array) $value; $o = ''; case 'radio': $o = '
'; foreach ( self::options( $f ) as $k => $l ) { $o .= ''; } return $o . '
'; case 'checkboxes': $cur = array_map( 'strval', (array) $value ); $o = '
'; foreach ( self::options( $f ) as $k => $l ) { $o .= ''; } return $o . '
'; case 'checkbox': return ''; case 'file': case 'image': case 'gallery': return self::uploader( $f, $value, $name, $id ); case 'table': return self::table_control( $f, $value, $name ); case 'number': $a = ''; foreach ( array( 'min', 'max' ) as $k ) { if ( '' !== (string) $f[ $k ] ) { $a .= ' ' . $k . '="' . esc_attr( $f[ $k ] ) . '"'; } } return ''; case 'color': return ''; case 'text': case 'email': case 'url': case 'tel': case 'password': case 'date': default: $t = in_array( $type, array( 'email', 'url', 'tel', 'password', 'date' ), true ) ? $type : 'text'; $ac = 'password' === $t ? ' autocomplete="new-password"' : ''; return ''; } } private static function lite_toolbar() { $b = array( 'bold' => 'B', 'italic' => 'I', 'formatBlock:h3' => 'H', 'formatBlock:blockquote' => '❝', 'insertUnorderedList' => '•', 'insertOrderedList' => '1.', 'createLink' => '🔗', 'removeFormat' => '✕' ); $o = ''; foreach ( $b as $cmd => $lbl ) { $o .= ''; } return $o; } private static function uploader( array $f, $value, $name, $id ) { $multi = 'gallery' === $f['type'] || ! empty( $f['multiple'] ); $ids = array_filter( array_map( 'absint', is_array( $value ) ? $value : explode( ',', (string) $value ) ) ); $accept = 'image' === $f['type'] || 'gallery' === $f['type'] ? 'image/*' : ''; $o = '
'; $o .= '
'; foreach ( $ids as $aid ) { $o .= self::uploader_item( $aid, $name, $multi ); } $o .= '
'; $o .= '
'; return $o; } public static function uploader_item( $aid, $name, $multi ) { $aid = absint( $aid ); if ( ! $aid || ! get_post( $aid ) ) { return ''; } $thumb = wp_attachment_is_image( $aid ) ? wp_get_attachment_image( $aid, 'thumbnail' ) : ''; return '
' . $thumb . '' . esc_html( wp_basename( (string) get_attached_file( $aid ) ) ) . '
'; } private static function table_control( array $f, $value, $name ) { $cols = array_filter( array_map( 'trim', explode( ',', isset( $f['columns'] ) ? $f['columns'] : 'Column 1,Column 2' ) ) ); $rows = is_array( $value ) && $value ? array_values( $value ) : array( array() ); $o = '
'; foreach ( $cols as $c ) { $o .= ''; } $o .= ''; foreach ( $rows as $i => $row ) { $o .= ''; foreach ( array_values( $cols ) as $ci => $c ) { $v = isset( $row[ $ci ] ) ? $row[ $ci ] : ''; $o .= ''; } $o .= ''; } return $o . '
' . esc_html( $c ) . '
'; } /** Sanitize a raw submitted value for a field. */ public static function sanitize( array $f, $raw ) { $f = array_merge( self::defaults(), $f ); $type = $f['type']; $pre = apply_filters( 'frontglut_field_sanitize_' . $type, null, $f, $raw ); if ( null !== $pre ) { return $pre; } switch ( $type ) { case 'textarea': return sanitize_textarea_field( is_array( $raw ) ? '' : $raw ); case 'editor': return wp_kses_post( is_array( $raw ) ? '' : $raw ); case 'email': return sanitize_email( is_array( $raw ) ? '' : $raw ); case 'url': $u = is_array( $raw ) ? '' : trim( $raw ); return '' === $u ? '' : esc_url_raw( $u ); case 'number': $n = is_array( $raw ) ? '' : trim( $raw ); if ( '' === $n || ! is_numeric( $n ) ) { return ''; } return $n + 0; case 'date': $d = is_array( $raw ) ? '' : trim( $raw ); return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $d ) ? $d : ''; case 'color': return (string) sanitize_hex_color( is_array( $raw ) ? '' : $raw ); case 'checkbox': return ! empty( $raw ) ? '1' : ''; case 'select': case 'radio': $v = is_array( $raw ) ? '' : (string) $raw; return array_key_exists( $v, self::options( $f ) ) || '' === $v ? $v : ''; case 'multiselect': case 'checkboxes': $ok = array_map( 'strval', array_keys( self::options( $f ) ) ); return array_values( array_filter( array_map( 'sanitize_text_field', array_filter( (array) $raw, 'is_scalar' ) ), function ( $v ) use ( $ok ) { return in_array( (string) $v, $ok, true ); } ) ); case 'file': case 'image': case 'gallery': $ids = is_array( $raw ) ? $raw : explode( ',', (string) $raw ); $ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) ); if ( 'gallery' !== $type && empty( $f['multiple'] ) ) { $ids = array_slice( $ids, 0, 1 ); } return $ids; case 'table': $out = array(); foreach ( (array) $raw as $row ) { $row = array_map( function ( $c ) { return sanitize_text_field( is_scalar( $c ) ? $c : '' ); }, (array) $row ); if ( '' !== implode( '', $row ) ) { $out[] = array_values( $row ); } } return $out; case 'password': return is_array( $raw ) ? '' : (string) $raw; // never altered. case 'heading': case 'label': return ''; default: return sanitize_text_field( is_array( $raw ) ? '' : $raw ); } } public static function is_empty( $v ) { if ( is_array( $v ) ) { return 0 === count( array_filter( $v, function ( $x ) { return '' !== $x && null !== $x; } ) ); } return '' === trim( (string) $v ); } /** @return string|null error message */ public static function validate( array $f, $clean ) { $f = array_merge( self::defaults(), $f ); $label = $f['label'] ? $f['label'] : $f['key']; if ( ! empty( $f['required'] ) && self::is_empty( $clean ) ) { /* translators: %s: field label */ return sprintf( __( '%s is required.', 'frontglut' ), $label ); } if ( self::is_empty( $clean ) ) { return null; } if ( 'email' === $f['type'] && ! is_email( $clean ) ) { /* translators: %s: value. */ return sprintf( __( '%s must be a valid email.', 'frontglut' ), $label ); } if ( 'url' === $f['type'] && ! filter_var( $clean, FILTER_VALIDATE_URL ) ) { /* translators: %s: value. */ return sprintf( __( '%s must be a valid URL.', 'frontglut' ), $label ); } if ( 'number' === $f['type'] ) { if ( '' !== (string) $f['min'] && $clean < $f['min'] ) { /* translators: 1: value, 2: value. */ return sprintf( __( '%1$s must be at least %2$s.', 'frontglut' ), $label, $f['min'] ); } if ( '' !== (string) $f['max'] && $clean > $f['max'] ) { /* translators: 1: value, 2: value. */ return sprintf( __( '%1$s must be at most %2$s.', 'frontglut' ), $label, $f['max'] ); } } if ( is_string( $clean ) ) { $len = function_exists( 'mb_strlen' ) ? mb_strlen( $clean ) : strlen( $clean ); if ( $f['maxlength'] && $len > (int) $f['maxlength'] ) { /* translators: 1: value, 2: value. */ return sprintf( __( '%1$s must be at most %2$d characters.', 'frontglut' ), $label, (int) $f['maxlength'] ); } if ( $f['minlength'] && $len < (int) $f['minlength'] ) { /* translators: 1: value, 2: value. */ return sprintf( __( '%1$s must be at least %2$d characters.', 'frontglut' ), $label, (int) $f['minlength'] ); } } return apply_filters( 'frontglut_field_validate', null, $f, $clean ); } /** * Process a set of fields from posted data. * @return array [ values, errors ] */ public static function process( array $fields, array $post, array $ctx = array() ) { $raw_all = array(); foreach ( $fields as $f ) { if ( ! empty( $f['key'] ) ) { $raw_all[ $f['key'] ] = isset( $post[ $f['key'] ] ) ? $post[ $f['key'] ] : ''; } } $values = array(); $errors = array(); foreach ( $fields as $f ) { if ( empty( $f['key'] ) || in_array( isset( $f['type'] ) ? $f['type'] : '', array( 'heading', 'label' ), true ) ) { continue; } if ( ! self::is_visible( $f, $raw_all ) ) { continue; // hidden by logic: neither validated nor stored. } $clean = self::sanitize( $f, $raw_all[ $f['key'] ] ); $err = self::validate( $f, $clean ); if ( $err ) { $errors[ $f['key'] ] = $err; } $values[ $f['key'] ] = $clean; } return array( $values, $errors ); } }