| 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 : |
<?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 );
}
}