initial
This commit is contained in:
+391
@@ -0,0 +1,391 @@
|
||||
<?php
|
||||
/**
|
||||
* Confirmation email form action.
|
||||
*
|
||||
* Sends an auto-reply to the person who submitted the form.
|
||||
* Plain text only. Never uses user data in the From header.
|
||||
*
|
||||
* @package GenerateBlocksPro
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation email form action class.
|
||||
*/
|
||||
class GenerateBlocks_Pro_Form_Action_Confirmation_Email {
|
||||
|
||||
/**
|
||||
* Register the action with the registry.
|
||||
*/
|
||||
public static function init() {
|
||||
GenerateBlocks_Pro_Form_Action_Registry::register( 'confirmation-email', [ __CLASS__, 'send' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate confirmation email settings before publishing or delivery.
|
||||
*
|
||||
* @param array $form_settings The form block settings.
|
||||
* @param array|null $schema Optional derived field schema list.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function validate_settings( $form_settings, $schema = null ) {
|
||||
if ( is_array( $schema ) ) {
|
||||
$email_field = self::get_email_field( $form_settings );
|
||||
|
||||
if ( '' === $email_field ) {
|
||||
return new WP_Error(
|
||||
'gb_form_confirmation_email_field_missing',
|
||||
__( 'Confirmation email field is required.', 'generateblocks-pro' ),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$field = self::find_schema_field( $schema, $email_field );
|
||||
|
||||
if ( ! $field ) {
|
||||
return new WP_Error(
|
||||
'gb_form_confirmation_email_field_missing',
|
||||
sprintf(
|
||||
/* translators: %s: email field name. */
|
||||
__( 'Confirmation email field "%s" does not exist.', 'generateblocks-pro' ),
|
||||
$email_field
|
||||
),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$field_type = $field['type'] ?? '';
|
||||
|
||||
if ( ! in_array( $field_type, [ 'email', 'text' ], true ) ) {
|
||||
return new WP_Error(
|
||||
'gb_form_confirmation_email_field_invalid',
|
||||
sprintf(
|
||||
/* translators: %s: email field name. */
|
||||
__( 'Confirmation email field "%s" must be an email or text field.', 'generateblocks-pro' ),
|
||||
$email_field
|
||||
),
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the submitted confirmation email recipient before delivery.
|
||||
*
|
||||
* @param array $sanitized_data The sanitized form data.
|
||||
* @param array $form_settings The form block settings.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function validate_submission( $sanitized_data, $form_settings ) {
|
||||
$email_field = self::get_email_field( $form_settings );
|
||||
|
||||
if ( '' === $email_field ) {
|
||||
return new WP_Error(
|
||||
'missing_email_field',
|
||||
'Confirmation email field is not configured.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
// is_array catches both missing key and shape mutation by a
|
||||
// generateblocks_form_validate_submission filter that returned
|
||||
// a flat scalar instead of the [value, label, type] array.
|
||||
$field = $sanitized_data[ $email_field ] ?? null;
|
||||
|
||||
if ( ! is_array( $field ) ) {
|
||||
return new WP_Error(
|
||||
'missing_email_field',
|
||||
'Confirmation email field not found in submission.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
$to = sanitize_email( (string) ( $field['value'] ?? '' ) );
|
||||
|
||||
if ( ! is_email( $to ) ) {
|
||||
return new WP_Error(
|
||||
'invalid_recipient',
|
||||
'Invalid recipient email for confirmation.',
|
||||
[ 'status' => 400 ]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a confirmation email to the submitter.
|
||||
*
|
||||
* @param array $sanitized_data The sanitized form data (field_name => ['value' => ..., 'label' => ..., 'type' => ...]).
|
||||
* @param array $form_settings The form block settings.
|
||||
* @param array $context Request context (post_id, page_url).
|
||||
* @return true|WP_Error True on success, WP_Error on failure.
|
||||
*/
|
||||
public static function send( $sanitized_data, $form_settings, $context = [] ) {
|
||||
$validation = self::validate_submission( $sanitized_data, $form_settings );
|
||||
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
$email_field = self::get_email_field( $form_settings );
|
||||
$to_field = $sanitized_data[ $email_field ] ?? null;
|
||||
$to = sanitize_email( is_array( $to_field ) ? (string) ( $to_field['value'] ?? '' ) : '' );
|
||||
|
||||
// Build subject with merge tag support.
|
||||
$raw_subject = $form_settings['confirmationEmailSubject'] ?? '';
|
||||
|
||||
if ( empty( $raw_subject ) ) {
|
||||
$raw_subject = sprintf(
|
||||
/* translators: %s: site name */
|
||||
__( 'Thank you for contacting %s', 'generateblocks-pro' ),
|
||||
get_bloginfo( 'name' )
|
||||
);
|
||||
}
|
||||
|
||||
$subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
|
||||
self::replace_merge_tags( $raw_subject, $sanitized_data )
|
||||
);
|
||||
|
||||
// Build body with merge tag support.
|
||||
$raw_body = $form_settings['confirmationEmailBody'] ?? '';
|
||||
|
||||
if ( empty( $raw_body ) ) {
|
||||
$raw_body = __( 'Thank you for your submission. We will get back to you soon.', 'generateblocks-pro' );
|
||||
}
|
||||
|
||||
$body = self::replace_merge_tags( $raw_body, $sanitized_data );
|
||||
|
||||
// Plain text only. Never set From from user data.
|
||||
$headers = [ 'Content-Type: text/plain; charset=UTF-8' ];
|
||||
|
||||
$reply_to_header = self::build_reply_to_header(
|
||||
$form_settings['confirmationEmailReplyToEmail'] ?? '',
|
||||
$form_settings['confirmationEmailReplyToName'] ?? ''
|
||||
);
|
||||
|
||||
if ( '' !== $reply_to_header ) {
|
||||
$headers[] = $reply_to_header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the confirmation email arguments before sending.
|
||||
*
|
||||
* @since 2.6.0
|
||||
* @param array $email_args {
|
||||
* Email arguments.
|
||||
*
|
||||
* @type string $to Recipient email address.
|
||||
* @type string $subject Email subject line.
|
||||
* @type string $body Email body content.
|
||||
* @type array $headers Email headers.
|
||||
* }
|
||||
* @param array $sanitized_data The sanitized form data.
|
||||
* @param array $form_settings The form block settings.
|
||||
* @param array $context Request context.
|
||||
*/
|
||||
$email_args = apply_filters(
|
||||
'generateblocks_form_confirmation_email_args',
|
||||
[
|
||||
'to' => $to,
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'headers' => $headers,
|
||||
],
|
||||
$sanitized_data,
|
||||
$form_settings,
|
||||
$context
|
||||
);
|
||||
|
||||
// Re-validate filter output: defensive against third-party callbacks
|
||||
// returning values with embedded newlines (header injection) or
|
||||
// reshaping the array entirely.
|
||||
if ( ! is_array( $email_args ) ) {
|
||||
$email_args = [];
|
||||
}
|
||||
|
||||
$raw_to = $email_args['to'] ?? '';
|
||||
// Two-pass sanitization: strip CR/LF/tab + percent-encoded variants
|
||||
// before sanitize_email() so a filter that returned an address with
|
||||
// embedded newlines can't leak into wp_mail's `To` header. Belt and
|
||||
// suspenders — sanitize_email() rejects most malformed addresses
|
||||
// already, but this matches how `subject` and `headers` are
|
||||
// hardened above so the contract is symmetric.
|
||||
$safe_to = is_string( $raw_to )
|
||||
? sanitize_email( GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $raw_to ) )
|
||||
: '';
|
||||
$raw_subject = $email_args['subject'] ?? '';
|
||||
$safe_subject = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
|
||||
is_string( $raw_subject ) ? $raw_subject : ''
|
||||
);
|
||||
$safe_headers = self::sanitize_filter_headers( $email_args['headers'] ?? [] );
|
||||
$raw_body = $email_args['body'] ?? '';
|
||||
$safe_body = is_string( $raw_body ) ? $raw_body : '';
|
||||
|
||||
if ( ! is_email( $safe_to ) ) {
|
||||
return new WP_Error( 'confirmation_email_invalid_to', 'No valid email recipient.' );
|
||||
}
|
||||
|
||||
// This email is submitter-facing: with no other sender customization,
|
||||
// wp_mail sends as core's literal "WordPress". Swap that default for
|
||||
// the site name, scoped to this send only.
|
||||
add_filter( 'wp_mail_from_name', [ __CLASS__, 'filter_default_from_name' ], 999 );
|
||||
|
||||
try {
|
||||
$sent = wp_mail(
|
||||
$safe_to,
|
||||
$safe_subject,
|
||||
$safe_body,
|
||||
$safe_headers
|
||||
);
|
||||
} finally {
|
||||
remove_filter( 'wp_mail_from_name', [ __CLASS__, 'filter_default_from_name' ], 999 );
|
||||
}
|
||||
|
||||
if ( ! $sent ) {
|
||||
return new WP_Error( 'confirmation_email_failed', 'Failed to send confirmation email.' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap core's literal "WordPress" default From name for the site name.
|
||||
*
|
||||
* Runs at priority 999 so any name customized earlier — an SMTP plugin
|
||||
* or a site-wide wp_mail_from_name filter — passes through untouched;
|
||||
* only the unmodified core default is replaced.
|
||||
*
|
||||
* @param mixed $name Current From display name.
|
||||
* @return mixed From display name.
|
||||
*/
|
||||
public static function filter_default_from_name( $name ) {
|
||||
if ( 'WordPress' !== $name ) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
$site_name = trim(
|
||||
GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header(
|
||||
wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES )
|
||||
)
|
||||
);
|
||||
|
||||
return '' !== $site_name ? $site_name : $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-validate the `headers` array returned by a filter callback.
|
||||
*
|
||||
* Note: we only strip CR/LF/tab and their percent-encoded variants here.
|
||||
* Running sanitize_text_field() would mangle valid display-name email
|
||||
* syntax like "Reply-To: Name <a@b.com>" (strip_tags eats the <...>).
|
||||
*
|
||||
* @param mixed $headers The filtered headers value.
|
||||
* @return array Sanitized headers.
|
||||
*/
|
||||
private static function sanitize_filter_headers( $headers ) {
|
||||
if ( ! is_array( $headers ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$safe = [];
|
||||
|
||||
foreach ( $headers as $header ) {
|
||||
if ( ! is_string( $header ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$header = preg_replace( '/[\r\n\t]/', '', $header );
|
||||
$header = str_replace( [ '%0a', '%0d', '%0A', '%0D' ], '', $header );
|
||||
$header = trim( $header );
|
||||
|
||||
if ( '' !== $header ) {
|
||||
$safe[] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
return $safe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a safe Reply-To header from static confirmation email settings.
|
||||
*
|
||||
* @param mixed $raw_email Configured reply-to email.
|
||||
* @param mixed $raw_name Configured reply-to display name.
|
||||
* @return string Header line, or empty string when no valid email exists.
|
||||
*/
|
||||
private static function build_reply_to_header( $raw_email, $raw_name ) {
|
||||
$email = is_scalar( $raw_email ) ? sanitize_email( (string) $raw_email ) : '';
|
||||
|
||||
if ( ! is_email( $email ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$email = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $email );
|
||||
$name = is_scalar( $raw_name ) ? sanitize_text_field( (string) $raw_name ) : '';
|
||||
$name = GenerateBlocks_Pro_Form_Sanitizer::sanitize_email_header( $name );
|
||||
$name = trim( str_replace( [ '<', '>', '"', ',', ':', '\\' ], '', $name ) );
|
||||
|
||||
if ( '' === $name ) {
|
||||
return 'Reply-To: ' . $email;
|
||||
}
|
||||
|
||||
return 'Reply-To: ' . $name . ' <' . $email . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace {field:name} merge tags with sanitized field values.
|
||||
*
|
||||
* Uses sanitize_textarea_field() for textarea fields to preserve newlines,
|
||||
* and sanitize_text_field() for everything else.
|
||||
*
|
||||
* @param string $text The text containing merge tags.
|
||||
* @param array $sanitized_data The sanitized form data.
|
||||
* @return string Text with merge tags replaced.
|
||||
*/
|
||||
public static function replace_merge_tags( $text, $sanitized_data ) {
|
||||
return GenerateBlocks_Pro_Form_Sanitizer::replace_merge_tags( $text, $sanitized_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured recipient field.
|
||||
*
|
||||
* @param array $form_settings The form block settings.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_email_field( $form_settings ) {
|
||||
if (
|
||||
isset( $form_settings['confirmationEmailField'] )
|
||||
&& ( is_array( $form_settings['confirmationEmailField'] ) || is_object( $form_settings['confirmationEmailField'] ) )
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return isset( $form_settings['confirmationEmailField'] )
|
||||
? sanitize_key( $form_settings['confirmationEmailField'] )
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a field in the derived schema by name.
|
||||
*
|
||||
* @param array $schema Field schema list.
|
||||
* @param string $name Field name.
|
||||
* @return array|null
|
||||
*/
|
||||
private static function find_schema_field( $schema, $name ) {
|
||||
foreach ( $schema as $field ) {
|
||||
if ( is_array( $field ) && ( $field['name'] ?? '' ) === $name ) {
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user