wp_create_nonce( self::NONCE ) ) ); } public static function ajax_submit() { $r = self::process( self::submission() ); $payload = array( 'message' => $r['message'], 'errors' => $r['errors'] ); if ( $r['ok'] ) { wp_send_json_success( $payload ); } wp_send_json_error( $payload ); } public static function post_submit() { $post = self::submission(); $r = self::process( $post ); $back = self::referer(); $back = '' !== $back ? wp_validate_redirect( $back, home_url( '/' ) ) : home_url( '/' ); $url = add_query_arg( array( 'aieo_form' => $r['ok'] ? 'sent' : ( '' !== $r['code'] ? $r['code'] : 'error' ), 'aieo_form_for' => isset( $post['aieo_form_id'] ) ? absint( $post['aieo_form_id'] ) : 0, ), remove_query_arg( array( 'aieo_form', 'aieo_form_for' ), $back ) ); wp_safe_redirect( $url ); exit; } private static function submission() { if ( ! isset( $_POST['aieo_form_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['aieo_form_nonce'] ) ), self::NONCE ) ) { $keep = array(); foreach ( array( 'aieo_form_id', 'aieo_form_website', 'aieo_form_ts', 'elapsed' ) as $key ) { if ( isset( $_POST[ $key ] ) && is_scalar( $_POST[ $key ] ) ) { $keep[ $key ] = sanitize_text_field( wp_unslash( (string) $_POST[ $key ] ) ); } } return $keep; } return (array) map_deep( wp_unslash( $_POST ), 'sanitize_textarea_field' ); } public static function process( array $post ) { $out = array( 'ok' => false, 'message' => '', 'errors' => array(), 'code' => '', 'id' => 0 ); $form_id = isset( $post['aieo_form_id'] ) ? absint( $post['aieo_form_id'] ) : 0; $form = $form_id ? get_post( $form_id ) : null; if ( ! $form || ! class_exists( 'AIEO_Form_CPT' ) || AIEO_Form_CPT::POST_TYPE !== $form->post_type || 'publish' !== $form->post_status ) { return self::refuse( $out, 'unavailable' ); } $settings = AIEO_Form_CPT::words( $form_id ); if ( self::is_bot() ) { return self::refuse( $out, 'refused', $settings ); } $silent = array( 'ok' => true, 'message' => (string) $settings['success'], 'errors' => array(), 'code' => '', 'id' => 0 ); if ( ! empty( $post['aieo_form_website'] ) || self::elapsed( $post ) < self::MIN_SECONDS ) { return $silent; } $nonce = isset( $post['aieo_form_nonce'] ) && is_scalar( $post['aieo_form_nonce'] ) ? sanitize_text_field( (string) $post['aieo_form_nonce'] ) : ''; if ( ! wp_verify_nonce( $nonce, self::NONCE ) ) { return self::refuse( $out, 'expired', $settings ); } if ( ! self::rate_ok( 'ip', self::IP_LIMIT, self::IP_WINDOW ) ) { return self::refuse( $out, 'rate', $settings ); } $captcha = self::captcha_check(); if ( is_wp_error( $captcha ) ) { $out['message'] = $captcha->get_error_message(); $out['code'] = 'captcha'; return $out; } $fields = AIEO_Form_CPT::fields( $form_id ); $in = isset( $post['aieo_form'] ) && is_array( $post['aieo_form'] ) ? $post['aieo_form'] : array(); $v = self::validate( $fields, $in ); if ( $v['errors'] ) { $out['errors'] = $v['errors']; $out['message'] = (string) $settings['check']; $out['code'] = 'invalid'; return $out; } $email = self::first_email( $fields, $v['values'] ); if ( '' !== $email && ! self::rate_ok( 'email', self::EMAIL_LIMIT, self::EMAIL_WINDOW, strtolower( $email ) ) ) { return self::refuse( $out, 'rate', $settings ); } $once = isset( $post['aieo_form_once'] ) ? preg_replace( '/[^A-Za-z0-9-]/', '', (string) $post['aieo_form_once'] ) : ''; $seen = '' !== $once ? 'aieo_form_once_' . md5( $form_id . '|' . $once ) : ''; if ( '' !== $seen && get_transient( $seen ) ) { return $silent; } $id = AIEO_Form_Entries::record( $form_id, $v['values'], array( 'source' => 'site', 'ip' => self::client_ip(), 'page' => self::referer(), 'seen_before' => isset( $post['aieo_form_seen'] ) && is_scalar( $post['aieo_form_seen'] ) ? substr( (string) $post['aieo_form_seen'], 0, 4096 ) : '', 'location' => AIEO_Form_Render::location( array( 'location' => isset( $post['aieo_form_location'] ) && is_scalar( $post['aieo_form_location'] ) ? (string) $post['aieo_form_location'] : '' ) ), ) ); if ( is_wp_error( $id ) ) { return self::refuse( $out, 'error', $settings ); } if ( '' !== $seen ) { set_transient( $seen, (int) $id, self::ONCE_WINDOW ); } return array( 'ok' => true, 'message' => (string) $settings['success'], 'errors' => array(), 'code' => '', 'id' => (int) $id ); } public static function validate( array $fields, array $in ) { $values = array(); $errors = array(); foreach ( $fields as $f ) { $key = (string) $f['key']; $type = (string) $f['type']; $raw = $in[ $key ] ?? null; $missing = '' !== (string) $f['missing'] ? (string) $f['missing'] : __( 'This field is required.', 'ai-eshop-optimizer' ); $invalid = (string) $f['invalid']; $empty = false; if ( 'checkbox' === $type ) { $picked = array(); foreach ( (array) $raw as $one ) { $ck = sanitize_key( is_scalar( $one ) ? (string) $one : '' ); if ( isset( $f['choices'][ $ck ] ) ) { $picked[] = $ck; } } $values[ $key ] = array_values( array_unique( $picked ) ); $empty = ! $picked; } elseif ( 'radio' === $type || 'select' === $type ) { $ck = sanitize_key( is_scalar( $raw ) ? (string) $raw : '' ); if ( '' !== $ck && ! isset( $f['choices'][ $ck ] ) ) { $errors[ $key ] = '' !== $invalid ? $invalid : __( 'Please choose one of the options.', 'ai-eshop-optimizer' ); continue; } $values[ $key ] = $ck; $empty = '' === $ck; } elseif ( 'consent' === $type ) { $values[ $key ] = ! empty( $raw ) && is_scalar( $raw ) ? '1' : ''; $empty = '' === $values[ $key ]; } else { $text = is_scalar( $raw ) ? (string) $raw : ''; $text = 'textarea' === $type ? sanitize_textarea_field( $text ) : sanitize_text_field( $text ); $text = trim( mb_substr( $text, 0, AIEO_Form_CPT::max_length( $type ) ) ); if ( '' !== $text && 'email' === $type ) { $text = sanitize_email( $text ); if ( ! is_email( $text ) ) { $errors[ $key ] = '' !== $invalid ? $invalid : __( 'Please enter a valid email address.', 'ai-eshop-optimizer' ); continue; } } elseif ( '' !== $text && 'url' === $type ) { $url = esc_url_raw( $text, array( 'http', 'https' ) ); $host = (string) wp_parse_url( $url, PHP_URL_HOST ); if ( '' === $url || false === filter_var( $url, FILTER_VALIDATE_URL ) || ! str_contains( $host, '.' ) ) { $errors[ $key ] = '' !== $invalid ? $invalid : __( 'Please enter a web address, like https://example.com.', 'ai-eshop-optimizer' ); continue; } $text = $url; } elseif ( '' !== $text && 'tel' === $type && ( strlen( (string) preg_replace( '/\D+/', '', $text ) ) < 6 || preg_match( '/[^0-9+()\-.\/\s]/', $text ) ) ) { $errors[ $key ] = '' !== $invalid ? $invalid : __( 'Please enter a phone number we can reach you on.', 'ai-eshop-optimizer' ); continue; } $values[ $key ] = $text; $empty = '' === $text; } if ( $empty && ! empty( $f['required'] ) ) { $errors[ $key ] = $missing; } } return array( 'values' => $values, 'errors' => $errors ); } public static function first_email( array $fields, array $values ) { foreach ( $fields as $f ) { if ( 'email' === $f['type'] && ! empty( $values[ $f['key'] ] ) && is_email( (string) $values[ $f['key'] ] ) ) { return (string) $values[ $f['key'] ]; } } return ''; } public static function error_message( $code, array $settings = array() ) { switch ( (string) $code ) { case 'invalid': return '' !== (string) ( $settings['check'] ?? '' ) ? (string) $settings['check'] : __( 'Please correct the highlighted fields.', 'ai-eshop-optimizer' ); case 'rate': return __( 'Too many attempts. Please try again in a few minutes.', 'ai-eshop-optimizer' ); case 'captcha': return __( 'We could not verify that you are human. Please enable JavaScript, reload the page and try again.', 'ai-eshop-optimizer' ); case 'unavailable': return __( 'Requests are not available right now.', 'ai-eshop-optimizer' ); case 'refused': return __( 'Request refused.', 'ai-eshop-optimizer' ); case 'expired': return __( 'This page has expired. Please reload it and try again.', 'ai-eshop-optimizer' ); default: return __( 'Something went wrong. Please try again.', 'ai-eshop-optimizer' ); } } public static function mails_staff( $form_id ) { return 'email' === (string) get_post_meta( (int) $form_id, AIEO_Form_CPT::META_NOTIFY, true ); } public static function thanks_by_mail( $form_id ) { $settings = AIEO_Form_CPT::settings( (int) $form_id ); return '' !== trim( (string) $settings['ack_subject'] ) && '' !== trim( (string) $settings['ack_body'] ); } public static function queue_notification( $entry_id, $form_id ) { $entry_id = (int) $entry_id; if ( $entry_id <= 0 || ( ! self::mails_staff( $form_id ) && ! self::thanks_by_mail( $form_id ) ) ) { return ''; } $page = self::referer(); if ( function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( self::NOTIFY_HOOK, array( 'entry_id' => $entry_id, 'page' => $page ), 'aieo' ); return 'as_async'; } if ( false !== wp_schedule_single_event( time() + 1, self::NOTIFY_HOOK, array( $entry_id, $page ) ) ) { return 'cron'; } add_action( 'shutdown', static function () use ( $entry_id, $page ) { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } self::run_notification( $entry_id, $page ); }, 99 ); return 'shutdown'; } public static function run_notification( $entry_id = 0, $page = '' ) { $mail = self::compose( (int) $entry_id, (string) $page ); if ( ! $mail ) { return array( 'notified' => array(), 'listed' => array(), 'ack' => '' ); } $failed = array(); $to = $mail['to']; $listed = array(); if ( $to && class_exists( 'AIEO_Staff_Digest' ) ) { $split = AIEO_Staff_Digest::split( $to ); $missed = $split['later'] ? AIEO_Staff_Digest::queue( $split['later'], array( 'source' => 'form', 'source_id' => (int) $entry_id, 'group' => html_entity_decode( get_the_title( $mail['form_id'] ), ENT_QUOTES, 'UTF-8' ), 'title' => $mail['who'], 'summary' => '', 'link' => self::entry_link( (int) $entry_id, $mail['form_id'] ), ) ) : array(); $listed = array_values( array_diff( $split['later'], $missed ) ); $to = array_merge( $split['now'], $missed ); } if ( $to ) { $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); if ( '' !== $mail['reply_to'] ) { $headers[] = 'Reply-To: ' . $mail['reply_to']; } if ( count( $to ) > 1 ) { $headers[] = 'Bcc: ' . implode( ', ', array_slice( $to, 1 ) ); } if ( ! wp_mail( $to[0], $mail['subject'], $mail['body'], $headers ) ) { $failed[] = 'the notification to ' . implode( ', ', $to ); } } if ( '' !== $mail['ack_to'] && ! wp_mail( $mail['ack_to'], $mail['ack_subject'], $mail['ack_body'], array( 'Content-Type: text/plain; charset=UTF-8' ) ) ) { $failed[] = 'the thank-you to the visitor'; } if ( $failed ) { throw new RuntimeException( esc_html( sprintf( 'Form entry %d: wp_mail() refused %s.', (int) $entry_id, implode( ' and ', $failed ) ) ) ); } return array( 'notified' => $to, 'listed' => $listed, 'ack' => $mail['ack_to'] ); } public static function compose( $entry_id, $page = '' ) { global $wpdb; $entry = $entry_id > 0 ? $wpdb->get_row( $wpdb->prepare( 'SELECT id, form_id, created_at FROM %i WHERE id = %d', AIEO_Form_CPT::entries_table(), $entry_id ) ) : null; if ( ! $entry ) { return null; } $form_id = (int) $entry->form_id; $fields = AIEO_Form_CPT::fields( $form_id ); $settings = AIEO_Form_CPT::settings( $form_id ); $answers = array(); foreach ( AIEO_Form_Entries::entry_answers( $entry_id ) as $row ) { if ( 'answered' === (string) $row['disposition'] ) { $answers[ (string) $row['field_key'] ][] = (string) $row['value_text']; } } $values = array(); $lines = array(); foreach ( $fields as $f ) { $given = $answers[ $f['key'] ] ?? array(); $value = implode( ', ', AIEO_Form_CPT::is_choice_type( $f['type'] ) ? array_map( array( __CLASS__, 'plain' ), $given ) : $given ); $value = 'consent' === $f['type'] ? ( '' !== $value ? __( 'Yes', 'ai-eshop-optimizer' ) : __( 'No', 'ai-eshop-optimizer' ) ) : $value; $values[ $f['key'] ] = $value; $label = trim( '' !== trim( (string) $f['label'] ) ? self::plain( (string) $f['label'] ) : self::plain( (string) implode( ' ', $f['choices'] ) ) ); $lines[] = $label . ( preg_match( '/[.?!:;\x{037E}\x{0387}\x{00B7}\x{2026}]$/u', $label ) ? ' ' : ': ' ) . ( '' !== $value ? $value : '-' ); } $title = get_the_title( $form_id ); $fill = static function ( $text ) use ( $values, $title ) { $map = array( '{form}' => $title ); foreach ( $values as $k => $v ) { $map[ '{' . $k . '}' ] = $v; } return trim( strtr( (string) $text, $map ) ); }; $to = self::mails_staff( $form_id ) ? self::staff_addresses( $settings ) : array(); $loc = self::location_of( (int) $entry_id ); if ( $to && $loc && '' !== AIEO_Location_Contact::email( $loc ) && ! in_array( strtolower( AIEO_Location_Contact::email( $loc ) ), array_map( 'strtolower', $to ), true ) ) { $to[] = AIEO_Location_Contact::email( $loc ); } $email = self::first_email( $fields, $values ); $subject = $fill( '' !== (string) $settings['subject'] ? (string) $settings['subject'] : __( 'New entry: {form}', 'ai-eshop-optimizer' ) ); $body = implode( "\n", $lines ) . "\n\n" . ( '' !== $page ? sprintf( __( 'Sent from %s', 'ai-eshop-optimizer' ), $page ) . "\n" : '' ) . sprintf( __( 'Received %s (UTC)', 'ai-eshop-optimizer' ), (string) $entry->created_at ) . "\n" . self::trail_lines( $entry_id, $form_id ); $ack = '' !== $email && '' !== trim( (string) $settings['ack_subject'] ) && '' !== trim( (string) $settings['ack_body'] ); $who = ''; foreach ( $fields as $f ) { if ( false !== stripos( (string) $f['key'], 'name' ) && '' !== $values[ $f['key'] ] ) { $who = $values[ $f['key'] ]; break; } } return array( 'form_id' => $form_id, 'who' => trim( $who . ( '' !== $email ? ' <' . $email . '>' : '' ) ), 'to' => $to, 'subject' => str_replace( array( "\r", "\n" ), ' ', $subject ), 'body' => $body, 'reply_to' => $email, 'ack_to' => $ack ? $email : '', 'ack_subject' => $ack ? str_replace( array( "\r", "\n" ), ' ', $fill( (string) $settings['ack_subject'] ) ) : '', 'ack_body' => $ack ? $fill( (string) $settings['ack_body'] ) . "\n" : '', ); } public static function staff_addresses( array $settings ) { $to = self::split_emails( (string) $settings['notify_to'] ); if ( ! $to ) { $admin = sanitize_email( (string) get_option( 'admin_email' ) ); $to = is_email( $admin ) ? array( $admin ) : array(); } return $to; } public static function campaign( $entry_id ) { $none = array( 'source' => '', 'medium' => '', 'campaign' => '', 'landing' => '', 'referrer' => '', 'key' => '' ); if ( ! class_exists( 'AIEO_Submission_Panel', false ) && defined( 'AIEO_DIR' ) && is_readable( AIEO_DIR . 'includes/dmm/class-aieo-submission-panel.php' ) ) { require_once AIEO_DIR . 'includes/dmm/class-aieo-submission-panel.php'; } $lead = class_exists( 'AIEO_Submission_Panel' ) ? AIEO_Submission_Panel::for_entry( 'aieo_form', (int) $entry_id ) : null; if ( ! $lead ) { return $none; } $row = (array) $lead['row']; $pick = static function ( $canon, $utm ) use ( $row ) { return (string) ( '' !== (string) ( $row[ $canon ] ?? '' ) ? $row[ $canon ] : ( $row[ $utm ] ?? '' ) ); }; return array( 'source' => $pick( 'canon_source', 'utm_source' ), 'medium' => $pick( 'canon_medium', 'utm_medium' ), 'campaign' => $pick( 'canon_campaign', 'utm_campaign' ), 'landing' => (string) ( $row['landing_path'] ?? '' ), 'referrer' => (string) ( $row['referrer_host'] ?? '' ), 'key' => (string) ( $row['source_key'] ?? '' ), ); } public static function entry_link( $entry_id, $form_id ) { $c = self::campaign( (int) $entry_id ); if ( '' !== $c['key'] && class_exists( 'AIEO_Submission_Panel' ) ) { return AIEO_Submission_Panel::url( $c['key'] ); } return add_query_arg( array( 'post' => (int) $form_id, 'action' => 'edit' ), admin_url( 'post.php' ) ) . '#aieo-form-entries'; } public static function trail_lines( $entry_id, $form_id ) { global $wpdb; $lines = array(); $c = self::campaign( (int) $entry_id ); $from = implode( ' / ', array_filter( array( $c['source'], $c['medium'], $c['campaign'] ) ) ); $lines[] = sprintf( __( 'Campaign: %s', 'ai-eshop-optimizer' ), '' !== $from ? $from : __( 'none (a direct visit)', 'ai-eshop-optimizer' ) ); if ( '' !== $c['landing'] ) { $lines[] = sprintf( __( 'Landing page: %s', 'ai-eshop-optimizer' ), $c['landing'] ); } if ( '' !== $c['referrer'] ) { $lines[] = sprintf( __( 'Came from: %s', 'ai-eshop-optimizer' ), $c['referrer'] ); } if ( AIEO_Form_Entries::has_trail() ) { $seen = AIEO_Form_Entries::seen_ids( (string) $wpdb->get_var( $wpdb->prepare( 'SELECT seen_before FROM %i WHERE id = %d', AIEO_Form_CPT::entries_table(), (int) $entry_id ) ) ); $names = array_map( static function ( $pid ) { return html_entity_decode( get_the_title( $pid ), ENT_QUOTES, 'UTF-8' ); }, array_slice( array_reverse( $seen ), 0, 10 ) ); if ( $names ) { $lines[] = sprintf( __( 'Viewed before: %s', 'ai-eshop-optimizer' ), implode( ', ', $names ) ); } } $loc = self::location_of( (int) $entry_id ); if ( $loc ) { $lines[] = sprintf( __( 'Location: %s', 'ai-eshop-optimizer' ), (string) $loc['label'] ); } $lines[] = sprintf( __( 'Open the entry: %s', 'ai-eshop-optimizer' ), self::entry_link( (int) $entry_id, (int) $form_id ) ); return "\n" . implode( "\n", $lines ) . "\n"; } public static function location_of( $entry_id ) { if ( ! AIEO_Form_Entries::has_location() || ! class_exists( 'AIEO_Location_Contact' ) ) { return null; } global $wpdb; $slug = (string) $wpdb->get_var( $wpdb->prepare( 'SELECT location FROM %i WHERE id = %d', AIEO_Form_CPT::entries_table(), (int) $entry_id ) ); $all = AIEO_Location_Contact::all(); return '' !== $slug && isset( $all[ $slug ] ) ? $all[ $slug ] : null; } private static function captcha_gate() { if ( ! class_exists( 'AIEO_Form_Captcha' ) ) { return ''; } $cls = AIEO_Form_Captcha::captcha_class(); if ( '' === $cls ) { return ''; } if ( class_exists( 'AIEO_Captcha_Keys' ) && method_exists( 'AIEO_Captcha_Keys', 'verified_for_enforcement' ) && ! AIEO_Captcha_Keys::verified_for_enforcement() ) { return ''; } return $cls; } public static function captcha_field() { $cls = self::captcha_gate(); if ( '' === $cls || ! method_exists( $cls, 'field' ) ) { return ''; } ob_start(); call_user_func( array( $cls, 'field' ), self::CAPTCHA_SURFACE ); return (string) ob_get_clean(); } public static function captcha_check() { $cls = self::captcha_gate(); if ( '' === $cls || ! method_exists( $cls, 'verify' ) ) { return true; } return call_user_func( array( $cls, 'verify' ), self::CAPTCHA_SURFACE ); } private static function refuse( array $out, $code, array $settings = array() ) { $out['code'] = (string) $code; $out['message'] = self::error_message( $code, $settings ); return $out; } public static function elapsed( array $post ) { if ( isset( $post['elapsed'] ) && '' !== (string) $post['elapsed'] ) { return (int) $post['elapsed']; } $ts = isset( $post['aieo_form_ts'] ) ? (int) $post['aieo_form_ts'] : 0; return $ts > 0 ? time() - $ts : 999; } public static function is_bot() { $ua = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['HTTP_USER_AGENT'] ) ) : ''; if ( '' === $ua ) { return true; } if ( class_exists( 'AIEO_Session_Tracker' ) && method_exists( 'AIEO_Session_Tracker', 'is_bot_user_agent' ) ) { return (bool) AIEO_Session_Tracker::is_bot_user_agent( $ua ); } return (bool) preg_match( '/bot|crawl|spider|slurp|curl|wget|headless|python-requests/i', $ua ); } public static function client_ip() { if ( class_exists( 'AIEO_Product_Request_Store' ) && method_exists( 'AIEO_Product_Request_Store', 'client_ip' ) ) { return (string) AIEO_Product_Request_Store::client_ip(); } return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( (string) $_SERVER['REMOTE_ADDR'] ) ) : ''; } public static function rate_ok( $bucket, $limit, $window, $key = '' ) { $material = '' !== (string) $key ? (string) $key : self::client_ip(); $id = 'aieo_form_' . sanitize_key( $bucket ) . '_' . md5( $material ); $n = (int) get_transient( $id ); if ( $n >= (int) $limit ) { return false; } set_transient( $id, $n + 1, (int) $window ); return true; } public static function referer() { $r = (string) wp_get_referer(); if ( '' === $r && ! empty( $_SERVER['HTTP_REFERER'] ) ) { $r = esc_url_raw( wp_unslash( (string) $_SERVER['HTTP_REFERER'] ) ); } return substr( $r, 0, 512 ); } private static function split_emails( $csv ) { $out = array(); foreach ( (array) preg_split( '/[,;\s]+/', (string) $csv ) as $bit ) { $e = sanitize_email( trim( (string) $bit ) ); if ( is_email( $e ) && ! in_array( $e, $out, true ) ) { $out[] = $e; } } return $out; } private static function plain( $text ) { return class_exists( 'AIEO_Engine_Blocks' ) ? (string) AIEO_Engine_Blocks::plain( (string) $text ) : wp_strip_all_tags( (string) $text ); } } }