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 ? '' . implode( '', $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 ? '' . implode( '', $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;
}
}