json_error( array( 'code' => 'missing_form', 'message' => __( 'Missing form ID.', 'stacklar-forms' ) ), 400 ); } $this->verify_nonce_with( 'stacklar_mail_' . $form_id ); $post = get_post( $form_id ); if ( ! $post || $post->post_type !== 'stacklar' ) { return $this->json_error( array( 'code' => 'form_not_found', 'message' => __( 'Form not found.', 'stacklar-forms' ) ), 404 ); } $this->enforce_form_protection( $form_id ); $action_settings = $this->get_server_action_settings_or_fail( $form_id, $this->get_action() ); $data = $this->collect_post_data( array( 'action', 'stacklar_id', 'entry_id', 'stacklar_entry_token', '_stacklar_nonce', '_wp_http_referer' ) ); $data = $this->strip_hidden_field_values( $data, $post ); $label_mode = $this->is_label_key_mode(); // Field metadata that Stacklar Forms Pro fields post next to their values. unset( $data['__stacklar_signature_meta'], $data['stacklar_signature_meta'], $data['__stacklar_rating_meta'], $data['__stacklar_acceptance_meta'], $data['__stacklar_color_meta'], $data['__stacklar_hidden_meta'], $data['__stacklar_address_meta'] ); $data = $this->merge_tel_helpers( $data ); $flat_fields_plain = array(); $flat_fields_html = array(); $field_entries = array(); foreach ( $data as $key => $value ) { $key = sanitize_key( $key ); if ( '' === $key ) { continue; } $path = $key; $display_key = $label_mode ? $this->lookup_label_for_path( $path, $key ) : $key; list( $plain_value, $html_value ) = $this->normalise_field_output( $value, $path ); $flat_fields_plain[ $key ] = $plain_value; $flat_fields_html[ $key ] = $html_value; if ( $this->is_tel_helper_key( $key ) ) { continue; } $field_entries[] = array( 'key' => $display_key, 'plain' => $plain_value, 'html' => $html_value, ); } $all_fields_plain = ''; if ( ! empty( $field_entries ) ) { $lines = array(); foreach ( $field_entries as $entry ) { $lines[] = $entry['key'] . ': ' . $entry['plain']; } $all_fields_plain = implode( "\n", $lines ); } $all_fields_html = ''; if ( ! empty( $field_entries ) ) { $rows = array(); foreach ( $field_entries as $entry ) { $value_html = $entry['html'] !== '' ? $entry['html'] : '' . esc_html__( 'Empty', 'stacklar-forms' ) . ''; $rows[] = '
' . '' . esc_html( $entry['key'] ) . ':' . $value_html . '
'; } $all_fields_html = implode( '', $rows ); } $request_context = $this->get_request_context( $form_id ); $request_token_map = function_exists( 'stacklar_flatten_request_context' ) ? stacklar_flatten_request_context( $request_context ) : array(); $request_details_plain = ''; $request_details_html = ''; if ( ! empty( $request_context ) ) { $plain_lines = array(); $html_rows = array(); foreach ( $request_context as $key => $value ) { $display_value = function_exists( 'stacklar_request_value_to_string' ) ? stacklar_request_value_to_string( $value ) : ( is_scalar( $value ) ? (string) $value : wp_json_encode( $value ) ); if ( 'submitted' === $key ) { $timestamp = mysql2date( 'U', $display_value, true ); if ( $timestamp ) { $display_value = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ); } } $plain_lines[] = $key . ': ' . $display_value; $html_rows[] = '' . '' . esc_html( $key ) . '' . '' . esc_html( $display_value ) . '' . ''; $request_token_map[ 'request_' . $key ] = $display_value; } if ( ! empty( $plain_lines ) ) { $request_details_plain = "Request Details:\n" . implode( "\n", $plain_lines ); $request_details_html = '
' . '

' . esc_html__( 'Request Details', 'stacklar-forms' ) . '

' . '' . implode( '', $html_rows ) . '
' . '
'; if ( '' !== $all_fields_plain ) { $all_fields_plain .= "\n\n" . $request_details_plain; } else { $all_fields_plain = $request_details_plain; } if ( '' !== $all_fields_html ) { $all_fields_html .= $request_details_html; } else { $all_fields_html = $request_details_html; } } } $replace_tokens = function( $template, $format = 'text' ) use ( $flat_fields_plain, $flat_fields_html, $form_id, $all_fields_plain, $all_fields_html, $request_token_map, $request_details_plain, $request_details_html ) { if ( ! is_string( $template ) || $template === '' ) { return ''; } return preg_replace_callback( '/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', function ( $matches ) use ( $flat_fields_plain, $flat_fields_html, $form_id, $all_fields_plain, $all_fields_html, $request_token_map, $request_details_plain, $request_details_html, $format ) { $token = sanitize_key( $matches[1] ); if ( 'form_id' === $token ) { return (string) $form_id; } if ( 'all_fields' === $token ) { return ( 'html' === $format ) ? $all_fields_html : $all_fields_plain; } if ( 'request_details' === $token ) { if ( '' === $request_details_html && '' === $request_details_plain ) { return ''; } if ( 'html' === $format ) { return $request_details_html ? $request_details_html : nl2br( esc_html( $request_details_plain ) ); } return $request_details_plain; } if ( 'request_details_plain' === $token ) { if ( '' === $request_details_plain ) { return ''; } return ( 'html' === $format ) ? nl2br( esc_html( $request_details_plain ) ) : $request_details_plain; } if ( 'request_details_html' === $token ) { if ( '' === $request_details_html ) { return ''; } if ( 'html' === $format ) { return $request_details_html; } return wp_strip_all_tags( $request_details_html ); } if ( isset( $request_token_map[ $token ] ) ) { $value = $request_token_map[ $token ]; return ( 'html' === $format ) ? esc_html( $value ) : $value; } if ( 'html' === $format && isset( $flat_fields_html[ $token ] ) ) { return $flat_fields_html[ $token ]; } if ( isset( $flat_fields_plain[ $token ] ) ) { return $flat_fields_plain[ $token ]; } return ''; }, $template ); }; $to_setting = isset( $action_settings['to'] ) ? $action_settings['to'] : ''; $to_emails = array(); if ( is_string( $to_setting ) && $to_setting !== '' ) { $parts = array_map( 'trim', explode( ',', $to_setting ) ); foreach ( $parts as $part ) { $email = sanitize_email( $replace_tokens( $part, 'text' ) ); if ( $email ) { $to_emails[] = $email; } } } if ( empty( $to_emails ) ) { $default_email = sanitize_email( get_option( 'admin_email' ) ); if ( $default_email ) { $to_emails[] = $default_email; } } /* translators: %d: form ID. */ $default_subject = sprintf( __( 'New submission (Form #%d)', 'stacklar-forms' ), $form_id ); $subject_template = isset( $action_settings['subject'] ) && $action_settings['subject'] !== '' ? $action_settings['subject'] : $default_subject; $subject = $replace_tokens( $subject_template, 'text' ); if ( '' === $subject ) { $subject = $default_subject; } if ( isset( $action_settings['message'] ) && $action_settings['message'] !== '' ) { $template = wp_unslash( $action_settings['message'] ); $template = str_replace( array( "\r\n", "\r" ), "\n", $template ); $body = nl2br( $replace_tokens( $template, 'html' ) ); } else { /* translators: %d: form ID. */ $form_id_line = sprintf( esc_html__( 'Form ID: %d', 'stacklar-forms' ), $form_id ); $body = '

' . $form_id_line . '

' . $all_fields_html; } if ( '' === trim( $body ) ) { $body = $all_fields_html ?: '

' . esc_html__( 'No data submitted.', 'stacklar-forms' ) . '

'; } $headers = array( 'Content-Type: text/html; charset=UTF-8' ); $reply_to_field = isset( $action_settings['replyto'] ) ? sanitize_key( $action_settings['replyto'] ) : ''; if ( $reply_to_field && isset( $flat_fields_plain[ $reply_to_field ] ) ) { $reply_to_email = sanitize_email( $flat_fields_plain[ $reply_to_field ] ); if ( $reply_to_email ) { $headers[] = 'Reply-To: ' . $reply_to_email; } } $build_message = function( $key, $default ) use ( $action_settings, $replace_tokens ) { if ( empty( $action_settings[ $key ] ) ) { return $default; } $template = wp_unslash( (string) $action_settings[ $key ] ); $value = $replace_tokens( $template, 'text' ); $value = trim( wp_strip_all_tags( $value ) ); if ( '' === $value ) { return $default; } return $value; }; $success_message = $build_message( 'success_message', __( 'Email has been sent.', 'stacklar-forms' ) ); $error_message = $build_message( 'error_message', __( 'Unable to send email.', 'stacklar-forms' ) ); if ( $this->is_form_test_mode( $form_id ) ) { return $this->test_mode_success_response( $this->get_action(), __( 'Test mode: email was prepared but not sent.', 'stacklar-forms' ), array( 'prepared' => true, ) ); } if ( ! wp_mail( $to_emails, $subject, $body, $headers ) ) { return $this->json_error( array( 'code' => 'mail_failed', 'message' => $error_message ), 500 ); } // Hook for after-mail events if needed do_action( 'stacklar_after_mail', $form_id, $data ); return $this->json_success( array( 'code' => 'ok', 'message' => $success_message, ) ); } protected function normalise_field_output( $value, $path = '' ) { if ( is_array( $value ) ) { return $this->normalise_array_output( $value, $path ); } if ( is_bool( $value ) ) { $plain = $value ? '1' : '0'; return array( $plain, esc_html( $plain ) ); } if ( null === $value ) { return array( '', '' ); } if ( is_scalar( $value ) ) { $plain = (string) $value; $html = $plain !== '' ? nl2br( esc_html( $plain ) ) : ''; return array( $plain, $html ); } return array( '', '' ); } /** * Normalise array values (repeaters, lists, associative data). * * @param array $value Raw array value. * @return array{string,string} */ protected function normalise_array_output( array $value, $path = '' ) { if ( empty( $value ) ) { return array( '', '' ); } $empty_text = __( 'Empty', 'stacklar-forms' ); $empty_html = '' . esc_html( $empty_text ) . ''; if ( wp_is_numeric_array( $value ) ) { if ( $this->all_items_are_assoc( $value ) ) { return $this->normalise_repeater_output( $value, $empty_text, $empty_html, $path ); } return $this->normalise_list_output( $value, $empty_text, $empty_html, $path ); } return $this->normalise_assoc_output( $value, $empty_text, $empty_html, $path ); } /** * Normalise a list (numeric array) into plain and HTML strings. * * @param array $list List value. * @param string $empty_text Placeholder for empty content. * @param string $empty_html HTML placeholder for empty content. * @return array{string,string} */ protected function normalise_list_output( array $list, $empty_text, $empty_html, $path = '' ) { $plain_items = array(); $html_items = array(); foreach ( $list as $index => $item ) { $child_path = $path !== '' ? $path . '|' . $index : (string) $index; list( $plain, $html ) = $this->normalise_field_output( $item, $child_path ); if ( '' === $plain ) { $plain = $empty_text; } $plain_items[] = $plain; $html_items[] = '
  • ' . ( '' !== $html ? $html : $empty_html ) . '
  • '; } $plain = implode( ', ', array_map( 'trim', $plain_items ) ); $html = $html_items ? '' : ''; return array( $plain, $html ); } /** * Normalise an associative array. * * @param array $assoc Associative data. * @param string $empty_text Placeholder text. * @param string $empty_html Placeholder HTML. * @return array{string,string} */ protected function normalise_assoc_output( array $assoc, $empty_text, $empty_html, $path = '' ) { $plain_lines = array(); $html_items = array(); foreach ( $assoc as $key => $item ) { if ( $this->is_tel_helper_key( $key ) ) { continue; } $child_path = $path !== '' ? $path . '|' . $key : (string) $key; $label = $this->is_label_key_mode() ? $this->lookup_label_for_path( $child_path, (string) $key ) : (string) $key; list( $plain, $html ) = $this->normalise_field_output( $item, $child_path ); if ( '' === $plain ) { $plain = $empty_text; } $plain_lines[] = $label . ': ' . $plain; $html_items[] = '
  • ' . esc_html( $label ) . ': ' . ( '' !== $html ? $html : $empty_html ) . '
  • '; } if ( empty( $plain_lines ) ) { return array( '', '' ); } $plain = implode( "\n", array_map( 'trim', $plain_lines ) ); $html = $html_items ? '' : ''; return array( $plain, $html ); } /** * Normalise repeater rows into plain text and HTML table output. * * @param array $rows Repeater rows. * @param string $empty_text Placeholder text. * @param string $empty_html Placeholder HTML. * @return array{string,string} */ protected function normalise_repeater_output( array $rows, $empty_text, $empty_html, $path = '' ) { $columns = array(); foreach ( $rows as $row ) { foreach ( $row as $col_key => $_ ) { if ( $this->is_tel_helper_key( $col_key ) ) { continue; } $columns[ $col_key ] = true; } } $columns = array_keys( $columns ); if ( empty( $columns ) ) { return $this->normalise_list_output( $rows, $empty_text, $empty_html, $path ); } $plain_rows = array(); $html_rows = array(); foreach ( $rows as $index => $row ) { $pairs = array(); $cell_htmls = array(); foreach ( $columns as $column ) { $cell_value = array_key_exists( $column, $row ) ? $row[ $column ] : ''; $cell_path = $path !== '' ? $path . '|' . $index . '|' . $column : $column; list( $cell_plain, $cell_html ) = $this->normalise_field_output( $cell_value, $cell_path ); if ( '' === $cell_plain ) { $cell_plain = $empty_text; } if ( '' === $cell_html ) { $cell_html = $empty_html; } $cell_label = $this->is_label_key_mode() ? $this->lookup_label_for_path( $cell_path, (string) $column ) : (string) $column; $pairs[] = $cell_label . ': ' . $cell_plain; $cell_htmls[] = '' . $cell_html . ''; } $plain_rows[] = sprintf( '#%1$d %2$s', $index + 1, $pairs ? implode( '; ', $pairs ) : $empty_text ); $html_rows[] = '' . implode( '', $cell_htmls ) . ''; } $head_cells = array(); foreach ( $columns as $column ) { $header_path = $path !== '' ? $path . '|' . $column : $column; $header_label = $this->is_label_key_mode() ? $this->lookup_label_for_path( $header_path, (string) $column ) : (string) $column; $head_cells[] = '' . esc_html( $header_label ) . ''; } $html_table = '
    ' . '' . '' . implode( '', $head_cells ) . '' . '' . implode( '', $html_rows ) . '' . '
    ' . '
    '; $plain = implode( "\n", array_map( 'trim', $plain_rows ) ); return array( $plain, $html_table ); } /** * Resolve the base key and helper type for telephone helper inputs. * * @param mixed $key * @return array{0:?string,1:?string} */ protected function detect_tel_helper_key( $key ) { if ( ! is_scalar( $key ) ) { return array( null, null ); } $normalized = sanitize_key( (string) $key ); if ( '' === $normalized ) { return array( null, null ); } if ( substr( $normalized, -13 ) === '_intl_display' ) { return array( substr( $normalized, 0, -13 ), 'intl_display' ); } if ( substr( $normalized, -9 ) === '_national' ) { return array( substr( $normalized, 0, -9 ), 'national' ); } if ( substr( $normalized, -8 ) === '_country' ) { return array( substr( $normalized, 0, -8 ), 'country' ); } return array( null, null ); } /** * Check whether a key represents a helper field for telephone inputs. * * @param mixed $key * @return bool */ protected function is_tel_helper_key( $key ) { list( $base, $type ) = $this->detect_tel_helper_key( $key ); return ( null !== $base && null !== $type ); } /** * Determine whether a value should be treated as empty for output purposes. * * @param mixed $value * @return bool */ protected function is_empty_field_value( $value ) { if ( is_array( $value ) ) { foreach ( $value as $item ) { if ( ! $this->is_empty_field_value( $item ) ) { return false; } } return true; } if ( null === $value || $value === false ) { return true; } if ( is_string( $value ) ) { return trim( $value ) === ''; } return $value === ''; } /** * Merge telephone helper values back into their base field when missing. * * @param mixed $value * @return mixed */ protected function merge_tel_helpers( $value ) { if ( ! is_array( $value ) ) { return $value; } $merged = array(); foreach ( $value as $key => $child ) { $merged[ $key ] = $this->merge_tel_helpers( $child ); } if ( ! wp_is_numeric_array( $merged ) ) { foreach ( $merged as $key => $child ) { list( $base, $type ) = $this->detect_tel_helper_key( $key ); if ( null === $base || null === $type ) { continue; } if ( 'intl_display' !== $type && 'national' !== $type ) { continue; } if ( $this->is_empty_field_value( $child ) ) { continue; } if ( isset( $merged[ $base ] ) && ! $this->is_empty_field_value( $merged[ $base ] ) ) { continue; } $merged[ $base ] = $child; } } return $merged; } /** * Determine whether every list item is an associative array. * * @param array $items List items. * @return bool */ protected function all_items_are_assoc( array $items ) { if ( empty( $items ) ) { return false; } foreach ( $items as $item ) { if ( ! is_array( $item ) || wp_is_numeric_array( $item ) ) { return false; } } return true; } }