HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: /wordpress/plugins/wp-cloud-client/1.1.3/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 '';
	}
}