result of the last non-AJAX submission this request. */
private $results = array();
private $assets = false;
public function __construct( Bylt_Forms $forms ) {
$this->forms = $forms;
}
public function init() {
add_shortcode( 'bylt_form', array( $this, 'shortcode' ) );
add_action( 'init', array( $this, 'register_block' ) );
add_action( 'template_redirect', array( $this, 'preview' ), 4 );
add_action( 'template_redirect', array( $this, 'handle_post' ), 5 );
add_action( 'wp_ajax_bylt_form_submit', array( $this, 'ajax' ) );
add_action( 'wp_ajax_nopriv_bylt_form_submit', array( $this, 'ajax' ) );
add_action( 'woocommerce_single_product_summary', array( $this, 'quote_button' ), 35 );
}
/* ---------------------------------------------------------------------
* Output
* ------------------------------------------------------------------- */
public function register_block() {
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
wp_register_script( 'bylt-form-block', $this->forms->url( 'assets/forms-block.js' ), array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-server-side-render', 'wp-i18n' ), BYLT_VERSION, true );
register_block_type(
'bylt/form',
array(
'api_version' => 2,
'editor_script' => 'bylt-form-block',
'attributes' => array(
'formId' => array( 'type' => 'number', 'default' => 0 ),
),
'render_callback' => function ( $atts ) {
return $this->render( isset( $atts['formId'] ) ? (int) $atts['formId'] : 0 );
},
)
);
if ( is_admin() ) {
$list = array();
foreach ( Bylt_Forms_Store::all() as $f ) {
$list[] = array( 'value' => $f['id'], 'label' => $f['name'] );
}
wp_localize_script( 'bylt-form-block', 'byltFormsBlock', array( 'forms' => $list, 'adminUrl' => Bylt_Forms_Admin::url() ) );
}
}
public function shortcode( $atts ) {
$atts = shortcode_atts( array( 'id' => 0 ), $atts, 'bylt_form' );
return $this->render( (int) $atts['id'] );
}
private function enqueue() {
if ( $this->assets ) {
return;
}
$this->assets = true;
wp_enqueue_style( 'bylt-forms', $this->forms->url( 'assets/forms.css' ), array(), BYLT_VERSION );
wp_enqueue_script( 'bylt-forms', $this->forms->url( 'assets/forms.js' ), array(), BYLT_VERSION, true );
wp_localize_script(
'bylt-forms',
'byltForms',
array(
'ajax' => admin_url( 'admin-ajax.php' ),
'sending' => __( 'Sending…', 'byltcommerce-toolkit' ),
'error' => __( 'Something went wrong. Please try again.', 'byltcommerce-toolkit' ),
'preview' => __( 'Preview: everything checks out. Nothing was sent or saved.', 'byltcommerce-toolkit' ),
/* translators: %s: size like "5 MB" */
'tooBig' => sprintf( __( 'That file is too big. The limit is %s.', 'byltcommerce-toolkit' ), size_format( Bylt_Forms_Files::max_bytes() ) ),
)
);
}
private static function current_url() {
$uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
return remove_query_arg( array( 'bylt_sent' ), home_url( $uri ) );
}
/** Value to show in a field: posted value after an error, else URL parameter, else default. */
private static function initial( array $f, $posted ) {
if ( null !== $posted && isset( $posted[ $f['key'] ] ) ) {
return $posted[ $f['key'] ];
}
// phpcs:ignore WordPress.Security.NonceVerification -- prefill only.
if ( $f['param'] && isset( $_GET[ $f['param'] ] ) ) {
$v = wp_unslash( $_GET[ $f['param'] ] ); // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput
return is_array( $v ) ? array_map( 'sanitize_text_field', $v ) : sanitize_text_field( $v );
}
if ( 'checkboxes' === $f['type'] ) {
return '' === $f['default'] ? array() : array_map( 'trim', explode( ',', $f['default'] ) );
}
return $f['default'];
}
public function render( $id ) {
$form = Bylt_Forms_Store::get( $id );
if ( ! $form ) {
return current_user_can( 'edit_pages' ) ? '
' . esc_html__( 'Choose a form in the block settings (Forms).', 'byltcommerce-toolkit' ) . '
' : '';
}
return $this->render_form( $form );
}
/**
* Signed load time: "time.signature". Bots that skip it, change it, or
* post faster than a person can type are caught in process().
*/
public static function token( $form_id, $time = null ) {
$time = null === $time ? time() : (int) $time;
return $time . '.' . substr( hash_hmac( 'sha256', $time . '|' . (int) $form_id, wp_salt( 'nonce' ) ), 0, 20 );
}
/** @return string ok | bad | fast | old */
public static function check_token( $form_id, $token ) {
$parts = explode( '.', (string) $token );
if ( 2 !== count( $parts ) || ! ctype_digit( $parts[0] ) || ! hash_equals( self::token( $form_id, (int) $parts[0] ), (string) $token ) ) {
return 'bad';
}
$age = time() - (int) $parts[0];
if ( $age < self::MIN_SECONDS ) {
return 'fast';
}
return $age > self::MAX_AGE ? 'old' : 'ok';
}
/** What the page's script puts in the JS-check field (the signature, reversed). */
public static function js_answer( $token ) {
$parts = explode( '.', (string) $token );
return isset( $parts[1] ) ? strrev( $parts[1] ) : '';
}
/**
* Output one form.
*
* @param array $form Form (id, name, fields, settings).
* @param array $args preview (bool): no submit, nothing sent; as (guest|user|''): show the form as this visitor would see it.
*/
public function render_form( array $form, array $args = array() ) {
$args = wp_parse_args( $args, array( 'preview' => false, 'as' => '' ) );
$preview = (bool) $args['preview'];
$this->enqueue();
$s = $form['settings'];
$logged_in = '' === $args['as'] ? is_user_logged_in() : 'user' === $args['as'];
if ( ! $preview ) {
$instead = apply_filters( 'bylt_form_before_render', null, $form );
if ( is_string( $instead ) ) {
return $instead;
}
// phpcs:ignore WordPress.Security.NonceVerification -- display only.
if ( isset( $_GET['bylt_sent'] ) && (int) $_GET['bylt_sent'] === $form['id'] ) {
$msg = get_transient( 'bylt_form_msg_' . md5( self::client_key( $form['id'] ) ) );
return '' . esc_html( $msg ? $msg : $s['success'] ) . '
';
}
}
$result = ! $preview && isset( $this->results[ $form['id'] ] ) ? $this->results[ $form['id'] ] : null;
$errors = $result ? $result['errors'] : array();
$posted = $result ? $result['values'] : null;
// phpcs:ignore WordPress.Security.NonceVerification -- prefill only.
$product_id = isset( $_GET['quote_product_id'] ) ? absint( $_GET['quote_product_id'] ) : 0;
$classes = trim( 'bylt-form bylt-form-' . $s['kind'] . ' ' . $s['css_class'] );
$has_file = false;
foreach ( $form['fields'] as $f ) {
$has_file = $has_file || ( 'file' === $f['type'] && Bylt_Forms_Store::field_live( Bylt_Forms_Store::with_defaults( $f ), $logged_in ) );
}
ob_start();
echo '';
return ob_get_clean();
}
/**
* Mark a field's outer element for forms.js: its key, its conditions, and
* hidden (with its inputs off, so they aren't required or sent) when the
* conditions don't match the starting values.
*/
private static function with_cond( $html, array $f, $shown ) {
$attrs = ' data-key="' . esc_attr( $f['key'] ) . '"';
if ( ! empty( $f['cond']['rules'] ) ) {
$attrs .= ' data-cond="' . esc_attr( (string) wp_json_encode( $f['cond'] ) ) . '"';
if ( ! $shown ) {
$attrs .= ' hidden';
$html = preg_replace( '/<(input|select|textarea)\b/', '<$1 disabled', $html );
}
}
return (string) preg_replace( '/^<(div|input)\b/', '<$1' . $attrs, (string) $html, 1 );
}
private function field( $form_id, array $f, $value, $error ) {
$id = 'bylt-f-' . $form_id . '-' . $f['key'];
$name = 'bylt_f[' . $f['key'] . ']';
$req = $f['required'] ? ' required' : '';
$star = $f['required'] ? ' *' : '';
$ph = $f['placeholder'] ? ' placeholder="' . esc_attr( $f['placeholder'] ) . '"' : '';
$desc = $f['help'] && 'html' !== $f['type'] ? '' . esc_html( $f['help'] ) . '' : '';
$aria = ( $f['help'] ? ' aria-describedby="' . esc_attr( $id ) . '-help"' : '' ) . ( $error ? ' aria-invalid="true"' : '' );
$value = is_array( $value ) ? $value : (string) $value;
$class = trim( 'bylt-field bylt-field-' . $f['type'] . ' bylt-w-' . $f['width'] . ( $error ? ' bylt-has-error' : '' ) . ' ' . $f['css'] );
$err = '' . esc_html( $error ) . '';
switch ( $f['type'] ) {
case 'html':
return '' . ( $f['label'] ? '
' . esc_html( $f['label'] ) . '
' : '' ) . wp_kses_post( wpautop( $f['help'] ) ) . '';
case 'hidden':
return '';
case 'file':
if ( ! $f['help'] ) {
$desc = '' . esc_html( Bylt_Forms_Files::hint() ) . '';
$aria = ' aria-describedby="' . esc_attr( $id ) . '-help"' . ( $error ? ' aria-invalid="true"' : '' );
}
$input = '';
break;
case 'password':
$input = '';
break;
case 'textarea':
$input = '';
break;
case 'select':
$input = '';
break;
case 'radio':
case 'checkboxes':
$multi = 'checkboxes' === $f['type'];
$input = '';
return '' . $input . $desc . $err . '
';
case 'consent':
return '' . $desc . $err . '
';
default:
$type = in_array( $f['type'], array( 'email', 'tel', 'number', 'date', 'url' ), true ) ? $f['type'] : 'text';
$auto = array( 'email' => 'email', 'tel' => 'tel', 'url' => 'url' );
$input = '';
}
return '' . $input . $desc . $err . '
';
}
public function quote_button() {
global $product;
if ( ! $product || ! is_object( $product ) ) {
return;
}
foreach ( Bylt_Forms_Store::all() as $form ) {
$s = $form['settings'];
if ( 'quote' !== $s['kind'] || 'no' === $s['product_button'] || ! $s['page_id'] || 'publish' !== get_post_status( $s['page_id'] ) ) {
continue;
}
if ( 'cats' === $s['product_button'] && ! array_intersect( $s['button_cats'], wc_get_product_term_ids( $product->get_id(), 'product_cat' ) ) ) {
continue;
}
$url = add_query_arg(
array(
'quote_product' => rawurlencode( $product->get_name() ),
'quote_product_id' => $product->get_id(),
),
get_permalink( $s['page_id'] )
);
echo '' . esc_html( $s['button_label'] ) . '
';
return;
}
}
/* ---------------------------------------------------------------------
* Preview (Forms → editor → Preview): the editor posts its unsaved
* fields here, into an iframe, so the form shows with the theme's styles.
* ------------------------------------------------------------------- */
public function preview() {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput -- cleaned by the store below.
if ( empty( $_POST['bylt_pv'] ) || ! is_array( $_POST['bylt_pv'] ) ) {
return;
}
$nonce = isset( $_POST['bylt_pv_nonce'] ) ? sanitize_key( wp_unslash( $_POST['bylt_pv_nonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'bylt_form_preview' ) || ! class_exists( 'Bylt_Admin' ) || ! current_user_can( Bylt_Admin::cap() ) ) {
wp_die( esc_html__( 'The preview expired. Reload the editor and try again.', 'byltcommerce-toolkit' ), '', array( 'response' => 403 ) );
}
$pv = wp_unslash( $_POST['bylt_pv'] );
$as = isset( $_POST['bylt_pv_as'] ) && 'user' === $_POST['bylt_pv_as'] ? 'user' : 'guest';
// phpcs:enable
$settings = Bylt_Forms_Store::clean_settings( isset( $pv['settings'] ) ? (array) $pv['settings'] : array() );
$form = array(
'id' => isset( $pv['id'] ) ? absint( $pv['id'] ) : 0,
'name' => isset( $pv['name'] ) ? sanitize_text_field( $pv['name'] ) : '',
'fields' => Bylt_Forms_Store::clean_fields( Bylt_Forms_Admin::fields_from_post( isset( $pv['fields'] ) ? (array) $pv['fields'] : array() ), $settings['kind'] ),
'settings' => $settings,
);
show_admin_bar( false );
nocache_headers();
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'X-Robots-Tag: noindex' );
$html = $this->render_form( $form, array( 'preview' => true, 'as' => $as ) );
?>
>
>
process( $id, wp_unslash( $_POST ), self::posted_files() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- sanitized per field.
// phpcs:enable
if ( $result['ok'] ) {
$to = $result['redirect'] ? $result['redirect'] : add_query_arg( 'bylt_sent', $id, self::current_url() );
wp_safe_redirect( $to, 303 );
exit;
}
$this->results[ $id ] = $result;
}
public function ajax() {
// phpcs:disable WordPress.Security.NonceVerification -- verified in process(); see handle_post().
$id = isset( $_POST['bylt_form_id'] ) ? absint( $_POST['bylt_form_id'] ) : 0;
$result = $this->process( $id, wp_unslash( $_POST ), self::posted_files() ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
// phpcs:enable
unset( $result['values'] );
wp_send_json( $result );
}
private static function ip() {
return Bylt_Forms::client_ip();
}
/** Uploads for bylt_f[...] as key => array( name, tmp_name, size, error ). */
private static function posted_files() {
// phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput -- checked per file in Bylt_Forms_Files::check().
$f = isset( $_FILES['bylt_f'] ) && is_array( $_FILES['bylt_f'] ) ? $_FILES['bylt_f'] : array();
$out = array();
if ( empty( $f['name'] ) || ! is_array( $f['name'] ) ) {
return $out;
}
foreach ( array_keys( $f['name'] ) as $k ) {
foreach ( array( 'name', 'tmp_name', 'size', 'error' ) as $p ) {
$out[ $k ][ $p ] = isset( $f[ $p ][ $k ] ) && ! is_array( $f[ $p ][ $k ] ) ? $f[ $p ][ $k ] : '';
}
}
return $out;
}
/** Visitor + form, for the rate limit and the thank-you message. */
private static function client_key( $form_id ) {
return (int) $form_id . '|' . self::ip();
}
/** Only staff skip the rate limit (they test forms a lot). */
private static function staff() {
return class_exists( 'Bylt_Admin' ) && current_user_can( Bylt_Admin::cap() );
}
/** Links in the visitor's text (Website fields don't count). */
public static function count_links( array $form, array $data ) {
$n = 0;
foreach ( $form['fields'] as $f ) {
if ( 'url' === $f['type'] || ! isset( $data[ $f['key'] ] ) ) {
continue;
}
$n += preg_match_all( '#https?://|www\.|\[url| false, 'errors' => array(), 'message' => '', 'redirect' => '', 'values' => array(), 'entry_id' => 0 );
if ( ! $form ) {
$out['errors']['_form'] = __( 'This form is no longer available.', 'byltcommerce-toolkit' );
return $out;
}
$s = $form['settings'];
$fake = $out; // What a bot gets: looks like it worked.
$fake['ok'] = true;
$fake['message'] = $s['success'];
// Logged-in visitors: a nonce, so another site can't submit (and update their account) on their behalf.
$nonce = isset( $post['bylt_form_nonce'] ) ? sanitize_key( $post['bylt_form_nonce'] ) : '';
if ( is_user_logged_in() && ! wp_verify_nonce( $nonce, 'bylt_form_' . (int) $form['id'] ) ) {
$out['errors']['_form'] = __( 'This page has expired. Please reload it and send the form again.', 'byltcommerce-toolkit' );
return $out;
}
// Bots: filled the hidden field, skipped or forged the signed load time, or posted faster than a person could.
$token = isset( $post['bylt_form_t'] ) ? (string) $post['bylt_form_t'] : '';
$check = self::check_token( $form['id'], $token );
if ( ! empty( $post[ self::HONEYPOT ] ) || 'bad' === $check || 'fast' === $check ) {
do_action( 'bylt_form_spam', $form, $post, ! empty( $post[ self::HONEYPOT ] ) ? 'honeypot' : $check );
return $fake;
}
if ( 'old' === $check ) {
$out['errors']['_form'] = __( 'This page was open for a long time. Please press the button again to send.', 'byltcommerce-toolkit' );
$out['token'] = self::token( $form['id'] );
return $out;
}
if ( 'yes' === $s['spam_js'] && ( ! isset( $post['bylt_form_js'] ) || ! hash_equals( self::js_answer( $token ), (string) $post['bylt_form_js'] ) ) ) {
do_action( 'bylt_form_spam', $form, $post, 'no_js' );
$out['errors']['_form'] = __( 'Your browser didn\'t finish loading this form. Please make sure JavaScript is on, reload the page and try again.', 'byltcommerce-toolkit' );
return $out;
}
$raw = isset( $post['bylt_f'] ) && is_array( $post['bylt_f'] ) ? $post['bylt_f'] : array();
$data = array();
$secret = array(); // Passwords: handed to bylt_form_handle, never saved or emailed.
$errors = array();
$uploads = array(); // key => checked file, stored once everything else is valid.
// Conditional logic: fields the visitor couldn't see aren't required, checked or saved.
$seen = array();
foreach ( $form['fields'] as $f ) {
if ( 'file' === $f['type'] ) {
$seen[ $f['key'] ] = isset( $files[ $f['key'] ]['name'] ) && is_string( $files[ $f['key'] ]['name'] ) ? sanitize_file_name( $files[ $f['key'] ]['name'] ) : '';
} elseif ( isset( $raw[ $f['key'] ] ) ) {
$seen[ $f['key'] ] = is_array( $raw[ $f['key'] ] ) ? array_map( 'sanitize_text_field', array_filter( $raw[ $f['key'] ], 'is_scalar' ) ) : sanitize_text_field( (string) $raw[ $f['key'] ] );
}
}
$shown = Bylt_Forms_Store::visible( $form['fields'], $seen );
foreach ( $form['fields'] as $f ) {
if ( 'html' === $f['type'] || empty( $shown[ $f['key'] ] ) ) {
continue;
}
if ( 'file' === $f['type'] ) {
$chk = Bylt_Forms_Files::check( isset( $files[ $f['key'] ] ) ? (array) $files[ $f['key'] ] : array() );
if ( is_wp_error( $chk ) ) {
$errors[ $f['key'] ] = $chk->get_error_message();
} elseif ( null === $chk && $f['required'] ) {
/* translators: %s: field label */
$errors[ $f['key'] ] = sprintf( __( '%s is required.', 'byltcommerce-toolkit' ), $f['label'] );
} elseif ( $chk ) {
$uploads[ $f['key'] ] = $chk;
$data[ $f['key'] ] = $chk['name'];
} else {
$data[ $f['key'] ] = '';
}
continue;
}
$k = $f['key'];
$val = isset( $raw[ $k ] ) ? $raw[ $k ] : ( 'checkboxes' === $f['type'] ? array() : '' );
list( $val, $err ) = self::validate( $f, $val );
if ( 'password' === $f['type'] ) {
$secret[ $k ] = $val;
} else {
$data[ $k ] = $val;
}
if ( $err ) {
$errors[ $k ] = $err;
}
}
$out['values'] = $data;
if ( ! $errors ) {
$fp = $this->forms->protection( $form );
if ( $fp && ! $fp->passes( 'bylt_form' ) ) {
$errors['_form'] = __( 'Please confirm you\'re human and try again.', 'byltcommerce-toolkit' );
}
}
$errors = apply_filters( 'bylt_form_errors', $errors, $form, $data );
// Same visitor, too many entries in a few minutes.
$rate_key = 'bylt_form_rate_' . md5( self::client_key( $form['id'] ) );
$rate = get_transient( $rate_key );
$rate = is_array( $rate ) && isset( $rate['n'], $rate['t'] ) && $rate['t'] > time() - self::WINDOW ? $rate : array( 'n' => 0, 't' => time() );
if ( ! $errors && $s['spam_rate'] > 0 && $rate['n'] >= $s['spam_rate'] && ! self::staff() ) {
do_action( 'bylt_form_spam', $form, $post, 'rate' );
$errors['_form'] = __( 'You\'ve sent this form several times in the last few minutes. Please wait a little and try again.', 'byltcommerce-toolkit' );
}
if ( $errors ) {
if ( ! isset( $errors['_form'] ) ) {
$errors['_form'] = __( 'Please check the highlighted fields.', 'byltcommerce-toolkit' );
}
$out['errors'] = $errors;
return $out;
}
// The same entry again (double click, back button, bot replay): don't save or email it twice.
$dup_key = 'bylt_form_dup_' . md5( $form['id'] . '|' . wp_json_encode( $data ) );
if ( get_transient( $dup_key ) ) {
return $fake;
}
$email = self::first_email( $form, $data );
$product_id = isset( $post['bylt_product_id'] ) ? absint( $post['bylt_product_id'] ) : 0;
$page = isset( $post['bylt_form_page'] ) ? esc_url_raw( $post['bylt_form_page'] ) : '';
$page = $page && wp_validate_redirect( $page, '' ) ? $page : '';
$is_spam = $s['spam_links'] > 0 && self::count_links( $form, $data ) > $s['spam_links'];
// Files: stored only now that the entry is valid; spam keeps the names but not the files.
$stored = array();
if ( ! $is_spam ) {
foreach ( $uploads as $k => $info ) {
$rel = Bylt_Forms_Files::store( $info, 'forms' );
if ( is_wp_error( $rel ) ) {
foreach ( $stored as $r ) {
Bylt_Forms_Files::delete( $r );
}
$out['errors'] = array( $k => $rel->get_error_message(), '_form' => __( 'Please check the highlighted fields.', 'byltcommerce-toolkit' ) );
return $out;
}
$stored[ $k ] = $rel;
}
if ( $stored ) {
$data['_files'] = $stored;
}
}
$user_id = get_current_user_id();
$message = $s['success'];
if ( ! $is_spam ) {
$handled = apply_filters( 'bylt_form_handle', null, $form, $data, $secret );
if ( is_wp_error( $handled ) ) {
foreach ( $handled->get_error_codes() as $code ) {
$k = in_array( $code, wp_list_pluck( $form['fields'], 'key' ), true ) ? $code : '_form';
$errors[ $k ] = $handled->get_error_message( $code );
}
if ( ! isset( $errors['_form'] ) ) {
$errors['_form'] = __( 'Please check the highlighted fields.', 'byltcommerce-toolkit' );
}
foreach ( $stored as $r ) {
Bylt_Forms_Files::delete( $r );
}
$out['errors'] = $errors;
return $out;
}
if ( is_array( $handled ) ) {
$user_id = ! empty( $handled['user_id'] ) ? (int) $handled['user_id'] : $user_id;
$message = ! empty( $handled['message'] ) ? (string) $handled['message'] : $message;
}
if ( $user_id ) {
self::save_to_customer( $user_id, $form, $data );
}
}
$entry_id = 0;
if ( 'yes' === $s['store'] || $is_spam ) {
$entry_id = Bylt_Forms_Store::add_entry(
array(
'form_id' => $form['id'],
'status' => $is_spam ? 'spam' : 'new',
'email' => $email,
'data' => $data,
'user_id' => $user_id,
'product_id' => $product_id,
'page_url' => $page,
'ip' => self::ip(),
)
);
}
set_transient( $dup_key, 1, self::WINDOW );
if ( $is_spam ) {
do_action( 'bylt_form_spam', $form, $post, 'links' );
return $fake;
}
set_transient( $rate_key, array( 'n' => $rate['n'] + 1, 't' => $rate['t'] ), self::WINDOW );
$this->forms->mailer()->notify( $form, $data, $entry_id, $email, $product_id, $page );
do_action( 'bylt_form_submitted', $entry_id, $form, $data );
if ( $message !== $s['success'] ) {
set_transient( 'bylt_form_msg_' . md5( self::client_key( $form['id'] ) ), $message, 60 );
}
$out['ok'] = true;
$out['entry_id'] = $entry_id;
$out['message'] = $message;
$out['redirect'] = $s['redirect'] ? wp_validate_redirect( $s['redirect'], '' ) : '';
return $out;
}
/** Copy fields marked "Save to customer" onto the customer's account. */
public static function save_to_customer( $user_id, array $form, array $data ) {
$targets = Bylt_Forms::map_targets();
foreach ( $form['fields'] as $f ) {
if ( empty( $f['map'] ) || ! isset( $targets[ $f['map'] ] ) || ! isset( $data[ $f['key'] ] ) || '' === Bylt_Forms_Mailer::text( $data[ $f['key'] ] ) ) {
continue;
}
// File targets, and targets a module saves itself (e.g. wholesale certificates).
if ( 0 === strpos( $f['map'], 'file:' ) || apply_filters( 'bylt_form_map_handled', false, $f['map'], $user_id, $data[ $f['key'] ], $f ) ) {
continue;
}
$v = Bylt_Forms_Mailer::text( $data[ $f['key'] ] );
if ( 'billing_country' === $f['map'] ) {
$v = strtoupper( substr( preg_replace( '/[^A-Za-z]/', '', $v ), 0, 2 ) );
}
update_user_meta( $user_id, $f['map'], $v );
}
}
public static function first_email( array $form, array $data ) {
foreach ( $form['fields'] as $f ) {
if ( 'email' === $f['type'] && ! empty( $data[ $f['key'] ] ) ) {
return $data[ $f['key'] ];
}
}
return '';
}
/**
* @return array{0: mixed, 1: string} Cleaned value, and an error message or ''.
*/
public static function validate( array $f, $val ) {
$label = $f['label'];
if ( 'checkboxes' === $f['type'] ) {
$val = array_values( array_intersect( array_map( 'sanitize_text_field', (array) $val ), $f['options'] ) );
return array( $val, $f['required'] && ! $val ? __( 'Please choose at least one.', 'byltcommerce-toolkit' ) : '' );
}
if ( 'password' === $f['type'] ) {
$val = is_string( $val ) ? substr( $val, 0, 200 ) : '';
if ( '' === $val ) {
/* translators: %s: field label */
return array( '', $f['required'] ? sprintf( __( '%s is required.', 'byltcommerce-toolkit' ), $label ) : '' );
}
if ( strlen( $val ) < self::MIN_PASSWORD ) {
/* translators: %d: number of characters */
return array( '', sprintf( __( 'Please use at least %d characters.', 'byltcommerce-toolkit' ), self::MIN_PASSWORD ) );
}
$rule = (string) apply_filters( 'bylt_password_error', '', $val ); // Site Tweaks → Password rules.
return '' !== $rule ? array( '', $rule ) : array( $val, '' );
}
if ( is_array( $val ) ) {
$val = implode( ', ', $val );
}
$val = 'textarea' === $f['type'] ? sanitize_textarea_field( $val ) : sanitize_text_field( $val );
$val = mb_substr( $val, 0, 'textarea' === $f['type'] ? 10000 : 500 );
if ( '' === $val ) {
/* translators: %s: field label */
return array( '', $f['required'] ? ( 'consent' === $f['type'] ? __( 'Please tick this box to continue.', 'byltcommerce-toolkit' ) : sprintf( __( '%s is required.', 'byltcommerce-toolkit' ), $label ) ) : '' );
}
switch ( $f['type'] ) {
case 'email':
return is_email( $val ) ? array( sanitize_email( $val ), '' ) : array( $val, __( 'Please enter a valid email address.', 'byltcommerce-toolkit' ) );
case 'url':
$u = esc_url_raw( preg_match( '#^https?://#i', $val ) ? $val : 'https://' . $val );
return $u ? array( $u, '' ) : array( $val, __( 'Please enter a valid web address.', 'byltcommerce-toolkit' ) );
case 'number':
return is_numeric( $val ) ? array( $val, '' ) : array( $val, __( 'Please enter a number.', 'byltcommerce-toolkit' ) );
case 'date':
return preg_match( '/^\d{4}-\d{2}-\d{2}$/', $val ) && strtotime( $val ) ? array( $val, '' ) : array( $val, __( 'Please enter a date.', 'byltcommerce-toolkit' ) );
case 'tel':
return preg_match( '/^[0-9 +().\-extEXT#*]{5,40}$/', $val ) ? array( $val, '' ) : array( $val, __( 'Please enter a valid phone number.', 'byltcommerce-toolkit' ) );
case 'select':
case 'radio':
return in_array( $val, $f['options'], true ) ? array( $val, '' ) : array( '', __( 'Please choose an option.', 'byltcommerce-toolkit' ) );
case 'consent':
return array( 'yes', '' );
}
return array( $val, '' );
}
}