Current File : /home/jeconsul/public_html/wp-content/plugins/suremails/inc/controller/content-guard.php
<?php
/**
 * ContentGuard class
 *
 * Handles the moderation of the email content.
 *
 * @package SureMails\Inc\Controller
 */

namespace SureMails\Inc\Controller;

use SureMails\Inc\ConnectionManager;
use SureMails\Inc\Emails\Handler\ProcessEmailData;
use SureMails\Inc\Settings;
use SureMails\Inc\Traits\Instance;
use SureMails\Inc\Utils\LogError;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class ContentGuard
 *
 * Handles the moderation of the email content.
 *
 * @since 1.0.0
 */
class ContentGuard {

	use Instance;

	/**
	 * Status of the content.
	 *
	 * @var bool
	 */
	public $status = false;

	/**
	 * Hashes of the content.
	 *
	 * @var array
	 */
	public $hashes = [];

	/**
	 * ContentGuard constructor.
	 */
	public function __construct() {
		if ( ! defined( 'SUREMAIL_HASHES' ) ) {
			define( 'SUREMAIL_HASHES', 'suremails_content_guard_hashes' );
		}
		$this->status = Settings::instance()->get_content_guard_status();
		$this->hashes = $this->get_hashes();
	}

	/**
	 * Check the email content and determine if it should be blocked.
	 *
	 * @param array $atts The email attributes.
	 * @return bool|array The status of the email content.
	 */
	public function check_email_content( array $atts ) {
		$result = true;
		if ( ! $this->status ) {
			return $result;
		}

		$subject = $atts['subject'] ?? '';
		$message = $atts['message'] ?? '';

		$is_flagged = $this->check( $subject, $message );

		if ( $is_flagged['status'] === true ) {
			$atts['content_guard'] = $is_flagged['response'];
			$result                = $is_flagged['response']['categories'];
			$this->handle_content_guard_response( $atts );
		}

		return $result;
	}

	/**
	 * Check if the content is protected.
	 *
	 * @param string $content The content to check.
	 * @return array Moderation response.
	 * @since 1.0.0
	 */
	public function validate( $content ) {
		$response = wp_safe_remote_post(
			SUREMAILS_CONTENT_GUARD_MIDDLEWARE . 'moderations',
			[
				'body' => [
					'input' => $content,
				],
			]
		);

		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return [
				'status'  => 'error',
				'message' => __( 'An error occurred while validating the content.', 'suremails' ),
			];
		}

		return json_decode( wp_remote_retrieve_body( $response ), true );
	}

	/**
	 * Generate hash for the content.
	 *
	 * @param string $content The content to generate hash for.
	 * @return string The generated hash.
	 * @since 1.0.0
	 */
	public function generate_hash( $content ) {
		$nouns   = $this->find_proper_nouns( $content );
		$content = $this->trim_strings( $content, $nouns );
		return md5( $content );
	}

	/**
	 * Get all the hashes.
	 *
	 * @return array Associative array of hashes and their responses.
	 * @since 1.0.0
	 */
	public function get_hashes() {
		return get_option( SUREMAIL_HASHES, [] );
	}

	/**
	 * Add a hash with its corresponding response.
	 *
	 * @param string $hash The hash to add.
	 * @param array  $response The moderation response associated with the hash.
	 * @since 1.0.0
	 * @return void
	 */
	public function add_hash( $hash, $response ) {
		$hashes = $this->hashes;

		if ( is_array( $hashes ) && ! empty( $hashes ) && array_key_exists( $hash, $hashes ) ) {
			return;
		}
		$hashes[ $hash ] = $response;
		update_option( SUREMAIL_HASHES, $hashes );
	}

	/**
	 * Check if the hash exists.
	 *
	 * @param string $hash The hash to check.
	 * @return bool
	 * @since 1.0.0
	 */
	public function hash_exists( $hash ) {
		$hashes = $this->hashes;
		return array_key_exists( $hash, $hashes );
	}

	/**
	 * Get the response associated with a hash.
	 *
	 * @param string $hash The hash to get the response for.
	 * @return array|bool The response array if the hash exists, false otherwise.
	 * @since 1.0.0
	 */
	public function get_hash( $hash ) {
		$hashes = $this->hashes;
		if ( array_key_exists( $hash, $hashes ) ) {
			return $hashes[ $hash ];
		}
		return false;
	}

	/**
	 * Process the content.
	 *
	 * @param string $subject The subject of the email.
	 * @param string $message The message body of the email.
	 * @return array The result of the moderation process.
	 * @since 1.0.0
	 */
	public function check( $subject = '', $message = '' ) {
		$result = [
			'status'   => false,
			'response' => [],
		];

		$content = $subject . wp_strip_all_tags( $message );
		$hash    = $this->generate_hash( $content );

		$is_flagged = $this->get_hash( $hash );

		if ( $is_flagged !== false && is_array( $is_flagged ) && ! empty( $is_flagged ) ) {

			if ( isset( $is_flagged['flagged'] ) && $is_flagged['flagged'] === true ) {
				$result['status']   = true;
				$result['response'] = $is_flagged;
				return $result;
			}

			$result['status']   = false;
			$result['response'] = $is_flagged;
			return $result;
		}

		$response = $this->validate( $content );

		if ( ! isset( $response['status'] ) && isset( $response['flagged'] ) ) {
			$this->add_hash( $hash, $response );
			$this->hashes [ $hash ] = $response;
			if ( $response['flagged'] === true ) {
				$result['status']   = true;
				$result['response'] = $response;
				return $result;
			}
			$result['status']   = false;
			$result['response'] = $response;
			return $result;
		}

		return $result;
	}

	/**
	 * Find proper nouns in the text.
	 *
	 * @param string $text The text to search for proper nouns.
	 * @return array The list of proper nouns found in the text.
	 * @since 1.3.0
	 */
	public function find_proper_nouns( $text ) {
		// Tokenize text into sentences using full stops, new lines, or other delimiters.
		$sentences = preg_split( '/(\.|\n|\?|!)/', $text );

		if ( ! $sentences ) {
			return [];
		}

		$proper_nouns = [];

		foreach ( $sentences as $sentence ) {
			// Trim and extract words.
			$words = preg_split( '/\s+/', trim( $sentence ) );

			if ( ! $words ) {
				continue;
			}

			foreach ( $words as $index => $word ) {
				// Remove punctuation from the word.
				$word = preg_replace( '/[^a-zA-Z]/', '', $word );

				if ( empty( $word ) ) {
					continue;
				}

				// Check if the word starts with an uppercase letter and is not the first word of a sentence.
				if ( preg_match( '/^[A-Z][a-z]+$/', $word ) && ( $index > 0 || empty( $proper_nouns ) ) ) {
					$proper_nouns[] = $word;
				}
			}
		}

		return array_unique( $proper_nouns );
	}

	/**
	 * Trim all the given strings from the text.
	 *
	 * @param string $text The text to trim.
	 * @param array  $strings The strings to trim from the text.
	 *
	 * @return string The trimmed text.
	 * @since 1.3.0
	 */
	public function trim_strings( $text, $strings ) {
		// Trim all the given strings from the text.
		foreach ( $strings as $string ) {
			$text = str_replace( $string, '', $text );
		}

		return $text;
	}

	/**
	 * Delete the content guard hashes.
	 *
	 * @return void
	 */
	public static function flush_hashes() {
		update_option( 'suremails_content_guard_hashes', [] );
	}

	/**
	 * Handles the response when content is flagged.
	 *
	 * @param array $atts The email attributes with content guard response.
	 * @return int|bool The log ID after handling the response.
	 */
	private function handle_content_guard_response( array $atts ) {

		$new_server_response = [
			'retry'      => 0,
			'Message'    => __( 'Email content is flagged.', 'suremails' ),
			'Connection' => __( 'Reputation Shield', 'suremails' ),
			'timestamp'  => current_time( 'mysql' ),
		];

		$email_data_processor = ProcessEmailData::instance();
		$logger               = Logger::instance();
		$connection_manager   = ConnectionManager::instance();
		$atts['to']           = $email_data_processor->process_to( $atts['to'] );
		$atts['headers']      = $email_data_processor->process_headers( $atts['headers'] );
		$atts['attachments']  = $email_data_processor->process_attachments( $atts['attachments'] );
		$from_email           = $atts['headers']['from']['email'];
		$email_from           = '';
		if ( ! empty( $from_email ) ) {
			$from_name  = ! empty( $atts['headers']['from']['name'] ) ? $atts['headers']['from']['name'] : 'WordPress';
			$email_from = "{$from_name} <{$from_email}>";
		}

		$handler_response = $logger->prepare_log_data(
			[
				'email_to'    => $email_data_processor->format_email_recipients( $atts['to'] ),
				'email_from'  => ! empty( $email_from ) ? $email_from : ' ',
				'subject'     => $atts['subject'],
				'body'        => $atts['message'],
				'attachments' => $atts['attachments'],
				'headers'     => $email_data_processor->format_processed_headers( $atts['headers'] ),
				'status'      => Logger::STATUS_BLOCKED,
				'connection'  => '',
				'meta'        => [
					'retry'         => 0,
					'resend'        => 0,
					'content_guard' => $atts['content_guard'],
				],
				'response'    => [ $new_server_response ],
			]
		);

		$log_id = $logger->get_id();

		if ( $log_id === null ) {
			// First time logging.
			$log_id = $logger->log_email( $handler_response );

			if ( is_wp_error( $log_id ) ) {
				LogError::instance()->log_error( 'Failed to log email: ' . $log_id->get_error_message() );
				return false;
			}

			return $log_id;
		}
		$log_entry = (array) $logger->get_log( $log_id );
		$meta      = $log_entry['meta'] ?? [
			'retry'  => 0,
			'resend' => 0,
		];

		if ( $connection_manager->get_is_retried() ) {
			$meta['retry'] += 1;
		}

		$existing_responses = $log_entry['response'];
		if ( ! is_array( $existing_responses ) ) {
			$existing_responses = [];
		}
		$meta['content_guard']        = $atts['content_guard'];
		$new_server_response['retry'] = $meta['retry'];
		$existing_responses[]         = $new_server_response;
		$update_data                  = [
			'meta'       => $meta,
			'response'   => $existing_responses,
			'status'     => Logger::STATUS_BLOCKED,
			'updated_at' => current_time( 'mysql' ),
			'connection' => '',
		];
		$update_result                = $logger->update_log( $log_id, $update_data );
		$connection_manager->reset();
		if ( is_wp_error( $update_result ) || ! $update_result ) {
			// translators: %d is the log ID.
			LogError::instance()->log_error( sprintf( __( 'Failed to update log ID %d.', 'suremails' ), $log_id ) );
			return false;
		}
		return $log_id;
	}

}