File: /wordpress/plugins/wp-cloud-client/1.1.7/src/Mail/FormPluginReplyTo.php
<?php
declare(strict_types=1);
namespace VPlugins\WPCloudClient\Mail;
/**
* Ensures outgoing form notification emails carry a "Name <email>" Reply-To
* header so wp-cloud-manager can create CRM contacts with the correct name.
*
* Supported plugins (each hook registered only when the plugin is active):
* - Contact Form 7 (WPCF7)
* - Gravity Forms (GFForms)
* - WPForms (WPForms)
* - Ninja Forms (Ninja_Forms)
* - Formidable Forms (FrmAppController)
*/
final class FormPluginReplyTo {
/**
* Ninja Forms submitted fields cached between the submit and email hooks.
*
* @var array
*/
private static array $nfFields = [];
/**
* Check whether any of the five supported form plugins is active:
* Contact Form 7, Gravity Forms, WPForms, Ninja Forms, or Formidable Forms.
*/
public static function isActive(): bool {
return class_exists( 'GFForms' )
|| class_exists( 'WPCF7' )
|| class_exists( 'WPForms' )
|| class_exists( 'Ninja_Forms' )
|| class_exists( 'FrmAppController' );
}
/**
* Register hooks for each active form plugin.
*/
public function register(): void {
if ( class_exists( 'WPCF7' ) ) {
add_filter( 'wpcf7_mail_components', [ $this, 'handleCF7' ], 10, 1 );
}
if ( class_exists( 'GFForms' ) ) {
add_filter( 'gform_pre_send_email', [ $this, 'handleGF' ], 10, 4 );
}
if ( class_exists( 'WPForms' ) ) {
// $email (array), $fields (array) — drop unused $entry/$form_data/$notification_id.
add_filter( 'wpforms_entry_email_atts', [ $this, 'handleWPForms' ], 10, 2 );
}
if ( class_exists( 'Ninja_Forms' ) ) {
add_filter( 'ninja_forms_submit_data', [ $this, 'cacheNFFields' ] );
// Only $settings needed — drop unused $action.
add_filter( 'ninja_forms_run_action_settings', [ $this, 'handleNinjaForms' ], 10, 1 );
}
if ( class_exists( 'FrmAppController' ) ) {
add_filter( 'frm_email_header', [ $this, 'handleFormidable' ], 10, 2 );
}
}
// -------------------------------------------------------------------------
// Contact Form 7
// -------------------------------------------------------------------------
/**
* Upgrade "Reply-To: email" to "Reply-To: Name <email>" using CF7 submission data.
*
* @param array $components Mail components (additional_headers, body, …).
* @return array
*/
public function handleCF7( array $components ): array {
try {
$headers = (string) ( $components['additional_headers'] ?? '' );
if ( ! preg_match( '/^Reply-To:\s*(.+)$/im', $headers, $m ) ) {
return $components;
}
$replyToValue = trim( $m[1] );
// Already has a non-empty display name before '<'.
if ( preg_match( '/^[^<]+</', $replyToValue ) ) {
return $components;
}
$submission = class_exists( 'WPCF7_Submission' ) ? \WPCF7_Submission::get_instance() : null;
if ( null === $submission ) {
return $components;
}
$name = $this->findNameFromKeyedData( $submission->get_posted_data(), 'your-name' );
if ( '' === $name ) {
return $components;
}
$name = $this->sanitizeHeaderValue( $name );
$email = $this->sanitizeHeaderValue( trim( $replyToValue, '<> ' ) );
$components['additional_headers'] = preg_replace(
'/^Reply-To:\s*.+$/im',
"Reply-To: {$name} <{$email}>",
$headers
);
return $components;
} catch ( \Throwable $e ) {
return $components;
}
}
// -------------------------------------------------------------------------
// Gravity Forms
// -------------------------------------------------------------------------
/**
* Inject "Reply-To: Name <email>" from the GF entry when Reply-To is absent.
*
* @param array $emailData GF email array (to, from, subject, message, headers, …).
* @param string $messageFormat 'html' or 'text'.
* @param array $notification GF notification settings.
* @param array $entry GF entry data.
* @return array
*/
public function handleGF( array $emailData, string $messageFormat, array $notification, array $entry ): array {
try {
$headers = (array) ( $emailData['headers'] ?? [] );
foreach ( $headers as $header ) {
if ( stripos( (string) $header, 'reply-to:' ) === 0 ) {
return $emailData;
}
}
$form = class_exists( 'GFAPI' ) ? ( \GFAPI::get_form( (int) ( $entry['form_id'] ?? 0 ) ) ?: [] ) : [];
$visitorEmail = $this->findEmailFromGFEntry( $form, $entry );
if ( '' === $visitorEmail ) {
return $emailData;
}
$name = $this->sanitizeHeaderValue( $this->findNameFromGFEntry( $form, $entry ) );
$visitorEmail = $this->sanitizeHeaderValue( $visitorEmail );
$replyTo = '' !== $name ? "{$name} <{$visitorEmail}>" : $visitorEmail;
$emailData['headers'][] = "Reply-To: {$replyTo}";
return $emailData;
} catch ( \Throwable $e ) {
return $emailData;
}
}
// -------------------------------------------------------------------------
// WPForms
// -------------------------------------------------------------------------
/**
* Inject or upgrade Reply-To from WPForms submitted fields.
*
* @param array $email WPForms email array (headers as string, subject, message, …).
* @param array $fields Submitted form fields keyed by field ID.
* @return array
*/
public function handleWPForms( array $email, array $fields ): array {
try {
$headers = (string) ( $email['headers'] ?? '' );
$visitorEmail = $this->findEmailFromWPFormsFields( $fields );
if ( '' === $visitorEmail ) {
return $email;
}
$name = $this->sanitizeHeaderValue( $this->findNameFromWPFormsFields( $fields ) );
$visitorEmail = $this->sanitizeHeaderValue( $visitorEmail );
if ( preg_match( '/^Reply-To:\s*(.+)$/im', $headers, $m ) ) {
// Upgrade only if display name is missing.
if ( preg_match( '/^[^<]+</', trim( $m[1] ) ) || '' === $name ) {
return $email;
}
$email['headers'] = preg_replace(
'/^Reply-To:\s*.+$/im',
"Reply-To: {$name} <{$visitorEmail}>",
$headers
);
return $email;
}
$replyTo = '' !== $name ? "{$name} <{$visitorEmail}>" : $visitorEmail;
$email['headers'] .= "\r\nReply-To: {$replyTo}";
return $email;
} catch ( \Throwable $e ) {
return $email;
}
}
// -------------------------------------------------------------------------
// Ninja Forms
// -------------------------------------------------------------------------
/**
* Cache submitted Ninja Forms fields for use in the email action hook.
* Keyed by form_id to avoid cross-form contamination.
*
* @param array $data Submission data including 'fields' and 'form_id'.
* @return array
*/
public function cacheNFFields( array $data ): array {
$formId = (string) ( $data['form_id'] ?? '0' );
self::$nfFields[ $formId ] = $data['fields'] ?? [];
return $data;
}
/**
* Inject Reply-To into Ninja Forms email action settings.
* Uses the most recently cached field set and clears the cache after use
* to prevent stale data from bleeding into subsequent submissions.
*
* @param array $settings Email action settings.
* @return array
*/
public function handleNinjaForms( array $settings ): array {
try {
if ( 'email' !== ( $settings['type'] ?? '' ) ) {
return $settings;
}
if ( ! empty( $settings['reply_to'] ) ) {
return $settings;
}
// Use the most recently cached entry (last inserted).
$fields = ! empty( self::$nfFields ) ? end( self::$nfFields ) : [];
self::$nfFields = [];
$visitorEmail = $this->findEmailFromNFFields( (array) $fields );
if ( '' === $visitorEmail ) {
return $settings;
}
$settings['reply_to'] = $this->sanitizeHeaderValue( $visitorEmail );
$settings['reply_to_name'] = $this->sanitizeHeaderValue( $this->findNameFromNFFields( (array) $fields ) );
return $settings;
} catch ( \Throwable $e ) {
return $settings;
}
}
// -------------------------------------------------------------------------
// Formidable Forms
// -------------------------------------------------------------------------
/**
* Inject or upgrade Reply-To from Formidable Forms entry data.
*
* @param array $header Email headers array.
* @param array $atts Email attributes including 'entry' and 'form'.
* @return array
*/
public function handleFormidable( array $header, array $atts ): array {
try {
foreach ( $header as $h ) {
if ( stripos( (string) $h, 'reply-to:' ) === 0 ) {
return $header;
}
}
$entry = $atts['entry'] ?? null;
if ( ! is_object( $entry ) ) {
return $header;
}
$metas = (array) ( $entry->metas ?? [] );
$visitorEmail = $this->findEmailFromMetas( $metas );
if ( '' === $visitorEmail ) {
return $header;
}
$name = $this->sanitizeHeaderValue( $this->findNameFromFormidable( $atts, $metas ) );
$visitorEmail = $this->sanitizeHeaderValue( $visitorEmail );
$replyTo = '' !== $name ? "{$name} <{$visitorEmail}>" : $visitorEmail;
$header[] = "Reply-To: {$replyTo}";
return $header;
} catch ( \Throwable $e ) {
return $header;
}
}
// -------------------------------------------------------------------------
// Shared helpers
// -------------------------------------------------------------------------
/**
* Strip carriage-return and newline characters from a header value to
* prevent header injection attacks, then trim surrounding whitespace.
*
* Must be called on every name and email value before building a Reply-To
* header string.
*
* @param string $value Raw header value.
* @return string Sanitized value.
*/
private function sanitizeHeaderValue( string $value ): string {
return trim( preg_replace( '/[\r\n]+/', ' ', $value ) );
}
/**
* Find a name from a flat key=>value data array (CF7 posted data, WPForms text fields).
* Checks $defaultKey first, then any key containing "name".
*
* @param array $data Key => value pairs.
* @param string $defaultKey Preferred key to check first.
* @return string
*/
private function findNameFromKeyedData( array $data, string $defaultKey ): string {
if ( isset( $data[ $defaultKey ] ) && '' !== trim( (string) $data[ $defaultKey ] ) ) {
return trim( (string) $data[ $defaultKey ] );
}
foreach ( $data as $key => $value ) {
if ( is_string( $value ) && '' !== trim( $value ) && stripos( (string) $key, 'name' ) !== false ) {
return trim( $value );
}
}
return '';
}
/**
* Find an email address from a flat array of values by FILTER_VALIDATE_EMAIL.
*
* @param array $metas Flat array of values.
* @return string
*/
private function findEmailFromMetas( array $metas ): string {
foreach ( $metas as $value ) {
if ( is_string( $value ) && false !== filter_var( $value, FILTER_VALIDATE_EMAIL ) ) {
return $value;
}
}
return '';
}
// -------------------------------------------------------------------------
// GF helpers
// -------------------------------------------------------------------------
/**
* Find the first email-type field value in a GF entry.
*
* @param array $form GF form definition.
* @param array $entry GF entry data.
* @return string
*/
private function findEmailFromGFEntry( array $form, array $entry ): string {
foreach ( $form['fields'] ?? [] as $field ) {
if ( 'email' === $field->type ) {
$value = (string) ( $entry[ (string) $field->id ] ?? '' );
if ( '' !== $value ) {
return $value;
}
}
}
return '';
}
/**
* Find the visitor's name from a GF entry.
*
* @param array $form GF form definition.
* @param array $entry GF entry data.
* @return string
*/
private function findNameFromGFEntry( array $form, array $entry ): string {
foreach ( $form['fields'] ?? [] as $field ) {
if ( 'name' === $field->type ) {
$first = trim( (string) ( $entry[ $field->id . '.3' ] ?? '' ) );
$last = trim( (string) ( $entry[ $field->id . '.6' ] ?? '' ) );
$name = trim( $first . ' ' . $last );
if ( '' !== $name ) {
return $name;
}
// Also check the base field id (simple name format with no sub-fields).
$base = trim( (string) ( $entry[ (string) $field->id ] ?? '' ) );
if ( '' !== $base ) {
return $base;
}
}
}
foreach ( $form['fields'] ?? [] as $field ) {
if ( 'text' === $field->type && stripos( (string) ( $field->label ?? '' ), 'name' ) !== false ) {
$value = trim( (string) ( $entry[ (string) $field->id ] ?? '' ) );
if ( '' !== $value ) {
return $value;
}
}
}
return '';
}
// -------------------------------------------------------------------------
// WPForms helpers
// -------------------------------------------------------------------------
/**
* Find the visitor's email from WPForms submitted fields.
*
* @param array $fields WPForms fields array keyed by field ID.
* @return string
*/
private function findEmailFromWPFormsFields( array $fields ): string {
foreach ( $fields as $field ) {
if ( 'email' === ( $field['type'] ?? '' ) && '' !== trim( (string) ( $field['value'] ?? '' ) ) ) {
return trim( (string) $field['value'] );
}
}
return '';
}
/**
* Find the visitor's name from WPForms submitted fields.
*
* @param array $fields WPForms fields array keyed by field ID.
* @return string
*/
private function findNameFromWPFormsFields( array $fields ): string {
foreach ( $fields as $field ) {
if ( 'name' === ( $field['type'] ?? '' ) ) {
$raw = $field['value_raw'] ?? [];
$first = trim( (string) ( $raw['first'] ?? '' ) );
$last = trim( (string) ( $raw['last'] ?? '' ) );
$name = trim( $first . ' ' . $last );
if ( '' !== $name ) {
return $name;
}
if ( '' !== trim( (string) ( $field['value'] ?? '' ) ) ) {
return trim( (string) $field['value'] );
}
}
}
// Fallback: look for a text field whose label contains "name".
foreach ( $fields as $field ) {
$label = (string) ( $field['label'] ?? '' );
if ( stripos( $label, 'name' ) !== false ) {
$value = trim( (string) ( $field['value'] ?? '' ) );
if ( '' !== $value && is_string( $field['value'] ?? null ) ) {
return $value;
}
}
}
return '';
}
// -------------------------------------------------------------------------
// Ninja Forms helpers
// -------------------------------------------------------------------------
/**
* Find the visitor's email from Ninja Forms cached fields.
*
* @param array $fields NF fields array.
* @return string
*/
private function findEmailFromNFFields( array $fields ): string {
foreach ( $fields as $field ) {
if ( 'email' === ( $field['type'] ?? '' ) && '' !== trim( (string) ( $field['value'] ?? '' ) ) ) {
return trim( (string) $field['value'] );
}
}
return '';
}
/**
* Find the visitor's name from Ninja Forms cached fields.
*
* @param array $fields NF fields array.
* @return string
*/
private function findNameFromNFFields( array $fields ): string {
$first = '';
$last = '';
foreach ( $fields as $field ) {
$type = (string) ( $field['type'] ?? '' );
$value = trim( (string) ( $field['value'] ?? '' ) );
if ( '' === $value ) {
continue;
}
if ( 'firstname' === $type ) {
$first = $value;
} elseif ( 'lastname' === $type ) {
$last = $value;
} elseif ( 'name' === $type ) {
return $value;
}
}
$name = trim( $first . ' ' . $last );
if ( '' !== $name ) {
return $name;
}
// Fallback: text field whose label contains "name".
foreach ( $fields as $field ) {
if ( stripos( (string) ( $field['label'] ?? '' ), 'name' ) !== false ) {
$value = trim( (string) ( $field['value'] ?? '' ) );
if ( '' !== $value ) {
return $value;
}
}
}
return '';
}
// -------------------------------------------------------------------------
// Formidable helpers
// -------------------------------------------------------------------------
/**
* Find the visitor's name from Formidable entry and form field definitions.
*
* @param array $atts Hook attributes (may include 'form' with field definitions).
* @param array $metas Entry meta values (field_id => value).
* @return string
*/
private function findNameFromFormidable( array $atts, array $metas ): string {
$form = $atts['form'] ?? null;
if ( is_object( $form ) && isset( $form->fields ) ) {
foreach ( (array) $form->fields as $field ) {
if ( stripos( (string) ( $field->name ?? '' ), 'name' ) !== false ) {
$value = trim( (string) ( $metas[ $field->id ] ?? '' ) );
if ( '' !== $value && false === filter_var( $value, FILTER_VALIDATE_EMAIL ) ) {
return $value;
}
}
}
}
// Fallback: non-email string values from metas.
foreach ( $metas as $value ) {
if ( is_string( $value ) && '' !== trim( $value )
&& false === filter_var( $value, FILTER_VALIDATE_EMAIL )
) {
return trim( $value );
}
}
return '';
}
}