[label, group, icon] */ public static function types() { $types = array( 'text' => array( __( 'Text', 'logvia' ), 'input', 'Aa' ), 'textarea' => array( __( 'Paragraph text', 'logvia' ), 'input', '¶' ), 'email' => array( __( 'Email', 'logvia' ), 'input', '@' ), 'tel' => array( __( 'Phone', 'logvia' ), 'input', '☎' ), 'number' => array( __( 'Number', 'logvia' ), 'input', '#' ), 'url' => array( __( 'Website', 'logvia' ), 'input', '⌘' ), 'date' => array( __( 'Date', 'logvia' ), 'input', '▦' ), 'select' => array( __( 'Dropdown', 'logvia' ), 'choice', '▾' ), 'radio' => array( __( 'Radio buttons', 'logvia' ), 'choice', '◉' ), 'checkbox' => array( __( 'Checkbox', 'logvia' ), 'choice', '☑' ), 'checkboxes' => array( __( 'Checkbox list', 'logvia' ), 'choice', '☰' ), 'country' => array( __( 'Country', 'logvia' ), 'choice', '⚑' ), 'state' => array( __( 'State / region', 'logvia' ), 'choice', '◫' ), 'heading' => array( __( 'Heading', 'logvia' ), 'content', 'H' ), 'paragraph' => array( __( 'Text block', 'logvia' ), 'content', '≡' ), ); if ( ! class_exists( 'WooCommerce' ) ) { unset( $types['country'], $types['state'] ); } /** * Filter the builder's field types. * * @param array $types Types. */ return apply_filters( 'logvia_field_types', $types ); } /** * Types that may appear only once. * * @return string[] */ public static function single_types() { return apply_filters( 'logvia_single_field_types', array( 'state' ) ); } /** * Built-in fields that cannot be deleted. * * @return string[] */ public static function core_types() { return array( 'core_email', 'core_password', 'core_password_confirm' ); } /** * Types without a value (layout only). * * @return string[] */ public static function content_types() { return array( 'heading', 'paragraph' ); } /** * Where a field's value can be saved. * * @return array key => label */ public static function maps() { $maps = array( 'meta' => __( 'Custom user field (meta key)', 'logvia' ), 'first_name' => __( 'First name', 'logvia' ), 'last_name' => __( 'Last name', 'logvia' ), 'nickname' => __( 'Nickname', 'logvia' ), 'user_url' => __( 'Website', 'logvia' ), ); if ( class_exists( 'WooCommerce' ) ) { $maps += array( 'billing_company' => __( 'Billing: company', 'logvia' ), 'billing_phone' => __( 'Billing: phone', 'logvia' ), 'billing_address_1' => __( 'Billing: address line 1', 'logvia' ), 'billing_address_2' => __( 'Billing: address line 2', 'logvia' ), 'billing_city' => __( 'Billing: city', 'logvia' ), 'billing_state' => __( 'Billing: state / region', 'logvia' ), 'billing_postcode' => __( 'Billing: postcode', 'logvia' ), 'billing_country' => __( 'Billing: country', 'logvia' ), ); } return apply_filters( 'logvia_field_maps', $maps ); } /** * Destinations a type may use ('*' = any). * * @param string $type Type. * @return string[] */ public static function maps_for( $type ) { switch ( $type ) { case 'country': $maps = array( 'billing_country', 'meta' ); break; case 'state': $maps = array( 'billing_state', 'meta' ); break; case 'checkbox': case 'checkboxes': $maps = array( 'meta' ); break; default: $maps = array( '*' ); } /** * Filter where a field type may save its value (an empty list means * the type saves itself). * * @param string[] $maps Destinations. * @param string $type Type. */ return apply_filters( 'logvia_field_maps_for', $maps, $type ); } /** * Roles visitors may be given or choose: never roles that can publish, * upload, manage the shop or manage users. * * @return array slug => name */ public static function assignable_roles() { $danger = array( 'edit_posts', 'publish_posts', 'upload_files', 'manage_options', 'manage_woocommerce', 'edit_shop_orders', 'edit_users', 'create_users', 'delete_users', 'promote_users', 'list_users', 'edit_theme_options', 'activate_plugins', 'unfiltered_html', 'moderate_comments', 'edit_others_posts' ); $out = array(); foreach ( wp_roles()->roles as $slug => $role ) { $caps = isset( $role['capabilities'] ) ? array_filter( (array) $role['capabilities'] ) : array(); if ( array_intersect( $danger, array_keys( $caps ) ) ) { continue; } $out[ $slug ] = translate_user_role( $role['name'] ); } return apply_filters( 'logvia_assignable_roles', $out ); } /** * Default registration form (matches the plugin's original form). * * @return array */ public static function default_fields() { return array( array( 'id' => 'email', 'type' => 'core_email', 'label' => __( 'Email', 'logvia' ), 'placeholder' => '', 'help' => '', 'width' => 'full', ), array( 'id' => 'password', 'type' => 'core_password', 'label' => __( 'Password', 'logvia' ), 'placeholder' => '', 'help' => '', 'width' => 'full', ), array( 'id' => 'password_confirm', 'type' => 'core_password_confirm', 'label' => __( 'Confirm password', 'logvia' ), 'placeholder' => '', 'help' => '', 'width' => 'full', 'enabled' => true, ), ); } /** * Saved registration fields (sanitized). * * @return array */ public static function fields() { if ( null === self::$cache ) { $saved = get_option( self::OPTION, array() ); $fields = ( is_array( $saved ) && ! empty( $saved['register'] ) && is_array( $saved['register'] ) ) ? $saved['register'] : self::default_fields(); self::$cache = self::sanitize_fields( $fields ); } return self::$cache; } /** * Save registration fields. * * @param array $fields Raw fields. * @return array Sanitized fields. */ public static function save( $fields ) { $clean = self::sanitize_fields( $fields ); update_option( self::OPTION, array( 'register' => $clean ), false ); self::$cache = null; return $clean; } /** * Forget the request cache. */ public static function flush() { self::$cache = null; } /** * Confirm-password field is on. * * @return bool */ public static function confirm_enabled() { if ( self::passwords_hidden() ) { return false; } foreach ( self::fields() as $f ) { if ( 'core_password_confirm' === $f['type'] ) { return ! empty( $f['enabled'] ); } } return false; } /** * Are the password fields left out of the form? * * @return bool */ public static function passwords_hidden() { /** * Hide the password fields (when an add-on sets passwords another way). * * @param bool $hidden Hidden. */ return (bool) apply_filters( 'logvia_hide_password_fields', false ); } /** * Does the form need multipart encoding (file inputs)? * * @return bool */ public static function has_files() { return (bool) apply_filters( 'logvia_form_has_files', false, self::fields() ); } /* ------------------------------------------------------------------ * Sanitizing the schema * ------------------------------------------------------------------ */ /** * Meta keys a visitor-supplied value must never be written to. * * @param string $key Meta key. * @return bool */ public static function meta_key_allowed( $key ) { global $wpdb; if ( '' === $key || '_' === $key[0] ) { return false; } $lower = strtolower( $key ); if ( 0 === strpos( $lower, strtolower( $wpdb->base_prefix ) ) || 0 === strpos( $lower, 'wp_' ) ) { return false; } foreach ( array( 'capabilit', 'user_level', 'session_token', 'password', 'nonce', 'token', 'secret' ) as $bad ) { if ( false !== strpos( $lower, $bad ) ) { return false; } } $reserved = array( 'first_name', 'last_name', 'nickname', 'description', 'rich_editing', 'syntax_highlighting', 'comment_shortcuts', 'show_admin_bar_front', 'locale', 'use_ssl', 'primary_blog', 'source_domain', 'dismissed_wp_pointers', 'paying_customer', 'wc_last_active', 'logvia_status', 'logvia_avatar', '_logvia_google_sub' ); if ( in_array( $lower, $reserved, true ) || 0 === strpos( $lower, 'billing_' ) || 0 === strpos( $lower, 'shipping_' ) ) { return false; } return (bool) apply_filters( 'logvia_meta_key_allowed', true, $key ); } /** * Clean a field list coming from the builder. * * @param mixed $fields Raw fields. * @return array */ public static function sanitize_fields( $fields ) { $fields = is_array( $fields ) ? $fields : array(); $types = array_merge( array_keys( self::types() ), self::core_types() ); $maps = array_keys( self::maps() ); $out = array(); $seen = array(); $singles = array(); $used_to = array(); foreach ( $fields as $raw ) { if ( ! is_array( $raw ) || empty( $raw['type'] ) || ! in_array( $raw['type'], $types, true ) ) { continue; } $type = $raw['type']; if ( in_array( $type, self::single_types(), true ) ) { if ( isset( $singles[ $type ] ) ) { continue; } $singles[ $type ] = true; } // Core fields have fixed IDs and appear once. if ( in_array( $type, self::core_types(), true ) ) { $core_ids = array( 'core_email' => 'email', 'core_password' => 'password', 'core_password_confirm' => 'password_confirm', ); $id = $core_ids[ $type ]; } else { $id = isset( $raw['id'] ) ? sanitize_key( $raw['id'] ) : ''; if ( '' === $id || in_array( $id, array( 'email', 'password', 'password_confirm' ), true ) ) { $id = 'f_' . substr( md5( wp_json_encode( $raw ) . count( $out ) ), 0, 8 ); } } if ( isset( $seen[ $id ] ) ) { continue; } $seen[ $id ] = true; $field = array( 'id' => $id, 'type' => $type, 'label' => sanitize_text_field( isset( $raw['label'] ) ? $raw['label'] : '' ), 'placeholder' => sanitize_text_field( isset( $raw['placeholder'] ) ? $raw['placeholder'] : '' ), 'help' => sanitize_text_field( isset( $raw['help'] ) ? $raw['help'] : '' ), 'width' => ( isset( $raw['width'] ) && 'half' === $raw['width'] ) ? 'half' : 'full', ); if ( 'core_password_confirm' === $type ) { $field['enabled'] = ! empty( $raw['enabled'] ); } $has_value = ! in_array( $type, self::core_types(), true ) && ! in_array( $type, self::content_types(), true ); if ( $has_value ) { $field['required'] = ! empty( $raw['required'] ); $field['default'] = sanitize_text_field( isset( $raw['default'] ) && is_scalar( $raw['default'] ) ? $raw['default'] : '' ); $field['maxlength'] = isset( $raw['maxlength'] ) ? min( 2000, absint( $raw['maxlength'] ) ) : 0; $allowed = self::maps_for( $type ); $map = isset( $raw['map'] ) && in_array( $raw['map'], $maps, true ) ? $raw['map'] : 'meta'; if ( ! $allowed ) { $map = 'self'; } elseif ( '*' !== $allowed[0] && ! in_array( $map, $allowed, true ) ) { $map = $allowed[0]; } // A core destination may receive only one field. if ( ! in_array( $map, array( 'meta', 'self' ), true ) && isset( $used_to[ $map ] ) ) { $map = 'meta'; } $meta_key = ''; if ( 'meta' === $map ) { $meta_key = sanitize_key( isset( $raw['meta_key'] ) ? $raw['meta_key'] : '' ); if ( ! self::meta_key_allowed( $meta_key ) || isset( $used_to[ 'meta:' . $meta_key ] ) ) { $meta_key = 'logvia_' . $id; } } $used_to[ 'meta' === $map ? 'meta:' . $meta_key : $map ] = true; $field['map'] = $map; $field['meta_key'] = $meta_key; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Array key in the form schema, not a query. } if ( in_array( $type, array( 'select', 'radio', 'checkboxes' ), true ) ) { $options = isset( $raw['options'] ) ? $raw['options'] : array(); if ( is_string( $options ) ) { $options = preg_split( '/\r\n|\r|\n/', $options ); } $options = array_values( array_unique( array_filter( array_map( 'sanitize_text_field', (array) $options ), 'strlen' ) ) ); $field['options'] = array_slice( $options, 0, 100 ); } if ( 'tel' === $type ) { $field['country_code'] = ! empty( $raw['country_code'] ) && class_exists( 'WooCommerce' ); } if ( in_array( $type, array( 'checkbox', 'paragraph' ), true ) ) { // Short rich text: links and emphasis only (for "I agree to the terms"). $field['content'] = wp_kses( isset( $raw['content'] ) ? (string) $raw['content'] : '', array( 'a' => array( 'href' => true, 'target' => true, 'rel' => true, ), 'strong' => array(), 'em' => array(), 'br' => array(), ) ); } /** * Filter one sanitized field (add-ons keep their own settings here). * * @param array $field Sanitized field. * @param array $raw Raw field from the builder. */ $out[] = apply_filters( 'logvia_sanitize_field', $field, $raw ); } // Core fields must always exist. foreach ( self::default_fields() as $core ) { if ( ! isset( $seen[ $core['id'] ] ) ) { $out[] = $core; } } return array_slice( $out, 0, 60 ); } /* ------------------------------------------------------------------ * Rendering * ------------------------------------------------------------------ */ /** * Echo the registration fields. * * @param array $args Template args (min_password, sfx). */ public static function render_register_fields( $args ) { echo '
'; foreach ( self::fields() as $field ) { if ( self::passwords_hidden() && in_array( $field['type'], array( 'core_password', 'core_password_confirm' ), true ) ) { continue; } self::render_field( $field, $args ); } echo '
'; } /** * Echo one field. * * @param array $f Field. * @param array $args Template args. * @param mixed|null $value Current value (null = use the default). */ public static function render_field( $f, $args = array(), $value = null ) { $type = $f['type']; $sfx = isset( $args['sfx'] ) ? (string) $args['sfx'] : ''; $core_ids = array( 'core_email' => 'logvia-register-email', 'core_password' => 'logvia-register-password', 'core_password_confirm' => 'logvia-register-confirm', ); $id = ( isset( $core_ids[ $type ] ) ? $core_ids[ $type ] : 'logvia-reg-' . $f['id'] ) . $sfx; $name = 'logvia_f[' . $f['id'] . ']'; $class = 'logvia-field logvia-field--' . $f['width'] . ' logvia-field--' . $type; $req = ! empty( $f['required'] ); $label = '' !== $f['label'] ? $f['label'] : self::type_label( $type ); $min = isset( $args['min_password'] ) ? (int) $args['min_password'] : 8; $star = $req ? ' ' : ''; $current = null === $value ? ( isset( $f['default'] ) ? $f['default'] : '' ) : $value; if ( 'core_password_confirm' === $type && empty( $f['enabled'] ) ) { return; } /** * Render a field type registered by an add-on. Echo the markup and * return true when handled. * * @param bool $handled Handled. * @param array $f Field. * @param array $args Template args. * @param mixed|null $current Current value. */ if ( apply_filters( 'logvia_render_field', false, $f, $args, $current ) ) { return; } if ( 'heading' === $type ) { echo '

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

'; return; } if ( 'paragraph' === $type ) { echo '
' . wp_kses_post( wpautop( $f['content'] ) ) . '
'; return; } echo '
'; if ( 'checkbox' === $type ) { $text = '' !== $f['content'] ? $f['content'] : esc_html( $label ); echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $star is static markup. self::help( $f ); echo '
'; return; } if ( 'radio' === $type || 'checkboxes' === $type ) { $multi = 'checkboxes' === $type; $selected = $multi ? array_map( 'strval', (array) ( is_array( $current ) ? $current : array_filter( array( $current ) ) ) ) : array( (string) $current ); echo '
' . esc_html( $label ) . $star . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $star is static markup. foreach ( $f['options'] as $i => $opt ) { echo ''; } echo '
'; self::help( $f ); echo ''; return; } echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $star is static markup. $ph = '' !== $f['placeholder'] ? ' placeholder="' . esc_attr( $f['placeholder'] ) . '"' : ''; switch ( $type ) { case 'core_email': echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. break; case 'core_password': Logvia_Frontend::password_input( 'pwd', $id, 'new-password', array_filter( array( 'minlength' => $min, 'placeholder' => $f['placeholder'] ) ), true ); if ( '' === $f['help'] ) { /* translators: %d: minimum number of characters. */ echo '' . esc_html( sprintf( __( 'At least %d characters.', 'logvia' ), $min ) ) . ''; } break; case 'core_password_confirm': Logvia_Frontend::password_input( 'pwd_confirm', $id, 'new-password', array_filter( array( 'minlength' => $min, 'placeholder' => $f['placeholder'] ) ) ); break; case 'textarea': echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. break; case 'select': case 'country': if ( 'country' === $type ) { $options = self::countries(); if ( '' === (string) $current && function_exists( 'WC' ) && WC()->countries ) { $current = WC()->countries->get_base_country(); } } else { $options = array_combine( $f['options'], $f['options'] ); } echo ''; break; case 'state': self::render_state( $f, $id, $name, $current, $req, $ph ); break; case 'tel': if ( ! empty( $f['country_code'] ) ) { self::render_phone( $f, $id, $name, $current, $req, $ph ); break; } echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. break; default: $input_type = in_array( $type, array( 'email', 'number', 'url', 'date' ), true ) ? $type : 'text'; $autocomplete = self::autocomplete( $f ); echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. } self::help( $f ); echo ''; } /** * State field: a select when the country has states, otherwise text. * * @param array $f Field. * @param string $id Input ID. * @param string $name Input name. * @param string $current Current value. * @param bool $req Required. * @param string $ph Placeholder attribute. */ private static function render_state( $f, $id, $name, $current, $req, $ph ) { $country = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_base_country() : ''; $states = $country ? self::states( $country ) : array(); $prompt = '' !== $f['placeholder'] ? $f['placeholder'] : __( '— Select —', 'logvia' ); if ( ! self::$states_printed && function_exists( 'WC' ) && WC()->countries ) { self::$states_printed = true; echo ''; } echo ''; if ( $states ) { echo ''; } else { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. } echo ''; } /** * Phone with a country calling-code picker. * * @param array $f Field. * @param string $id Input ID. * @param string $name Input name. * @param string $current Current value. * @param bool $req Required. * @param string $ph Placeholder attribute. */ private static function render_phone( $f, $id, $name, $current, $req, $ph ) { $codes = self::calling_codes(); if ( ! $codes ) { echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. return; } $names = self::countries(); $base = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_base_country() : ''; $base = isset( $codes[ $base ] ) ? $base : (string) key( $codes ); $flags = LOGVIA_URL . 'assets/flags/'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above. echo ''; echo ''; } /** * Country => calling code for allowed countries. * * @return array */ public static function calling_codes() { $out = array(); if ( ! function_exists( 'WC' ) || ! WC()->countries ) { return $out; } foreach ( array_keys( self::countries() ) as $cc ) { $code = WC()->countries->get_country_calling_code( $cc ); $code = is_array( $code ) ? reset( $code ) : $code; if ( $code ) { $out[ $cc ] = $code; } } return $out; } /** * States of a country. * * @param string $country Country code. * @return array */ public static function states( $country ) { if ( ! function_exists( 'WC' ) || ! WC()->countries ) { return array(); } $states = WC()->countries->get_states( $country ); return is_array( $states ) ? $states : array(); } /** * Help text under a field. * * @param array $f Field. */ private static function help( $f ) { if ( '' !== $f['help'] ) { echo '' . esc_html( $f['help'] ) . ''; } } /** * Browser autocomplete hint from the field's destination. * * @param array $f Field. * @return string */ private static function autocomplete( $f ) { $map = array( 'first_name' => 'given-name', 'last_name' => 'family-name', 'nickname' => 'nickname', 'user_url' => 'url', 'billing_company' => 'organization', 'billing_phone' => 'tel', 'billing_address_1' => 'address-line1', 'billing_address_2' => 'address-line2', 'billing_city' => 'address-level2', 'billing_state' => 'address-level1', 'billing_postcode' => 'postal-code', ); if ( isset( $f['map'], $map[ $f['map'] ] ) ) { return $map[ $f['map'] ]; } return ''; } /** * Human label of a type. * * @param string $type Type. * @return string */ public static function type_label( $type ) { $types = self::types(); $core = array( 'core_email' => __( 'Email', 'logvia' ), 'core_password' => __( 'Password', 'logvia' ), 'core_password_confirm' => __( 'Confirm password', 'logvia' ), ); if ( isset( $core[ $type ] ) ) { return $core[ $type ]; } return isset( $types[ $type ] ) ? $types[ $type ][0] : $type; } /** * WooCommerce country list. * * @return array */ public static function countries() { if ( function_exists( 'WC' ) && WC()->countries ) { return WC()->countries->get_allowed_countries(); } return array(); } /* ------------------------------------------------------------------ * Submission * ------------------------------------------------------------------ */ /** * Validate the custom fields of a registration. * * @param array $data Raw request data. * @param array|null $fields Fields to check (default: the whole form). * @return array|WP_Error Values keyed by field ID, or an error whose data * holds the field ID and a human message. */ public static function validate( $data, $fields = null ) { $posted = isset( $data['logvia_f'] ) && is_array( $data['logvia_f'] ) ? wp_unslash( $data['logvia_f'] ) : array(); $codes = isset( $data['logvia_fc'] ) && is_array( $data['logvia_fc'] ) ? wp_unslash( $data['logvia_fc'] ) : array(); $fields = null === $fields ? self::fields() : $fields; $values = array(); // Country chosen in this form (for the state field). $country = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_base_country() : ''; foreach ( $fields as $f ) { if ( 'country' === $f['type'] && isset( $posted[ $f['id'] ] ) && is_scalar( $posted[ $f['id'] ] ) && '' !== $posted[ $f['id'] ] ) { $country = sanitize_text_field( $posted[ $f['id'] ] ); break; } } foreach ( $fields as $f ) { $type = $f['type']; if ( in_array( $type, self::core_types(), true ) || in_array( $type, self::content_types(), true ) || ! apply_filters( 'logvia_field_posts_value', true, $f ) ) { continue; } $label = wp_strip_all_tags( '' !== $f['label'] ? $f['label'] : ( isset( $f['content'] ) && '' !== $f['content'] ? $f['content'] : self::type_label( $type ) ) ); if ( 'checkboxes' === $type ) { $raw = isset( $posted[ $f['id'] ] ) ? (array) $posted[ $f['id'] ] : array(); $value = array_values( array_intersect( array_map( 'strval', array_filter( $raw, 'is_scalar' ) ), $f['options'] ) ); if ( ! $value && ! empty( $f['required'] ) ) { /* translators: %s: field label. */ return self::field_error( $f['id'], 'field_required', sprintf( __( '%s is required.', 'logvia' ), $label ) ); } if ( $value ) { $values[ $f['id'] ] = $value; } continue; } $raw = isset( $posted[ $f['id'] ] ) && is_scalar( $posted[ $f['id'] ] ) ? (string) $posted[ $f['id'] ] : ''; if ( 'tel' === $type && ! empty( $f['country_code'] ) && '' !== trim( $raw ) ) { $cc = isset( $codes[ $f['id'] ] ) && is_scalar( $codes[ $f['id'] ] ) ? strtoupper( sanitize_key( $codes[ $f['id'] ] ) ) : ''; $available = self::calling_codes(); if ( ! isset( $available[ $cc ] ) ) { /* translators: %s: field label. */ return self::field_error( $f['id'], 'field_invalid', sprintf( __( 'Please check %s.', 'logvia' ), $label ) ); } $raw = $available[ $cc ] . ' ' . ltrim( preg_replace( '/^\+?' . preg_quote( ltrim( $available[ $cc ], '+' ), '/' ) . '/', '', trim( $raw ) ) ); } $value = self::clean_value( $f, $raw, $country ); if ( '' === $value ) { if ( ! empty( $f['required'] ) ) { /* translators: %s: field label. */ return self::field_error( $f['id'], 'field_required', sprintf( __( '%s is required.', 'logvia' ), $label ) ); } continue; } if ( false === $value ) { /* translators: %s: field label. */ return self::field_error( $f['id'], 'field_invalid', sprintf( __( 'Please check %s.', 'logvia' ), $label ) ); } $values[ $f['id'] ] = $value; } /** * Filter validated custom field values before they are stored. * * @param array|WP_Error $values Values. * @param array $data Request data. */ return apply_filters( 'logvia_validate_fields', $values, $data ); } /** * Error helper. * * @param string $field Field ID. * @param string $code Message code. * @param string $text Human message. * @return WP_Error */ private static function field_error( $field, $code, $text ) { return new WP_Error( $code, $text, array( 'field' => $field, 'text' => $text, ) ); } /** * Sanitize one value. Returns '' when empty, false when invalid. * * @param array $f Field. * @param string $raw Raw value. * @param string $country Country chosen in the form. * @return string|false */ private static function clean_value( $f, $raw, $country = '' ) { /** * Clean the value of an add-on field type. Return null to use the * built-in rules, '' for empty or false for invalid. * * @param mixed $value Null. * @param array $f Field. * @param string $raw Raw value. */ $custom = apply_filters( 'logvia_clean_field_value', null, $f, $raw ); if ( null !== $custom ) { return $custom; } $raw = trim( $raw ); if ( '' === $raw ) { return ''; } if ( ! empty( $f['maxlength'] ) && mb_strlen( $raw ) > $f['maxlength'] ) { return false; } switch ( $f['type'] ) { case 'textarea': return sanitize_textarea_field( $raw ); case 'email': $v = sanitize_email( $raw ); return is_email( $v ) ? $v : false; case 'tel': $v = preg_replace( '/[^0-9+\-\s().]/', '', $raw ); return preg_match( '/\d{5,}/', preg_replace( '/\D/', '', $v ) ) ? trim( $v ) : false; case 'number': return is_numeric( $raw ) ? (string) ( 0 + $raw ) : false; case 'url': $v = esc_url_raw( $raw, array( 'http', 'https' ) ); return $v ? $v : false; case 'date': $d = DateTime::createFromFormat( 'Y-m-d', $raw ); return ( $d && $d->format( 'Y-m-d' ) === $raw ) ? $raw : false; case 'select': case 'radio': return in_array( $raw, $f['options'], true ) ? $raw : false; case 'country': return array_key_exists( $raw, self::countries() ) ? $raw : false; case 'state': $states = $country ? self::states( $country ) : array(); if ( $states ) { return array_key_exists( $raw, $states ) ? $raw : false; } return sanitize_text_field( $raw ); case 'checkbox': return '1' === $raw ? '1' : ''; default: return sanitize_text_field( $raw ); } } /** * Store validated values (and uploaded files) on a user. * * @param int $user_id User ID. * @param array $values Values keyed by field ID. * @param array|null $fields Fields (default: the whole form). */ public static function save_user_values( $user_id, $values, $fields = null ) { $fields = null === $fields ? self::fields() : $fields; $core = array(); foreach ( $fields as $f ) { if ( ! isset( $f['map'] ) || ! array_key_exists( $f['id'], $values ) ) { continue; } $value = $values[ $f['id'] ]; switch ( $f['map'] ) { case 'first_name': case 'last_name': case 'nickname': case 'user_url': $core[ $f['map'] ] = $value; if ( in_array( $f['map'], array( 'first_name', 'last_name' ), true ) && class_exists( 'WooCommerce' ) && ! get_user_meta( $user_id, 'billing_' . $f['map'], true ) ) { update_user_meta( $user_id, 'billing_' . $f['map'], $value ); } break; case 'meta': if ( self::meta_key_allowed( $f['meta_key'] ) ) { update_user_meta( $user_id, $f['meta_key'], $value ); } break; default: if ( 0 === strpos( $f['map'], 'billing_' ) ) { update_user_meta( $user_id, $f['map'], $value ); } } } if ( $core ) { $core['ID'] = $user_id; if ( ! empty( $core['first_name'] ) ) { $core['display_name'] = trim( $core['first_name'] . ' ' . ( isset( $core['last_name'] ) ? $core['last_name'] : '' ) ); } wp_update_user( $core ); } /** * Fires after registration form values are saved. * * @param int $user_id User ID. * @param array $values Values keyed by field ID. */ do_action( 'logvia_fields_saved', $user_id, $values ); } /** * Fields saved to custom meta (shown on the user profile screen). * * @return array */ public static function meta_fields() { return array_values( array_filter( self::fields(), function ( $f ) { return isset( $f['map'] ) && 'meta' === $f['map'] && '' !== $f['meta_key']; } ) ); } /** * Display text for a stored value (lists, files, checkboxes). * * @param array $f Field. * @param mixed $value Value. * @return string Escaped HTML. */ public static function display_value( $f, $value ) { /** * Display an add-on type's stored value (return escaped HTML). * * @param string|null $html Null to use the default. * @param array $f Field. * @param mixed $value Value. */ $custom = apply_filters( 'logvia_display_field_value', null, $f, $value ); if ( null !== $custom ) { return $custom; } if ( is_array( $value ) ) { return esc_html( implode( ', ', $value ) ); } if ( 'checkbox' === $f['type'] ) { return '1' === (string) $value ? '✓' : '—'; } if ( 'country' === $f['type'] ) { $c = self::countries(); return esc_html( isset( $c[ $value ] ) ? $c[ $value ] : (string) $value ); } return '' === (string) $value ? '—' : esc_html( (string) $value ); } }