Uname:Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64

403WebShell
403Webshell
Server IP : 146.59.209.152  /  Your IP : 216.73.216.152
Web Server : Apache
System : Linux webm009.cluster131.gra.hosting.ovh.net 6.18.42-ovh-vps-grsec-zfs+ #1 SMP PREEMPT_DYNAMIC Wed Aug 5 15:59:48 CEST 2026 x86_64
User : monpetu ( 144298)
PHP Version : 7.4.33
Disable Function : _dyuweyrj4,_dyuweyrj4r,dl
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : OFF  |  Pkexec : OFF
Directory :  /home/monpetu/www/ipprint/wp-content/plugins/surerank/inc/modules/email-reports/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /home/monpetu/www/ipprint/wp-content/plugins/surerank/inc/modules/email-reports/controller.php
<?php
/**
 * Email Reports
 *
 * The main module for scheduling and sending email reports.
 *
 * @package SureRank\Inc\Modules\EmailReports
 * @since 1.6.0
 */

namespace SureRank\Inc\Modules\EmailReports;

use Exception;
use SureRank\Inc\Frontend\Image;
use SureRank\Inc\Traits\Get_Instance;
use SureRank\Inc\Traits\Logger;
use SureRank\Inc\GoogleSearchConsole\Controller as  GSC_Controller;
use SureRank\Inc\GoogleSearchConsole\Auth as GSC_Auth;


if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Email Reports Controller.
 *
 * @since 1.6.0
 */
class Controller {



	use Get_Instance;
	use Logger;

	/**
	 * Cron event name for email reports.
	 *
	 * @since 1.6.0
	 */
	public const EMAIL_REPORTS_CRON_EVENT = 'surerank_send_email_report';

	/**
	 * Email Reports Controller.
	 *
	 * @since 1.6.0
	 */
	public function __construct() {
		$this->init();
	}

	/**
	 * Initialize the Email Reports module.
	 *
	 * @since 1.6.0
	 * @return void
	 */
	public function init() {
		add_action( self::EMAIL_REPORTS_CRON_EVENT, [ $this, 'send_email_report' ] );
		add_action( 'init', [ $this, 'maybe_send_email_fallback' ] );

		$this->cleanup_old_cron_schedules();

		// Schedule daily cron if not already scheduled (uses WordPress built-in 'daily' schedule).
		if ( ! wp_next_scheduled( self::EMAIL_REPORTS_CRON_EVENT ) ) {
			wp_schedule_event( time(), 'daily', self::EMAIL_REPORTS_CRON_EVENT );
		}
	}

	/**
	 * Clean up old weekly cron schedules from previous version.
	 *
	 * In previous versions, we used a custom 'weekly' schedule. Now we use 'daily'.
	 * This method unschedules any old weekly crons and ensures only daily cron exists.
	 *
	 * @since 1.6.2
	 * @return void
	 */
	private function cleanup_old_cron_schedules() {
		$cleanup_done = get_option( 'surerank_email_reports_cron_cleanup_done' );
		if ( $cleanup_done ) {
			return;
		}

		try {

			$crons = _get_cron_array();
			if ( empty( $crons ) ) {
				update_option( 'surerank_email_reports_cron_cleanup_done', true, false );
				return;
			}

			foreach ( $crons as $timestamp => $cron ) {
				if ( isset( $cron[ self::EMAIL_REPORTS_CRON_EVENT ] ) ) {
					foreach ( $cron[ self::EMAIL_REPORTS_CRON_EVENT ] as $event ) {
						if ( isset( $event['schedule'] ) && 'weekly' === $event['schedule'] ) {
							wp_unschedule_event( $timestamp, self::EMAIL_REPORTS_CRON_EVENT, $event['args'] ?? [] );
						}
					}
				}
			}

			update_option( 'surerank_email_reports_cron_cleanup_done', true, false );
		} catch ( Exception $e ) {

			Logger::log( 'Error cleaning up old email reports cron schedules: ' . $e->getMessage() );
			update_option( 'surerank_email_reports_cron_cleanup_done', true, false );
		}
	}

	/**
	 * Get email reports frequency setting.
	 *
	 * @since 1.6.2
	 * @return string Frequency setting ('weekly' or 'monthly').
	 */
	private function get_email_reports_frequency() {
		$utils    = Utils::get_instance();
		$settings = $utils->get_settings();
		return $settings['frequency'] ?? 'weekly';
	}

	/**
	 * Check if WP-Cron is available.
	 *
	 * @since 1.6.0
	 * @return bool True if cron is available, false otherwise.
	 */
	private function is_cron_available() {
		// Check if DISABLE_WP_CRON is defined and true.
		if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
			return false;
		}

		// Check if cron test has been performed.
		$cron_test = get_option( 'surerank_cron_test_ok' );
		if ( false !== $cron_test ) {
			return (bool) $cron_test;
		}

		return true; // Assume cron is available by default.
	}

	/**
	 * Fallback to send email via init action if cron is not available.
	 *
	 * @since 1.6.0
	 * @return void
	 */
	public function maybe_send_email_fallback() {
		// Only run if cron is not available.
		if ( $this->is_cron_available() ) {
			return;
		}

		// Use the same logic as the cron job.
		$this->send_email_report();
	}

	/**
	 * Send the email report.
	 * This method is called daily by cron and checks if today is the day to send.
	 *
	 * @since 1.6.2
	 * @return void
	 */
	public function send_email_report() {
		$utils    = Utils::get_instance();
		$settings = $utils->get_settings();

		// Validate settings.
		if ( empty( $settings['enabled'] ) || empty( $settings['recipientEmail'] ) ) {
			return;
		}

		// Check if Google Search Console is authenticated and site is selected.
		if ( ! $this->is_gsc_ready() ) {
			return;
		}

		// Check if today is the scheduled day/date.
		if ( ! $this->should_send_today( $settings ) ) {
			return;
		}

		// Check if we already sent today.
		$last_sent = get_option( 'surerank_email_report_last_sent' );
		$today     = gmdate( 'Y-m-d' );
		if ( $last_sent === $today ) {
			return;
		}

		// Send email to configured recipient.
		$sent = $this->send_email_to( $settings['recipientEmail'] );

		if ( $sent ) {
			update_option( 'surerank_email_report_last_sent', gmdate( 'Y-m-d' ), false );
		}
	}

	/**
	 * Check if email should be sent today based on frequency and schedule settings.
	 *
	 * @since 1.6.0
	 * @param array<string, mixed> $settings Email reports settings.
	 * @return bool True if should send today, false otherwise.
	 */
	private function should_send_today( $settings ) {
		$frequency = $this->get_email_reports_frequency();

		if ( 'monthly' === $frequency ) {
			// For monthly, check if today's date matches the scheduled date.
			$current_date   = (int) gmdate( 'd' );
			$scheduled_date = (int) ( $settings['monthlyDate'] ?? 1 );

			// Handle edge case: if scheduled date is beyond current month's days (e.g., 31st in February).
			$days_in_month = (int) gmdate( 't' );
			if ( $scheduled_date > $days_in_month ) {
				// Send on the last day of the month instead.
				return $current_date === $days_in_month;
			}

			return $current_date === $scheduled_date;
		} else {
			// For weekly, check if today's day matches the scheduled day.
			$current_day   = strtolower( gmdate( 'l' ) );
			$scheduled_day = $settings['scheduledOn'] ?? 'sunday';
			return $current_day === $scheduled_day;
		}
	}

	/**
	 * Send email report to a specific recipient.
	 *
	 * @since 1.6.0
	 * @param string $recipient_email The recipient email address.
	 * @return bool True if email was sent successfully, false otherwise.
	 */
	public function send_email_to( $recipient_email ) {
		// Validate email.
		if ( empty( $recipient_email ) || ! is_email( $recipient_email ) ) {
			return false;
		}

		$subject = $this->get_email_subject();
		$message = $this->get_email_template();
		$headers = $this->get_email_headers();

		// Send the email.
        // @phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_mail_wp_mail
		return wp_mail( $recipient_email, $subject, $message, $headers );
	}

	/**
	 * Get email subject.
	 *
	 * @since 1.6.0
	 * @return string Email subject.
	 */
	public function get_email_subject() {
		$frequency = $this->get_email_reports_frequency();
		$end_date  = gmdate( 'M j, Y', strtotime( '-2 days' ) );

		if ( 'monthly' === $frequency ) {
			$start_date = gmdate( 'M j, Y', strtotime( '-32 days' ) );
			/* translators: 1: Start date, 2: End date */
			return sprintf( __( 'SEO Summary of last month - %1$s to %2$s', 'surerank' ), $start_date, $end_date );
		} else {
			$start_date = gmdate( 'M j, Y', strtotime( '-9 days' ) );
			/* translators: 1: Start date, 2: End date */
			return sprintf( __( 'SEO Summary of last week - %1$s to %2$s', 'surerank' ), $start_date, $end_date );
		}
	}

	/**
	 * Get email headers.
	 *
	 * @since 1.6.0
	 * @return array<string> Email headers.
	 */
	public function get_email_headers() {
		return [
			'Content-Type: text/html; charset=UTF-8',
			'From: ' . get_bloginfo( 'name' ) . ' <' . get_option( 'admin_email' ) . '>',
		];
	}

	/**
	 * Get email template with inline styles.
	 *
	 * @since 1.6.0
	 * @return string HTML email template.
	 */
	public function get_email_template() {
		$site_url         = GSC_Auth::get_instance()->get_credentials( 'site_url' );
		$admin_url        = admin_url( 'admin.php?page=surerank#/search-console' );
		$default_logo_url = 'https://surerank.com/wp-content/uploads/2025/11/surerank-logo.png';
		$frequency        = $this->get_email_reports_frequency();
		$end_date         = gmdate( 'jS F Y', strtotime( '-2 days' ) );
		$start_date       = 'monthly' === $frequency ? gmdate( 'jS F Y', strtotime( '-32 days' ) ) : gmdate( 'jS F Y', strtotime( '-9 days' ) );
		$date_range       = $start_date . ' to ' . $end_date;
		$gsc_data         = $this->get_gsc_data();

		$logo_url    = apply_filters( 'surerank_email_reports_brand_logo', $default_logo_url );
		$logo_url    = $this->validate_logo_url( $logo_url, $default_logo_url );
		$brand_color = apply_filters( 'surerank_email_reports_brand_color', '#4330D2' );
		ob_start();
		?>
		<!DOCTYPE html>
		<html lang="en">
		<head>
			<meta charset="UTF-8">
			<meta name="viewport" content="width=device-width, initial-scale=1.0">
			<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
			<meta http-equiv="X-UA-Compatible" content="IE=edge">
			<title><?php echo esc_html( $this->get_email_subject() ); ?></title>
			<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600&display=swap" rel="stylesheet">
			<style type="text/css">
				@media screen and (max-width: 840px) {
					.metric-cell-left,
					.metric-cell-right,
					.metric-cell-center {
						display: block;
						width: 100% !important;
						padding: 0 !important;
						margin-bottom: 1rem;
					}
				}
			</style>
		</head>
		<body style="font-family: 'Figtree', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; margin: 0; padding: 24px; background-color: #FFFFFF;">
			<table role="presentation" style="width: 100%; max-width: 700px; margin: 0 auto; border-collapse: collapse; background-color: #FFFFFF; padding: 24px;">
				<tr>
					<td>
						<?php echo $this->get_email_header( $logo_url, $site_url, $date_range, $brand_color ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<?php echo $this->get_email_body( $gsc_data, $admin_url, $brand_color ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<?php echo $this->get_email_promotion(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
						<?php echo $this->get_email_footer( $logo_url ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</td>
				</tr>
			</table>
		</body>
		</html>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get email header section.
	 *
	 * @since 1.6.0
	 * @param string $logo_url Logo URL.
	 * @param string $site_url Site URL.
	 * @param string $date_range Date range text.
	 * @param string $brand_color Brand color hex code.
	 * @return string Header HTML.
	 */
	private function get_email_header( $logo_url, $site_url, $date_range, $brand_color ) {
		// Convert brand color to lighter background (add opacity/lightness).

		$header_bg = apply_filters( 'surerank_email_reports_lighten_color', '#EEF2FF', $brand_color, 90 );

		// Get contrasting text color for the site URL against the header background.
		$site_url_color = apply_filters( 'surerank_email_reports_site_url_color', $brand_color, $header_bg );

		// Get logo style (allows Pro to customize based on custom vs default logo).
		// Default to original fixed dimensions for SureRank logo.
		$default_logo_style = 'display: block; width: 139px; height: 22px; margin: 0 auto 16px auto;';
		$logo_style         = apply_filters( 'surerank_email_reports_header_logo_style', $default_logo_style, $logo_url );

		$frequency = $this->get_email_reports_frequency();
		$period    = 'monthly' === $frequency ? __( 'Monthly', 'surerank' ) : __( 'Weekly', 'surerank' );

		$site_name = get_bloginfo( 'name' );
		/* translators: %s: Site name */
		$logo_alt = sprintf( __( '%s Logo', 'surerank' ), $site_name );
		ob_start();
		?>
		<table role="presentation" style="width: 100%; max-width: 660px; margin: 0 auto; border-collapse: collapse; background-color: <?php echo esc_attr( $header_bg ); ?>; border-radius: 8px; padding: 20px 24px;">
			<tr>
				<td style="text-align: center; background: <?php echo esc_attr( $header_bg ); ?>; padding: 20px 24px;">
					<img src="<?php echo esc_url( $logo_url ); ?>" alt="<?php echo esc_attr( $logo_alt ); ?>" style="<?php echo esc_attr( $logo_style ); ?>" width="139" height="22" />
					<h1 style="margin: 0 0 0 0; font-size: 24px; font-weight: 600; line-height: 1.33; letter-spacing: -0.006em; color: #0A0A0A;">
						<?php
						/* translators: %s: Frequency (Weekly or Monthly) */
						echo esc_html( sprintf( __( 'SEO %s Summary of Your Website', 'surerank' ), $period ) );
						?>
					</h1>
					<p class="site-url" style="margin: 12px 0 12px 0; font-size: 14px; line-height: 1.43; color: <?php echo esc_attr( $site_url_color ); ?>; font-weight: 400; text-decoration: none;"><?php echo esc_url( $site_url ); ?></p>
					<p style="margin: 0; font-size: 14px; line-height: 1.43; color: #4B5563; font-weight: 500;"><?php echo esc_html( $date_range ); ?></p>
				</td>
			</tr>
		</table>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get email body section.
	 *
	 * @since 1.6.0
	 * @param array<string, mixed> $gsc_data GSC performance data.
	 * @param string               $admin_url Admin URL.
	 * @param string               $brand_color Brand color hex code.
	 * @return string Body HTML.
	 */
	private function get_email_body( $gsc_data, $admin_url, $brand_color ) {
		$frequency = $this->get_email_reports_frequency();
		$period    = 'monthly' === $frequency ? __( 'Last 30 Days', 'surerank' ) : __( 'Last 7 Days', 'surerank' );

		ob_start();
		?>
		<table role="presentation" style="width: 100%; max-width: 660px; margin: 32px auto 0; border-collapse: collapse;">
			<tr>
				<td>
					<div style="margin-bottom: 20px;">
						<h2 style="margin: 0; font-size: 16px; font-weight: 600; line-height: 1.5; color: #111827;">
							<?php
							/* translators: %s: Period (Last 7 Days or Last 30 Days) */
							echo esc_html( sprintf( __( 'Your Website Performance (%s)', 'surerank' ), $period ) );
							?>
						</h2>
					</div>
					<?php echo $this->get_metrics_section( $gsc_data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					<?php echo $this->get_top_pages_section( $gsc_data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					<?php echo $this->get_cta_section( $admin_url, $brand_color ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				</td>
			</tr>
		</table>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get metrics section.
	 *
	 * @since 1.6.0
	 * @param array<string, mixed> $gsc_data GSC performance data.
	 * @return string|string Metrics HTML.
	 */
	private function get_metrics_section( $gsc_data ) {
		ob_start();
		?>
		<table role="presentation" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
			<tr>
				<?php echo $this->get_metric_card( __( 'Site Clicks', 'surerank' ), $gsc_data['clicks'], 'left' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<?php echo $this->get_metric_card( __( 'Total Impressions', 'surerank' ), $gsc_data['impressions'], 'center' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<?php echo $this->get_metric_card( __( 'Average Position', 'surerank' ), $gsc_data['avg_position'], 'right' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
			</tr>
		</table>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get individual metric card.
	 *
	 * @since 1.6.0
	 * @param string               $label Metric label.
	 * @param array<string, mixed> $data Metric data.
	 * @param string               $position Card position (left, center, right).
	 * @return string Metric card HTML.
	 */
	private function get_metric_card( $label, $data, $position = 'left' ) {
		$padding_style     = '';
		$metric_cell_class = 'metric-cell-' . $position;
		if ( 'left' === $position ) {
			$padding_style = 'width: 33.33%; padding-right: 10px; vertical-align: top;';
		} elseif ( 'center' === $position ) {
			$padding_style = 'width: 33.33%; padding: 1px 5px; vertical-align: top;';
		} else {
			$padding_style = 'width: 33.33%; padding-left: 10px; vertical-align: top;';
		}
		$value      = Utils::format_number( $data['current'] );
		$percentage = $data['percentage'] ?? 0;

		ob_start();
		?>
		<td class="<?php echo esc_attr( $metric_cell_class ); ?>" style="<?php echo esc_attr( $padding_style ); ?>">
			<div style="padding: 16px; background: #F9FAFB; border-bottom: 2px solid #0000000D; border-radius: 6px;">
				<p style="padding: 4px; margin: 0 0 16px 0; font-size: 16px; font-weight: 500; line-height: 1.5; color: #111827;"><?php echo esc_html( $label ); ?></p>
				<div style="display: flex; align-items: center; justify-content: space-between; padding: 4px;">
					<h3 style="display: inline-block; margin: 0; font-size: 24px; font-weight: 600; line-height: 1.33; letter-spacing: -0.006em; color: #111827;"><?php echo esc_html( $value ); ?></h3>
					<?php if ( $percentage != 0 ) : ?>
						<?php
						$change_color = $percentage > 0 ? '#16A34A' : '#DC2626';
						$arrow        = $percentage > 0 ? '↑' : '↓';
						?>
						<div style="display: inline-flex; align-items: center; margin-top: 4px; margin-left: auto;">
							<span style="font-size: 14px; font-weight: 500; line-height: 1.43; color: <?php echo esc_attr( $change_color ); ?>;"><?php echo esc_html( $arrow ) . ' ' . esc_html( (string) abs( $percentage ) ); ?>%</span>
						</div>
					<?php endif; ?>
				</div>
			</div>
		</td>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get top pages section.
	 *
	 * @since 1.6.0
	 * @param array<string, mixed> $gsc_data GSC performance data.
	 * @return string Top pages HTML.
	 */
	private function get_top_pages_section( $gsc_data ) {
		if ( empty( $gsc_data['top_pages'] ) ) {
			return '';
		}

		$total_pages = count( $gsc_data['top_pages'] );

		ob_start();
		?>
		<div style="border-radius: 8px; padding: 12px; margin-bottom: 20px;">
			<h3 style="margin: 0 0 12px 0; font-size: 16px; font-weight: 600; line-height: 1.5; color: #111827;"><?php esc_html_e( 'Top Ranked Posts', 'surerank' ); ?></h3>
			<table role="presentation" style="width: 100%; border-collapse: separate; border-spacing: 0; background-color: #FFFFFF; border: 1px solid #E5E7EB; border-radius: 8px; overflow: hidden;">
				<thead>
					<tr style="background-color: #F9FAFB;">
						<th style="padding: 8px 12px; text-align: left; font-size: 14px; font-weight: 500; line-height: 1.43; color: #111827; border-bottom: 1px solid #E5E7EB; border-top-left-radius: 8px;"><?php esc_html_e( 'Posts', 'surerank' ); ?></th>
						<th style="padding: 8px 12px; text-align: left; font-size: 14px; font-weight: 500; line-height: 1.43; color: #111827; border-bottom: 1px solid #E5E7EB;"><?php esc_html_e( 'Clicks', 'surerank' ); ?></th>
						<th style="padding: 8px 12px; text-align: left; font-size: 14px; font-weight: 500; line-height: 1.43; color: #111827; border-bottom: 1px solid #E5E7EB;"><?php esc_html_e( 'Avg. Position', 'surerank' ); ?></th>
						<th style="padding: 8px 12px; text-align: left; font-size: 14px; font-weight: 500; line-height: 1.43; color: #111827; border-bottom: 1px solid #E5E7EB; border-top-right-radius: 8px;"><?php esc_html_e( 'Impressions', 'surerank' ); ?></th>
					</tr>
				</thead>
				<tbody>
					<?php foreach ( $gsc_data['top_pages'] as $index => $page ) : ?>
						<?php
						$page_url     = $page['url'] ?? '';
						$page_title   = $this->get_page_title_from_url( $page_url );
						$border_style = $index < $total_pages - 1 ? 'border-bottom: 1px solid #E5E7EB;' : '';
						?>
						<tr>
							<td style="padding: 12px; font-size: 14px; font-weight: 500; line-height: 1.43; color: #4B5563; <?php echo esc_attr( $border_style ); ?>"><?php echo esc_html( $page_title ); ?></td>
							<td style="padding: 12px; font-size: 12px; font-weight: 500; line-height: 1.33; color: #4B5563; <?php echo esc_attr( $border_style ); ?>"><?php echo esc_html( Utils::format_number( $page['clicks'] ?? 0 ) ); ?></td>
							<td style="padding: 12px; font-size: 12px; font-weight: 500; line-height: 1.33; color: #4B5563; <?php echo esc_attr( $border_style ); ?>"><?php echo esc_html( (string) round( $page['position'] ?? 0, 1 ) ); ?></td>
							<td style="padding: 12px; font-size: 12px; font-weight: 500; line-height: 1.33; color: #4B5563; <?php echo esc_attr( $border_style ); ?>"><?php echo esc_html( Utils::format_number( $page['impressions'] ?? 0 ) ); ?></td>
						</tr>
					<?php endforeach; ?>
				</tbody>
			</table>
		</div>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get CTA section.
	 *
	 * @since 1.6.0
	 * @param string $admin_url Admin URL.
	 * @param string $brand_color Brand color hex code.
	 * @return string CTA HTML.
	 */
	private function get_cta_section( $admin_url, $brand_color ) {

		$text_color = apply_filters( 'surerank_email_reports_contrasting_text_color', '#FFFFFF', $brand_color );

		$border_color = ( strtoupper( $brand_color ) === '#FFFFFF' ) ? '#000000' : $brand_color;

		ob_start();
		?>
		<div style="text-align: center;">
			<a href="<?php echo esc_url( $admin_url ); ?>" style="display: inline-block; padding: 10px 16px; background-color: <?php echo esc_attr( $brand_color ); ?>; color: <?php echo esc_attr( $text_color ); ?>; border: 1px solid <?php echo esc_attr( $border_color ); ?>; border-radius: 6px; font-size: 14px; font-weight: 600; line-height: 1.43; text-decoration: none;"><?php esc_html_e( 'View Full Analysis', 'surerank' ); ?></a>
		</div>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * All product promotions.
	 *
	 * @since 1.6.0
	 * @return array<string, array<string, string>> Product promotions.
	 */
	private function get_product_promotions() {
		return [
			'suredash'   => [
				'title'         => __( 'Want to see SEO + traffic + sales in one dashboard?', 'surerank' ),
				'text'          => __( 'See the data that matters, all in one clean, customizable dashboard. Track your site\'s traffic, sales, and form activity without the clutter.', 'surerank' ),
				'link_url'      => 'https://suredash.com',
				'link_text'     => __( 'Explore', 'surerank' ) . ' SureDash →',
				'logo_url'      => 'https://suremails.com/wp-content/uploads/2025/10/suredash.png',
				'product_title' => 'SureDash',
			],
			'surecookie' => [
				'title'         => __( 'Make your site GDPR & CCPA ready in minutes', 'surerank' ),
				'text'          => __( 'SureCookie adds a clean, customizable cookie consent banner with automatic cookie scanning and consent logging, so your visitors stay informed and your site stays compliant.', 'surerank' ),
				'link_url'      => 'https://surecookie.com',
				'link_text'     => __( 'Explore', 'surerank' ) . ' SureCookie →',
				'logo_url'      => 'https://ps.w.org/surecookie/assets/icon-256x256.gif',
				'product_title' => 'SureCookie',
			],
			'sureforms'  => [
				'title'         => __( 'Build modern forms without the complexity', 'surerank' ),
				'text'          => __( 'SureForms is a simple yet powerful way to create modern, responsive forms for your website, no code required.', 'surerank' ),
				'link_url'      => 'https://sureforms.com',
				'link_text'     => __( 'Explore', 'surerank' ) . ' SureForms →',
				'logo_url'      => 'https://ps.w.org/sureforms/assets/icon-256x256.gif',
				'product_title' => 'SureForms',
			],
			'suremails'  => [
				'title'         => __( 'Stop your WordPress emails from landing in spam', 'surerank' ),
				'text'          => __( 'SureMail connects your site to trusted SMTP services so your emails reliably reach inboxes, no more lost messages or frustrated customers.', 'surerank' ),
				'link_url'      => 'https://suremails.com',
				'link_text'     => __( 'Explore', 'surerank' ) . ' SureMail →',
				'logo_url'      => 'https://ps.w.org/suremails/assets/icon-256x256.gif',
				'product_title' => 'SureMail',
			],
		];
	}

	/**
	 * Get email promotion section.
	 *
	 * @since 1.6.0
	 *
	 * @return string Promotion HTML.
	 */
	private function get_email_promotion() {
		// Don't show promotions if Pro is active - user has already paid.
		$nudges_utils = \SureRank\Inc\Modules\Nudges\Utils::get_instance();
		if ( $nudges_utils->is_pro_active() ) {
			return '';
		}

		$all_promotions = $this->get_product_promotions();
		if ( empty( $all_promotions ) ) {
			return '';
		}

		// Randomly promote one product per email so every product gets exposure.
		$promotion_key = array_rand( $all_promotions );
		$product       = $all_promotions[ $promotion_key ];
		$title         = $product['title'];
		$text          = $product['text'];
		$link_url      = $product['link_url'];
		$report_link   = add_query_arg(
			[
				'utm_source'   => 'surerank_plugin',
				'utm_medium'   => 'wordpress_plugin',
				'utm_campaign' => 'core_plugin',
				'utm_content'  => 'surerank_seo_weekly_report',
			],
			$link_url
		);
		$link_text     = $product['link_text'];
		$logo_url      = $product['logo_url'];
		$product_title = $product['product_title'];

		ob_start();
		?>
		<div style="height: 1px; background-color: #E5E7EB; margin: 32px 0;"></div>
		<div style="background-color: #EEF2FF; padding: 20px 24px; text-align: left;">
			<img src="<?php echo esc_url( $logo_url ); ?>" alt="<?php echo esc_attr( $product_title ); ?>" style="text-align: left;" width="20" height="20" />
			<h3 style="margin: 0 0 2px 0; font-size: 14px; font-weight: 600; line-height: 1.43; color: #141338;"><?php echo esc_html( $title ); ?></h3>
			<p style="margin: 0 0 4px 0; font-size: 12px; font-weight: 400; line-height: 1.33; color: #4F4E7C;"><?php echo esc_html( $text ); ?></p>
			<a href="<?php echo esc_url( $report_link ); ?>" style="display: inline-block; padding: 0; color: #4338CA; font-size: 12px; font-weight: 600; line-height: 1.33; text-decoration: none; border-radius: 4px;"><?php echo esc_html( $link_text ); ?></a>
		</div>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get email footer section.
	 *
	 * @since 1.6.0
	 * @param string $logo_url Logo URL.
	 * @return string Footer HTML.
	 */
	private function get_email_footer( $logo_url ) {
		// Get logo style (allows Pro to customize based on custom vs default logo).
		// Default to original fixed height for SureRank logo.
		$default_logo_style = 'display: inline-block; width: auto; height: 16px;';
		$logo_style         = apply_filters( 'surerank_email_reports_footer_logo_style', $default_logo_style, $logo_url );

		$site_name = get_bloginfo( 'name' );
		/* translators: %s: Site name */
		$logo_alt = sprintf( __( '%s Logo', 'surerank' ), $site_name );
		ob_start();
		?>
		<div style="height: 1px; width: 80%; background-color: #E5E7EB; margin: 32px auto;"></div>
		<div style="text-align: center;">
			<img src="<?php echo esc_url( $logo_url ); ?>" alt="<?php echo esc_attr( $logo_alt ); ?>" style="<?php echo esc_attr( $logo_style ); ?>" width="auto" height="16" />
		</div>
		<?php
		$content = ob_get_clean();
		return $content === false ? '' : $content;
	}

	/**
	 * Get GSC performance data for email report.
	 *
	 * @since 1.6.0
	 * @return array<string, mixed> GSC performance data.
	 */
	private function get_gsc_data() {
		try {
			$gsc_controller = GSC_Controller::get_instance();
			$frequency      = $this->get_email_reports_frequency();
			$end_date       = gmdate( 'Y-m-d', strtotime( '-2 days' ) );
			$start_date     = 'monthly' === $frequency
				? gmdate( 'Y-m-d', strtotime( '-32 days' ) )
				: gmdate( 'Y-m-d', strtotime( '-9 days' ) );

			// Get clicks and impressions data using the new method.
			$clicks_impressions_data = $gsc_controller->get_clicks_and_impressions_data( $start_date, $end_date );
			$clicks_data             = $clicks_impressions_data['data']['clicks'] ?? [
				'current'    => 0,
				'percentage' => 0,
			];
			$impressions_data        = $clicks_impressions_data['data']['impressions'] ?? [
				'current'    => 0,
				'percentage' => 0,
			];

			// Get content performance (top pages) - this includes position data for current and previous periods.
			$content_performance_response = $gsc_controller->get_content_performance_data( $start_date, $end_date );
			$content_performance_data     = $content_performance_response['data'] ?? [];

			// Calculate current and previous average positions from content performance data.
			$current_avg_position  = 0;
			$previous_avg_position = 0;
			$position_count        = 0;

			foreach ( $content_performance_data as $page ) {
				if ( isset( $page['current']['position'] ) && $page['current']['position'] > 0 ) {
					$current_avg_position += $page['current']['position'];
					++$position_count;
				}
			}

			// Calculate previous period average position.
			// Note: We need to calculate previous position from the changes data.
			$previous_position_count = 0;
			foreach ( $content_performance_data as $page ) {
				if ( isset( $page['current']['position'] ) && $page['current']['position'] > 0 && isset( $page['changes']['position'] ) ) {
					// Calculate previous position from current and percentage change.
					$current_pos            = $page['current']['position'];
					$change_pct             = $page['changes']['position'];
					$previous_pos           = $change_pct != 0 ? $current_pos / ( 1 + ( $change_pct / 100 ) ) : $current_pos;
					$previous_avg_position += $previous_pos;
					++$previous_position_count;
				}
			}

			$current_avg_position  = $position_count > 0 ? round( $current_avg_position / $position_count, 1 ) : 0;
			$previous_avg_position = $previous_position_count > 0 ? round( $previous_avg_position / $previous_position_count, 1 ) : 0;

			// Calculate percentage change for average position.
			// Note: For position, lower is better, so we invert the calculation.
			$position_percentage = 0;
			if ( $previous_avg_position > 0 ) {
				$position_percentage = round( ( ( $previous_avg_position - $current_avg_position ) / $previous_avg_position ) * 100, 2 );
			} elseif ( $current_avg_position > 0 ) {
				$position_percentage = -100;
			}

			// Get top 3 pages and format them for the email.
			$top_pages = array_slice( $content_performance_data, 0, 3 );
			$top_pages = array_map(
				function( $page ) {
					return [
						'url'         => $page['url'] ?? '',
						'clicks'      => $page['current']['clicks'] ?? 0,
						'impressions' => $page['current']['impressions'] ?? 0,
						'position'    => $page['current']['position'] ?? 0,
					];
				},
				$top_pages
			);

			return [
				'clicks'       => $clicks_data,
				'impressions'  => $impressions_data,
				'avg_position' => [
					'current'    => $current_avg_position,
					'percentage' => $position_percentage,
				],
				'top_pages'    => $top_pages,
			];
		} catch ( \Exception $e ) {
			// Return default data if GSC fails.
			return $this->get_default_gsc_data();
		}
	}

	/**
	 * Get default GSC data structure.
	 *
	 * @since 1.6.0
	 * @return array<string, mixed> Default GSC data.
	 */
	private function get_default_gsc_data() {
		return [
			'clicks'       => [
				'current'    => 0,
				'percentage' => 0,
			],
			'impressions'  => [
				'current'    => 0,
				'percentage' => 0,
			],
			'avg_position' => [
				'current'    => 0,
				'percentage' => 0,
			],
			'top_pages'    => [],
		];
	}

	/**
	 * Check if Google Search Console is ready (authenticated and site selected).
	 *
	 * @since 1.6.0
	 * @return bool True if GSC is authenticated and site is selected, false otherwise.
	 */
	private function is_gsc_ready() {
		$auth = GSC_Auth::get_instance();

		// Check if user is authenticated.
		if ( ! $auth->auth_check() ) {
			return false;
		}

		// Check if site is selected.
		$site_url = $auth->get_credentials( 'site_url' );
		if ( empty( $site_url ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Get page title from URL.
	 *
	 * Tries to get the post title if available, otherwise returns formatted URL.
	 *
	 * @since 1.6.0
	 * @param string $url Page URL.
	 * @return string Page title or formatted URL.
	 */
	private function get_page_title_from_url( $url ) {
		// Try to get post ID from URL.
		$post_id = url_to_postid( $url ); // @phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid

		// If we found a post, get its title.
		if ( $post_id > 0 ) {
			$title = get_the_title( $post_id );
			if ( ! empty( $title ) ) {
				return $title;
			}
		}

		// Fallback: Format the URL path as a readable title.
		$parsed_path = wp_parse_url( $url, PHP_URL_PATH );
		$page_title  = basename( is_string( $parsed_path ) ? $parsed_path : '' );

		// Clean up the slug to make it readable.
		$page_title = str_replace( [ '-', '_' ], ' ', $page_title );
		$page_title = ucwords( $page_title );

		if ( empty( $page_title ) || $page_title === '/' ) {
			return __( 'Home', 'surerank' );
		}

		return $page_title;
	}

	/**
	 * Validate logo URL and fallback to default if invalid.
	 *
	 * @since 1.6.0
	 * @param string $logo_url Logo URL to validate.
	 * @param string $default_logo_url Default logo URL to use as fallback.
	 * @return string Valid logo URL or default logo URL.
	 */
	public function validate_logo_url( $logo_url, $default_logo_url ) {

		if ( empty( $logo_url ) ) {
			return esc_url( $default_logo_url );
		}

		if ( ! filter_var( $logo_url, FILTER_VALIDATE_URL ) ) {
			return esc_url( $default_logo_url );
		}

		// Get URL path and validate it's a string.
		$url_path = wp_parse_url( $logo_url, PHP_URL_PATH );
		if ( ! is_string( $url_path ) || empty( $url_path ) ) {
			return esc_url( $default_logo_url );
		}

		// Use the existing image extension validator.
		$image_instance = Image::get_instance();
		if ( ! $image_instance->is_valid_image_extension( $logo_url ) ) {
			return esc_url( $default_logo_url );
		}

		return esc_url( $logo_url );
	}

}

Youez - 2016 - github.com/yon3zu
LinuXploit