Current path: home/webdevt/www/wdp/wp-content/plugins/google-site-kit/includes/Core/Email_Reporting/
?? Go up: /home/webdevt/www/wdp/wp-content/plugins/google-site-kit/includes/Core
<?php
/**
* Class Google\Site_Kit\Core\Email_Reporting\Email_Log
*
* @package Google\Site_Kit\Core\Email_Reporting
* @copyright 2025 Google LLC
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
* @link https://sitekit.withgoogle.com
*
* phpcs:disable PHPCS.Commenting.RequireDocTagDescription -- Pre-existing violations; tracked for follow-up cleanup.
*/
namespace Google\Site_Kit\Core\Email_Reporting;
use Google\Site_Kit\Core\User\Email_Reporting_Settings as Reporting_Settings;
use Google\Site_Kit\Core\Util\BC_Functions;
use Google\Site_Kit\Core\Util\Method_Proxy_Trait;
/**
* Registers the internal Email Reporting log storage.
*
* @since 1.166.0
* @access private
* @ignore
*/
final class Email_Log {
use Method_Proxy_Trait;
/**
* Post type slug.
*
* @since 1.166.0
*/
const POST_TYPE = 'googlesitekit_email';
/**
* Report frequency meta key.
*
* @since 1.166.0
*/
const META_REPORT_FREQUENCY = '_report_frequency';
/**
* Batch ID meta key.
*
* @since 1.166.0
*/
const META_BATCH_ID = '_batch_id';
/**
* Maximum length for stored log strings (MySQL utf8mb4 index safety).
*
* @since 1.166.0
*/
const META_STRING_MAX_LENGTH = 191;
/**
* Send attempts meta key.
*
* @since 1.166.0
*/
const META_SEND_ATTEMPTS = '_send_attempts';
/**
* Error details meta key.
*
* @since 1.166.0
*/
const META_ERROR_DETAILS = '_error_details';
/**
* Report reference dates meta key.
*
* @since 1.166.0
*/
const META_REPORT_REFERENCE_DATES = '_report_reference_dates';
/**
* Site ID meta key.
*
* @since 1.172.0
*/
const META_SITE_ID = '_site_id';
/**
* Template type meta key.
*
* @since 1.174.0
*/
const META_TEMPLATE_TYPE = '_template_type';
/**
* Admin notified meta key.
*
* @since 1.175.0
*/
const META_ADMIN_NOTIFIED = '_admin_notified';
/**
* Email log post statuses.
*
* Slugs must stay within the posts table varchar(20) limit.
*
* @since 1.166.0
*/
const STATUS_SENT = 'email_sent';
const STATUS_FAILED = 'email_failed';
const STATUS_SCHEDULED = 'email_scheduled';
/**
* Email template types.
*
* @since 1.174.0
*/
const TEMPLATE_TYPE_EMAIL_REPORT = 'email-report';
const TEMPLATE_TYPE_SUBSCRIBE_SUCCESS = 'subscribe-success';
/**
* Extracts a normalized date range array from an email log post.
*
* @since 1.167.0
*
* @param mixed $email_log Potential email log post.
* @return array|null
*/
public static function get_date_range_from_log( $email_log ) {
$decoded = self::validate_and_decode_email_log( $email_log );
if ( null === $decoded ) {
return null;
}
$normalized = array();
$keys = array(
'startDate' => 'startDate',
'endDate' => 'endDate',
'compareStartDate' => 'compareStartDate',
'compareEndDate' => 'compareEndDate',
);
foreach ( $keys as $key => $alias ) {
if ( ! isset( $decoded[ $key ] ) ) {
continue;
}
$formatted = self::format_reference_date( $decoded[ $key ] );
if ( null !== $formatted ) {
$normalized[ $alias ] = $formatted;
}
}
if ( empty( $normalized['startDate'] ) || empty( $normalized['endDate'] ) ) {
return null;
}
return $normalized;
}
/**
* Validates an email log and returns decoded reference date metadata.
*
* @since 1.167.0
*
* @param mixed $email_log Potential email log post.
* @return array|null Decoded reference date metadata, or null on failure.
*/
protected static function validate_and_decode_email_log( $email_log ) {
if ( ! ( $email_log instanceof \WP_Post ) ) {
return null;
}
if ( self::POST_TYPE !== $email_log->post_type ) {
return null;
}
$raw = get_post_meta( $email_log->ID, self::META_REPORT_REFERENCE_DATES, true );
if ( empty( $raw ) ) {
return null;
}
if ( is_string( $raw ) ) {
$decoded = json_decode( $raw, true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
return null;
}
} elseif ( is_array( $raw ) ) {
$decoded = $raw;
} else {
return null;
}
return $decoded;
}
/**
* Validates and normalizes a reference date value into a UNIX timestamp.
*
* @since 1.167.0
*
* @param mixed $value Date value.
* @return int|null UNIX timestamp or null on failure.
*/
protected static function validate_reference_date( $value ) {
if ( '' === $value || null === $value ) {
return null;
}
if ( is_numeric( $value ) ) {
$timestamp = (int) $value;
} else {
// Parse date strings in site timezone for consistent round-tripping.
$timezone = BC_Functions::wp_timezone();
$date = date_create_immutable( $value, $timezone );
$timestamp = $date ? $date->getTimestamp() : false;
}
if ( empty( $timestamp ) || $timestamp < 0 ) {
return null;
}
return $timestamp;
}
/**
* Formats a timestamp or date string stored in reference date meta.
*
* @since 1.167.0
*
* @param mixed $value Date value.
* @return string|null
*/
protected static function format_reference_date( $value ) {
$timestamp = self::validate_reference_date( $value );
if ( null === $timestamp ) {
return null;
}
if ( function_exists( 'wp_date' ) ) {
$timezone = BC_Functions::wp_timezone();
if ( $timezone ) {
return wp_date( 'Y-m-d', $timestamp, $timezone );
}
}
return gmdate( 'Y-m-d', $timestamp );
}
/**
* Registers functionality through WordPress hooks.
*
* @since 1.166.0
*/
public function register() {
add_action( 'init', $this->get_method_proxy_once( 'register_email_log' ) );
}
/**
* Registers the email log post type, statuses, and meta.
*
* @since 1.166.0
*/
protected function register_email_log() {
$this->register_post_type();
$this->register_post_statuses();
$this->register_post_meta();
}
/**
* Registers the internal email log post type.
*
* @since 1.166.0
*/
protected function register_post_type() {
if ( post_type_exists( self::POST_TYPE ) ) {
return;
}
register_post_type(
self::POST_TYPE,
array(
'public' => false,
'map_meta_cap' => true,
'rewrite' => false,
'query_var' => false,
)
);
}
/**
* Registers internal delivery statuses.
*
* @since 1.166.0
*/
protected function register_post_statuses() {
$statuses = array(
self::STATUS_SENT,
self::STATUS_FAILED,
self::STATUS_SCHEDULED,
);
foreach ( $statuses as $key => $status ) {
register_post_status(
$status,
array(
'public' => false,
'internal' => true,
'exclude_from_search' => true,
'show_in_admin_all_list' => false,
'show_in_admin_status_list' => false,
)
);
}
}
/**
* Registers meta data for the email log post type.
*
* @since 1.166.0
*/
protected function register_post_meta() {
$auth_callback = array( __CLASS__, 'meta_auth_callback' );
register_post_meta(
self::POST_TYPE,
self::META_REPORT_FREQUENCY,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_frequency' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_BATCH_ID,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_batch_id' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_SEND_ATTEMPTS,
array(
'type' => 'integer',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_attempts' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_ERROR_DETAILS,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_error_details' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_REPORT_REFERENCE_DATES,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_reference_dates' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_SITE_ID,
array(
'type' => 'integer',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_site_id' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_TEMPLATE_TYPE,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_template_type' ),
)
);
register_post_meta(
self::POST_TYPE,
self::META_ADMIN_NOTIFIED,
array(
'type' => 'string',
'single' => true,
'auth_callback' => $auth_callback,
'sanitize_callback' => array( __CLASS__, 'sanitize_admin_notified' ),
)
);
}
/**
* Sanitizes the report frequency meta value.
*
* Allows only known scheduling frequencies, normalizing strings to lowercase.
*
* @since 1.166.0
*
* @param mixed $value Meta value.
* @return string Sanitized value.
*/
public static function sanitize_frequency( $value ) {
$allowed = array(
Reporting_Settings::FREQUENCY_WEEKLY,
Reporting_Settings::FREQUENCY_MONTHLY,
Reporting_Settings::FREQUENCY_QUARTERLY,
);
$value = is_string( $value ) ? strtolower( $value ) : '';
return in_array( $value, $allowed, true ) ? $value : '';
}
/**
* Sanitizes the batch ID meta value.
*
* Strips unsafe characters and limits identifier string length so IDs
* remain index-safe in MySQL databases.
*
* @since 1.166.0
*
* @param mixed $value Meta value.
* @return string Sanitized value.
*/
public static function sanitize_batch_id( $value ) {
$value = sanitize_text_field( (string) $value );
return substr( $value, 0, self::META_STRING_MAX_LENGTH );
}
/**
* Sanitizes the send attempts meta value.
*
* @since 1.166.0
*
* @param mixed $value Meta value.
* @return int Sanitized value.
*/
public static function sanitize_attempts( $value ) {
if ( (int) $value < 0 ) {
return 0;
}
return absint( $value );
}
/**
* Sanitizes the error details meta value.
*
* Converts WP_Error instances and other payloads into JSON for storage.
*
* @since 1.166.0
*
* @param mixed $value Meta value.
* @return string Sanitized value.
*/
public static function sanitize_error_details( $value ) {
if ( is_wp_error( $value ) ) {
$value = array(
'errors' => $value->errors,
'error_data' => $value->error_data,
);
}
if ( is_array( $value ) || is_object( $value ) ) {
$encoded = wp_json_encode( $value, JSON_UNESCAPED_UNICODE );
return is_string( $encoded ) ? $encoded : '';
}
if ( is_string( $value ) ) {
// Treat existing JSON strings as-is by checking decode status instead of rebuilding them.
json_decode( $value, true );
if ( json_last_error() === JSON_ERROR_NONE ) {
return $value;
}
$encoded = wp_json_encode(
array(
'message' => $value,
),
JSON_UNESCAPED_UNICODE
);
return is_string( $encoded ) ? $encoded : '';
}
return '';
}
/**
* Sanitizes the report reference dates meta value.
*
* Extracts known timestamps, coercing them to integers before encoding.
*
* @since 1.166.0
*
* @param mixed $value Meta value.
* @return string Sanitized value.
*/
public static function sanitize_reference_dates( $value ) {
if ( ! is_array( $value ) && ! is_object( $value ) ) {
return '';
}
$normalized = self::normalize_reference_dates( (array) $value );
$encoded = wp_json_encode( $normalized, JSON_UNESCAPED_UNICODE );
return is_string( $encoded ) ? $encoded : '';
}
/**
* Sanitizes the site ID meta value.
*
* @since 1.172.0
*
* @param mixed $value Meta value.
* @return int Sanitized site ID.
*/
public static function sanitize_site_id( $value ) {
return absint( $value );
}
/**
* Sanitizes the template type meta value.
*
* @since 1.174.0
*
* @param mixed $value Meta value.
* @return string Sanitized template type.
*/
public static function sanitize_template_type( $value ) {
$value = sanitize_text_field( $value );
$allowed = array(
self::TEMPLATE_TYPE_EMAIL_REPORT,
self::TEMPLATE_TYPE_SUBSCRIBE_SUCCESS,
);
if ( in_array( $value, $allowed, true ) ) {
return $value;
}
return self::TEMPLATE_TYPE_EMAIL_REPORT;
}
/**
* Sanitizes the admin notified meta value.
*
* @since 1.175.0
*
* @param mixed $value Meta value.
* @return string Sanitized value: '1' if truthy, empty string otherwise.
*/
public static function sanitize_admin_notified( $value ) {
return $value ? '1' : '';
}
/**
* Normalizes reference date values into timestamps for storage.
*
* @since 1.170.0
*
* @param array $raw_dates Raw reference date values keyed by meta field.
* @return array Normalized timestamps keyed by meta field.
*/
protected static function normalize_reference_dates( array $raw_dates ) {
$keys = array( 'startDate', 'endDate', 'compareStartDate', 'compareEndDate' );
$normalized = array();
foreach ( $keys as $key ) {
if ( ! isset( $raw_dates[ $key ] ) ) {
if ( 'compareStartDate' === $key || 'compareEndDate' === $key ) {
$normalized[ $key ] = 0;
}
continue;
}
$timestamp = self::normalize_reference_date_value( $raw_dates[ $key ] );
if ( null === $timestamp ) {
if ( 'compareStartDate' === $key || 'compareEndDate' === $key ) {
$normalized[ $key ] = 0;
}
continue;
}
// Store as integer timestamp.
$normalized[ $key ] = (int) $timestamp;
}
return $normalized;
}
/**
* Normalizes a single reference date value into a timestamp.
*
* @since 1.170.0
*
* @param mixed $raw_value Date value.
* @return int|null Normalized timestamp or null when invalid.
*/
protected static function normalize_reference_date_value( $raw_value ) {
if ( is_string( $raw_value ) ) {
$raw_value = trim( $raw_value );
}
if ( '' === $raw_value ) {
return null;
}
if ( is_numeric( $raw_value ) ) {
$timestamp = $raw_value;
} else {
// Parse date strings in the site timezone so the resulting UTC
// timestamp represents the correct calendar day when converted
// back via format_reference_date().
$timezone = BC_Functions::wp_timezone();
$date = date_create_immutable( $raw_value, $timezone );
$timestamp = $date ? $date->getTimestamp() : false;
}
if ( false === $timestamp || $timestamp <= 0 ) {
return null;
}
return $timestamp;
}
/**
* Authorization callback for protected log meta.
*
* Ensures only internal workflows (cron/init) or administrators touch the
* private log metadata so the CPT stays non-public.
*
* @since 1.166.0
*
* @return bool
*/
public static function meta_auth_callback() {
if ( current_user_can( 'manage_options' ) ) {
return true;
}
if ( wp_doing_cron() ) {
return true;
}
if ( doing_action( 'init' ) ) {
return true;
}
return false;
}
}