[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 '';
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 '';
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 '' . esc_html( $codes[ $base ] ) . '';
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 );
}
}