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 '
'; if ( ! $preview ) { echo ''; echo ''; if ( is_user_logged_in() ) { // Logged-in visitors get a nonce too: their submission can update their account. wp_nonce_field( 'bylt_form_' . (int) $form['id'], 'bylt_form_nonce' ); } echo ''; echo ''; if ( $product_id ) { echo ''; } echo ''; } echo ''; $initial = array(); foreach ( $form['fields'] as $f ) { $f = Bylt_Forms_Store::with_defaults( $f ); $initial[ $f['key'] ] = in_array( $f['type'], array( 'password', 'file' ), true ) ? '' : self::initial( $f, $posted ); } $shown = Bylt_Forms_Store::visible( $form['fields'], $initial, $logged_in ); echo '
'; foreach ( $form['fields'] as $f ) { $f = Bylt_Forms_Store::with_defaults( $f ); if ( ! Bylt_Forms_Store::field_live( $f, $logged_in ) ) { continue; } $html = $this->field( $form['id'], $f, $initial[ $f['key'] ], isset( $errors[ $f['key'] ] ) ? $errors[ $f['key'] ] : '' ); echo self::with_cond( $html, $f, ! empty( $shown[ $f['key'] ] ) ); // phpcs:ignore WordPress.Security.EscapeOutput -- built from escaped parts. } echo '
'; $fp = $preview ? null : $this->forms->protection( $form ); if ( $fp ) { $fp->widget( 'bylt_form' ); } echo '

'; 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 = '
' . esc_html( $f['label'] ) . $star . ''; foreach ( $f['options'] as $i => $o ) { $on = $multi ? in_array( $o, (array) $value, true ) : $value === $o; $input .= ''; } $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, '' ); } }