You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

649 lines
16 KiB
PHP

<?php
/**
* Analytics Email Reports.
*
* @since 1.0.68
* @package RankMath
* @subpackage RankMath\modules
* @author Rank Math <support@rankmath.com>
*/
namespace RankMath\Analytics;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Google\Console;
use RankMath\Admin\Admin_Helper;
use RankMath\Helpers\Param;
defined( 'ABSPATH' ) || exit;
/**
* Email_Reports class.
*/
class Email_Reports {
use Hooker;
/**
* Email content variables.
*
* @var array
*/
private $variables = [];
/**
* Path to the views directory.
*
* @var array
*/
private $views_path = '';
/**
* URL to the assets directory.
*
* @var array
*/
private $assets_url = '';
/**
* Charts Account.
*
* @var string
*/
private $charts_account = 'rankmath';
/**
* Charts Key.
*
* @var string
*/
private $charts_key = '10042B42-9193-428A-ABA7-5753F3370F84';
/**
* Graph data.
*
* @var array
*/
private $graph_data = [];
/**
* Debug mode.
*
* @var boolean
*/
private $debug = false;
/**
* The constructor.
*/
public function __construct() {
if ( ! Console::is_console_connected() ) {
return;
}
$directory = dirname( __FILE__ );
$this->views_path = $directory . '/views/email-reports/';
$url = plugin_dir_url( __FILE__ );
$this->assets_url = $this->do_filter( 'analytics/email_report_assets_url', $url . 'assets/' );
$this->hooks();
}
/**
* Add filter & action hooks.
*
* @return void
*/
public function hooks() {
$this->action( 'rank_math/analytics/email_report_event', 'email_report' );
$this->action( 'template_redirect', 'maybe_debug' );
$this->action( 'rank_math/analytics/email_report_html', 'replace_variables' );
$this->action( 'rank_math/analytics/email_report_html', 'strip_comments' );
}
/**
* Send Analytics report or error message.
*
* @return void
*/
public function email_report() {
$this->setup_variables();
$this->send_report();
}
/**
* Collect variables to be used in the Report template.
*
* @return void
*/
public function setup_variables() {
$stats = $this->get_stats();
$date = $this->get_date();
// Translators: placeholder is "rankmath.com" as a link.
$footer_text = sprintf( esc_html__( 'This email was sent to you as a registered member of %s.', 'rank-math' ), '<a href="###SITE_URL###">###SITE_URL_SIMPLE###</a>' );
$footer_text .= ' ';
// Translators: placeholder is "click here" as a link.
$footer_text .= sprintf( esc_html__( 'To update your email preferences, %s.', 'rank-math' ), '<a href="###SETTINGS_URL###">' . esc_html__( 'click here', 'rank-math' ) . '</a>' );
$footer_text .= '###ADDRESS###';
$this->variables = [
'site_url' => get_home_url(),
'site_url_simple' => explode( '://', get_home_url() )[1],
'settings_url' => Helper::get_admin_url( 'options-general#setting-panel-analytics' ),
'report_url' => Helper::get_admin_url( 'analytics' ),
'assets_url' => $this->assets_url,
'address' => '<br/> [rank_math_contact_info show="address"]',
'logo_link' => KB::get( 'email-reports-logo', 'Email Report Logo' ),
'period_days' => $date['period'],
'start_date' => $date['start'],
'end_date' => $date['end'],
'stats_clicks' => $stats['clicks'],
'stats_clicks_diff' => $stats['clicks_diff'],
'stats_traffic' => $stats['traffic'],
'stats_traffic_diff' => $stats['traffic_diff'],
'stats_impressions' => $stats['impressions'],
'stats_impressions_diff' => $stats['impressions_diff'],
'stats_keywords' => $stats['keywords'],
'stats_keywords_diff' => $stats['keywords_diff'],
'stats_position' => $stats['position'],
'stats_position_diff' => $stats['position_diff'],
'stats_top_3_positions' => $stats['top_3_positions'],
'stats_top_3_positions_diff' => $stats['top_3_positions_diff'],
'stats_top_10_positions' => $stats['top_10_positions'],
'stats_top_10_positions_diff' => $stats['top_10_positions_diff'],
'stats_top_50_positions' => $stats['top_50_positions'],
'stats_top_50_positions_diff' => $stats['top_50_positions_diff'],
'stats_invalid_data' => $stats['invalid_data'],
'footer_html' => $footer_text,
];
$this->variables = $this->do_filter( 'analytics/email_report_variables', $this->variables );
}
/**
* Get date data.
*
* @return array
*/
public function get_date() {
$period = self::get_period_from_frequency();
// Shift 3 days prior.
$subtract = DAY_IN_SECONDS * 3;
$start = strtotime( '-' . $period . ' days' ) - $subtract;
$end = strtotime( $this->do_filter( 'analytics/report_end_date', 'today' ) ) - $subtract;
$start = date_i18n( 'd M Y', $start );
$end = date_i18n( 'd M Y', $end );
return compact( 'start', 'end', 'period' );
}
/**
* Get Analytics stats.
*
* @return array
*/
public function get_stats() {
$period = self::get_period_from_frequency();
$stats = Stats::get();
$stats->set_date_range( "-{$period} days" );
// Basic stats.
$data = (array) $stats->get_analytics_summary();
$analytics = get_option( 'rank_math_google_analytic_options' );
$is_analytics_connected = ! empty( $analytics ) && ! empty( $analytics['view_id'] );
$out = [];
$out['impressions'] = $data['impressions']['total'];
$out['impressions_diff'] = $data['impressions']['difference'];
$out['traffic'] = 0;
$out['traffic_diff'] = 0;
if ( $is_analytics_connected && defined( 'RANK_MATH_PRO_FILE' ) && isset( $data['pageviews'] ) ) {
$out['traffic'] = $data['pageviews']['total'];
$out['traffic_diff'] = $data['pageviews']['difference'];
}
$out['clicks'] = 0;
$out['clicks_diff'] = 0;
if ( ! $is_analytics_connected || ( $is_analytics_connected && ! defined( 'RANK_MATH_PRO_FILE' ) ) ) {
$out['clicks'] = $data['clicks']['total'];
$out['clicks_diff'] = $data['clicks']['difference'];
}
$out['keywords'] = $data['keywords']['total'];
$out['keywords_diff'] = $data['keywords']['difference'];
$out['position'] = $data['position']['total'];
$out['position_diff'] = $data['position']['difference'];
// Keyword stats.
$kw_data = (array) $stats->get_top_keywords();
$out['top_3_positions'] = $kw_data['top3']['total'];
$out['top_3_positions_diff'] = $kw_data['top3']['difference'];
$out['top_10_positions'] = $kw_data['top10']['total'];
$out['top_10_positions_diff'] = $kw_data['top10']['difference'];
$out['top_50_positions'] = $kw_data['top50']['total'];
$out['top_50_positions_diff'] = $kw_data['top50']['difference'];
$out['invalid_data'] = false;
if ( ! count( array_filter( $out ) ) ) {
$out['invalid_data'] = true;
}
return $out;
}
/**
* Get date period (days) from the frequency option.
*
* @param string $frequency Frequency string.
*
* @return string
*/
public static function get_period_from_frequency( $frequency = null ) {
$periods = [
'monthly' => 30,
];
$periods = apply_filters( 'rank_math/analytics/email_report_periods', $periods );
if ( empty( $frequency ) ) {
$frequency = self::get_setting( 'frequency', 'monthly' );
}
if ( isset( $periods[ $frequency ] ) ) {
return absint( $periods[ $frequency ] );
}
return absint( reset( $periods ) );
}
/**
* Send report data.
*
* @return void
*/
public function send_report() {
$account = Admin_Helper::get_registration_data();
$report_email = [
'to' => $account['email'],
'subject' => sprintf(
// Translators: placeholder is the site URL.
__( 'Rank Math [SEO Report] - %s', 'rank-math' ),
explode( '://', get_home_url() )[1]
),
'message' => $this->get_template( 'report' ),
'headers' => 'Content-Type: text/html; charset=UTF-8',
];
/**
* Filter: rank_math/analytics/email_report_parameters
* Filters the report email parameters.
*/
$report_email = $this->do_filter( 'analytics/email_report_parameters', $report_email );
wp_mail(
$report_email['to'],
$report_email['subject'],
$report_email['message'],
$report_email['headers']
);
}
/**
* Get full HTML template for email.
*
* @param string $template Template name.
* @return string
*/
private function get_template( $template ) {
$file = $this->locate_template( $template );
/**
* Filter template file.
*/
$file = $this->do_filter( 'analytics/email_report_template', $file, $template );
if ( ! file_exists( $file ) ) {
return '';
}
ob_start();
include_once $file;
$content = ob_get_clean();
/**
* Filter template HTML.
*/
return $this->do_filter( 'analytics/email_report_html', $content );
}
/**
* Locate and include template part.
*
* @param string $part Template part.
* @param array $args Template arguments.
* @return mixed
*/
private function template_part( $part, $args = [] ) {
$file = $this->locate_template( $part );
/**
* Filter template part.
*/
$file = $this->do_filter( 'analytics/email_report_template_part', $file, $part, $args );
if ( ! file_exists( $file ) ) {
return '';
}
extract( $args, EXTR_SKIP ); // phpcs:ignore
include $file;
}
/**
* Replace variables in content.
*
* @param string $content Email content.
* @param string $recursion Recursion count, to account for double-encoded variables.
* @return string
*/
public function replace_variables( $content, $recursion = 1 ) {
foreach ( $this->variables as $key => $value ) {
if ( ! is_scalar( $value ) ) {
continue;
}
// Variables must be uppercase.
$key = mb_strtoupper( $key );
$content = str_replace( "###$key###", $value, $content );
}
if ( $recursion ) {
$recursion--;
$content = $this->replace_variables( $content, $recursion );
}
return do_shortcode( $content );
}
/**
* Strip HTML & CSS comments.
*
* @param string $content Email content.
* @return string
*/
public function strip_comments( $content ) {
$content = preg_replace( '[(<!--(.*)-->|/\*(.*)\*/)]isU', '', $content );
return $content;
}
/**
* Init debug mode if requested and allowed.
*
* @return void
*/
public function maybe_debug() {
if ( 1 !== absint( Param::get( 'rank_math_analytics_report_preview' ) ) ) {
return;
}
if ( ! Helper::has_cap( 'analytics' ) ) {
return;
}
$send = boolval( Param::get( 'send' ) );
$values = boolval( Param::get( 'values', '1' ) );
$this->debug( $send, $values );
}
/**
* Send or output the report email.
*
* @param boolean $send Send email or output to browser.
* @param boolean $values Replace variables with actual values.
* @return void
*/
private function debug( $send = false, $values = true ) {
$this->debug = true;
if ( $values ) {
$this->setup_variables();
}
if ( $send ) {
// Send it now.
$this->send_report();
$url = remove_query_arg(
[
'rank_math_analytics_report_preview',
'send',
'values',
]
);
Helper::redirect( $url );
exit;
}
// Output it to the browser.
echo $this->get_template( 'report' ); // phpcs:ignore
die();
}
/**
* Variable getter, whenever the value is needed in PHP.
*
* @param string $name Variable name.
* @return mixed
*/
public function get_variable( $name ) {
if ( isset( $this->variables[ $name ] ) ) {
return $this->variables[ $name ];
}
return "###$name###";
}
/**
* Setting getter.
*
* @param string $option Option name.
* @param mixed $default Default value.
* @return mixed
*/
public static function get_setting( $option, $default = false ) {
return Helper::get_settings( 'general.console_email_' . $option, $default );
}
/**
* Output image inside the email template.
*
* @param string $url Image URL.
* @param string $width Image width.
* @param string $height Image height.
* @param string $alt ALT text.
* @param array $attr Additional attributes.
* @return void
*/
public function image( $url, $width = 0, $height = 0, $alt = '', $attr = [] ) {
$atts = $attr;
$atts['border'] = '0';
if ( ! isset( $atts['src'] ) ) {
$atts['src'] = $url;
}
if ( ! isset( $atts['width'] ) && $width ) {
$atts['width'] = $width;
}
if ( ! isset( $atts['height'] ) && $height ) {
$atts['height'] = $height;
}
if ( ! isset( $atts['alt'] ) ) {
$atts['alt'] = $alt;
}
if ( ! isset( $atts['style'] ) ) {
$atts['style'] = 'border: 0; outline: none; text-decoration: none; display: inline-block;';
}
if ( substr( $atts['src'], 0, 4 ) !== 'http' && substr( $atts['src'], 0, 3 ) !== '###' ) {
$atts['src'] = $this->assets_url . 'img/' . $atts['src'];
}
$atts = $this->do_filter( 'analytics/email_report_image_atts', $atts, $url, $width, $height, $alt, $attr );
$attributes = '';
foreach ( $atts as $name => $value ) {
if ( ! empty( $value ) ) {
$value = ( 'src' === $name ) ? esc_url_raw( $value ) : esc_attr( $value );
$attributes .= ' ' . $name . '="' . $value . '"';
}
}
$image = "<img $attributes>";
$image = $this->do_filter( 'analytics/email_report_image_html', $image, $url, $width, $height, $alt, $attr );
echo $image; // phpcs:ignore
}
/**
* Gets template path.
*
* @param string $template_name Template name.
* @param bool $return_full_path Return the full path or not.
* @return string
*/
public function locate_template( $template_name, $return_full_path = true ) {
$default_paths = [ $this->views_path ];
$template_paths = $this->do_filter( 'analytics/email_report_template_paths', $default_paths );
$paths = array_reverse( $template_paths );
$located = '';
$path_partial = '';
foreach ( $paths as $path ) {
if ( file_exists( $full_path = trailingslashit( $path ) . $template_name . '.php' ) ) { // phpcs:ignore
$located = $full_path;
$path_partial = $path;
break;
}
}
return $return_full_path ? $located : $path_partial;
}
/**
* Load all graph data into memory.
*
* @return void
*/
private function load_graph_data() {
$period = self::get_period_from_frequency();
$stats = Stats::get();
$stats->set_date_range( "-{$period} days" );
$this->graph_data = (array) $stats->get_analytics_summary_graph();
}
/**
* Get data points for graph.
*
* @param string $chart Chart to get data for.
* @return array
*/
public function get_graph_data( $chart ) {
if ( empty( $this->graph_data ) ) {
$this->load_graph_data();
}
$data = [];
$group = 'merged';
$prop = $chart;
if ( 'traffic' === $chart ) {
$group = 'traffic';
$prop = 'pageviews';
}
if ( empty( $this->graph_data[ $group ] ) ) {
return $data;
}
foreach ( (array) $this->graph_data[ $group ] as $range_data ) {
$range_data = (array) $range_data;
if ( isset( $range_data[ $prop ] ) ) {
$data[] = $range_data[ $prop ];
}
}
return $data;
}
/**
* Charts API sign request.
*
* @param string $query Query.
* @param string $code Code.
* @return string
*/
private function charts_api_sign( $query, $code ) {
return hash_hmac( 'sha256', $query, $code );
}
/**
* Generate URL for the Charts API image.
*
* @param array $graph_data Graph data points.
* @param int $width Image height.
* @param int $height Image width.
*
* @return string
*/
private function charts_api_url( $graph_data, $width = 192, $height = 102 ) {
$params = [
'chco' => '80ace7',
'chds' => 'a',
'chf' => 'bg,s,f7f9fb',
'chls' => 4,
'chm' => 'B,e2eeff,0,0,0',
'chs' => "{$width}x{$height}",
'cht' => 'ls',
'chd' => 'a:' . join( ',', $graph_data ),
'icac' => $this->charts_account,
];
$query_string = urldecode( http_build_query( $params ) );
$signature = $this->charts_api_sign( $query_string, $this->charts_key );
return 'https://charts.rankmath.com/chart?' . $query_string . '&ichm=' . $signature;
}
/**
* Check if fields should be hidden.
*
* @return bool
*/
public static function are_fields_hidden() {
return apply_filters( 'rank_math/analytics/hide_email_report_options', false );
}
}