*/ public static function custom(): array { $fields = (array) apply_filters( 'dashbread_profile_fields', array() ); $out = array(); foreach ( $fields as $f ) { if ( empty( $f['key'] ) || empty( $f['label'] ) ) { continue; } $out[] = array_merge( array( 'field_type' => 'text', 'required' => false, 'options' => array(), 'help' => '', 'section' => __( 'More about you', 'dashbread' ) ), $f ); } return $out; } public static function value( int $user_id, string $key ): mixed { return get_user_meta( $user_id, 'dashbread_' . sanitize_key( $key ), true ); } /** @param array $field */ public static function render( array $field, mixed $value ): string { $type = (string) $field['field_type']; if ( ! in_array( $type, self::CORE_TYPES, true ) ) { /** Pro and add-ons render field types Core doesn't know. */ return (string) apply_filters( 'dashbread_profile_render_field', '', $field, $value ); } $key = sanitize_key( (string) $field['key'] ); $id = 'sui-f-' . $key; $name = 'dashbread_fields[' . $key . ']'; $req = ! empty( $field['required'] ) ? ' required' : ''; $opts = self::choices( $field ); $ph = ! empty( $field['placeholder'] ) ? ' placeholder="' . esc_attr( (string) $field['placeholder'] ) . '"' : ''; $label = ''; $help = ! empty( $field['help'] ) ? '' . esc_html( (string) $field['help'] ) . '' : ''; switch ( $type ) { case 'textarea': $ctl = ''; break; case 'select': $ctl = ''; break; case 'multiselect': case 'checkbox': $vals = (array) $value; $ctl = '
'; foreach ( $opts as $v => $l ) { $ctl .= ''; } if ( ! $opts ) { // a single yes/no checkbox $ctl .= ''; } $ctl .= '
'; break; case 'radio': $ctl = '
'; foreach ( $opts as $v => $l ) { $ctl .= ''; } $ctl .= '
'; break; default: $html_type = in_array( $type, array( 'email', 'url', 'number', 'date' ), true ) ? $type : ( 'phone' === $type ? 'tel' : 'text' ); $ctl = ''; } return self::wrap( $field, $label . $ctl . $help ); } /** * The `.sui-field` wrapper every custom field shares, so Pro's composite and file types sit in the * same grid and get the same conditional-logic data attributes. * * @param array $field */ public static function wrap( array $field, string $inner ): string { $type = sanitize_key( (string) ( $field['field_type'] ?? 'text' ) ); $class = 'sui-field sui-field-' . $type . ( ! empty( $field['full'] ) ? ' sui-field-full' : '' ); $attrs = ' data-sui-field="' . esc_attr( sanitize_key( (string) $field['key'] ) ) . '"'; if ( ! empty( $field['visible_if']['field'] ) ) { $attrs .= ' data-sui-if="' . esc_attr( (string) wp_json_encode( array( 'field' => sanitize_key( (string) $field['visible_if']['field'] ), 'value' => (string) ( $field['visible_if']['value'] ?? '' ) ) ) ) . '"'; } return '
' . $inner . '
'; } /** @param array $field @return array value => label */ private static function choices( array $field ): array { $raw = $field['options']['choices'] ?? $field['options'] ?? array(); $out = array(); foreach ( (array) $raw as $k => $v ) { if ( is_array( $v ) ) { $out[ (string) ( $v['value'] ?? $v['label'] ?? $k ) ] = (string) ( $v['label'] ?? $v['value'] ?? $k ); } elseif ( is_int( $k ) ) { $out[ (string) $v ] = (string) $v; } else { $out[ (string) $k ] = (string) $v; } } return $out; } /** Sanitize a submitted value for a field. */ public static function sanitize( array $field, mixed $raw ): mixed { $type = (string) $field['field_type']; if ( ! in_array( $type, self::CORE_TYPES, true ) ) { // A type Core doesn't know is plain text unless the add-on that defines it says otherwise // (Pro: composite name/address parts, file uploads). The add-on gets the sanitized value. $clean = is_array( $raw ) ? map_deep( $raw, 'sanitize_text_field' ) : sanitize_text_field( (string) $raw ); /** * Sanitize a field type Core doesn't know. Return null to leave the stored value unchanged. * * @param mixed $clean The value through sanitize_text_field() (each element, for arrays). * @param array $field The field definition. * @param mixed $value The same sanitized value (third argument kept for add-ons written against 1.0). */ return apply_filters( 'dashbread_profile_sanitize_field', $clean, $field, $clean ); } $opts = array_keys( self::choices( $field ) ); $text = is_scalar( $raw ) ? (string) $raw : ''; switch ( $type ) { case 'textarea': return sanitize_textarea_field( $text ); case 'email': return sanitize_email( $text ); case 'url': return esc_url_raw( $text ); case 'number': return is_scalar( $raw ) && is_numeric( $raw ) ? (string) ( 0 + $raw ) : ''; case 'select': case 'radio': return in_array( $text, $opts, true ) ? $text : ''; case 'multiselect': case 'checkbox': if ( ! $opts ) { return ! empty( $raw ) ? '1' : ''; } return array_values( array_intersect( array_map( 'strval', array_filter( (array) $raw, 'is_scalar' ) ), $opts ) ); default: return sanitize_text_field( $text ); } } }