File: /var/www/html/belairhomeloan.com/wp-content/plugins/wp-ses/classes/Email-Events.php
<?php
/**
* Email tracking class for WP Offload SES
*
* @package WP Offload SES
* @author Delicious Brains
*/
namespace DeliciousBrains\WP_Offload_SES;
use DOMDocument;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WPDB;
/**
* Class Email_Events
*
* @since 1.0.0
*/
class Email_Events {
/**
* The Email_Log class.
*
* @var Email_Log
*/
private $log;
/**
* The table to log email clicks.
*
* @var string
*/
private $clicks_table;
/**
* The email log table.
*
* @var string
*/
private $emails_table;
/**
* The WordPress database class.
*
* @var WPDB
*/
private $database;
/**
* The secret key used for HMAC authentication.
*
* @var string
*/
private $secret_key;
/**
* Constructor for Email_Events class.
*/
public function __construct() {
global $wpdb;
$this->log = new Email_Log();
$this->database = $wpdb;
$this->clicks_table = $this->database->base_prefix . 'oses_clicks';
$this->emails_table = $this->database->base_prefix . 'oses_emails';
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );
}
/**
* Registers the REST routes for click/open tracking.
*/
public function register_rest_routes() {
register_rest_route(
'wp-offload-ses/v1',
'/c/(?P<data>\S+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'update_clicks' ),
'permission_callback' => '__return_true',
)
);
register_rest_route(
'wp-offload-ses/v1',
'/o/(?P<data>\S+)',
array(
'methods' => 'GET',
'callback' => array( $this, 'update_opens' ),
'permission_callback' => '__return_true',
)
);
}
/**
* Add a link to the database.
*
* @param int $email_id The ID of the email to associate it to.
* @param string $link The URL to add.
*
* @return int|bool
*/
public function add_link( $email_id, $link ) {
$result = $this->database->insert(
$this->clicks_table,
array(
'email_id' => $email_id,
'email_click_url' => $link,
)
);
if ( ! $result ) {
return false;
}
return $this->database->insert_id;
}
/**
* Delete links associated with the provided email id.
*
* @param int $email_id The email id to delete links for.
*
* @return int|bool
*/
public function delete_links_by_email( $email_id ) {
return $this->database->delete(
$this->clicks_table,
array(
'email_id' => $email_id,
),
array(
'%d',
)
);
}
/**
* Get the number of clicks & last click date for an email.
*
* @param int $email_id The email ID.
*
* @return array
*/
public function get_email_click_data( $email_id ) {
$query = $this->database->prepare(
"SELECT SUM(email_click_count) AS email_click_count, MAX(email_last_click_date) AS email_last_click_date FROM $this->clicks_table WHERE email_id = %d",
$email_id
);
return $this->database->get_row( $query, ARRAY_A );
}
/**
* Gets the tracking URL for link clicks.
*
* @param int $email_id The ID of the email.
* @param int $email_click_id The ID of the email link.
* @param string $url The original URL.
*
* @return string
*/
private function get_click_tracking_url( $email_id, $email_click_id, $url ) {
// If the URL already has urlencoded elements,
// ensure we rehydrate intact before redirect.
if ( urldecode( $url ) !== $url ) {
$url = base64_encode( $url );
}
$hash = $this->generate_hmac_hash( $email_id . $email_click_id . $url );
$url_string = base64_encode( 'email_id=' . $email_id . '&email_click_id=' . $email_click_id . '&email_click_url=' . urlencode( $url ) . '&hash=' . $hash );
return get_rest_url( null, '/wp-offload-ses/v1/c/' . $url_string );
}
/**
* Gets the URL for tracking email opens.
*
* @param int $email_id The ID of the email.
*
* @return string
*/
private function get_open_tracking_url( $email_id ) {
$hash = $this->generate_hmac_hash( $email_id );
$url_string = base64_encode( 'email_id=' . $email_id . '&hash=' . $hash );
return get_rest_url( null, '/wp-offload-ses/v1/o/' . $url_string );
}
/**
* Filter email content to enable tracking.
*
* @param int $email_id The ID of the email being filtered.
* @param string $content The email content to filter.
*
* @return string
*/
public function filter_email_content( $email_id, $content ) {
/** @var WP_Offload_SES $wp_offload_ses */
global $wp_offload_ses;
$open_tracking = (bool) $wp_offload_ses->settings->get_setting( 'enable-open-tracking' );
$click_tracking = (bool) $wp_offload_ses->settings->get_setting( 'enable-click-tracking' );
if ( $open_tracking ) {
// Add the tracking pixel.
$content .= '<img src="' . $this->get_open_tracking_url( $email_id ) . '" alt="" />';
}
if ( ! $click_tracking ) {
return $content;
}
$dom = new DOMDocument();
$libxml_previous_state = libxml_use_internal_errors( true );
if ( function_exists( 'mb_convert_encoding' ) && function_exists( 'mb_encode_numericentity' ) ) {
// Convert content to UTF-8 encoding, and numerically encode any non-standard HTML entities.
$content = mb_encode_numericentity(
mb_convert_encoding(
$content,
'UTF-8',
get_bloginfo( 'charset' )
),
[ 0x80, 0x10FFFF, 0, ~0 ],
'UTF-8'
);
}
$dom->loadHTML( $content );
$links = $dom->getElementsByTagName( 'a' );
foreach ( $links as $link ) {
$url = $link->getAttribute( 'href' );
if ( '' === $url || '#' === substr( $url, 0, 1 ) ) {
continue;
}
// Store the link in the database.
$email_click_id = $this->add_link( $email_id, $url );
if ( false === $email_click_id ) {
continue;
}
// Change the href to our internal URL.
$link->setAttribute( 'href', $this->get_click_tracking_url( $email_id, $email_click_id, $url ) );
}
$saved = $dom->saveHTML();
if ( false !== $saved ) {
$content = $saved;
}
libxml_clear_errors();
libxml_use_internal_errors( $libxml_previous_state );
return $content;
}
/**
* Updates the click count for the email.
*
* @param WP_REST_Request $request The request object.
*/
public function update_clicks( WP_REST_Request $request ) {
/** @var WP_Offload_SES $wp_offload_ses */
global $wp_offload_ses;
$data = $request['data'];
$data = base64_decode( $data );
$response = new WP_REST_Response();
$args = array();
parse_str( $data, $args );
if ( ! isset( $args['email_id'] ) || ! isset( $args['email_click_id'] ) || ! isset( $args['hash'] ) || ! isset( $args['email_click_url'] ) ) {
return new WP_Error(
'invalid_request',
__( 'The request you made was invalid.', 'wp-offload-ses' ),
array( 'status' => 404 )
);
}
// Verify the HMAC hash matches what we expect.
if (
! $this->verify_hmac_hash(
$args['email_id'] . $args['email_click_id'] . $args['email_click_url'],
$args['hash']
)
) {
return new WP_Error(
'invalid_request',
__( 'The request you made was invalid.', 'wp-offload-ses' ),
array( 'status' => 404 )
);
}
// Log the click. We have to run the query manually to increment the event count.
$time = current_time( 'mysql' );
$query = $this->database->prepare(
"UPDATE {$this->clicks_table} SET email_click_count = email_click_count + 1, email_first_click_date = IFNULL(email_first_click_date, %s), email_last_click_date = %s WHERE email_click_id = %d",
$time,
$time,
$args['email_click_id']
);
$result = $this->database->query( $query );
// Increment the opens if there are no logged opens (due to disabled images).
if ( $wp_offload_ses->settings->get_setting( 'enable-open-tracking', false ) ) {
$query = $this->database->prepare(
"UPDATE {$this->emails_table} SET email_open_count = 1, email_first_open_date = %s, email_last_open_date = %s WHERE email_id = %d AND email_open_count = 0",
$time,
$time,
$args['email_id']
);
$result = $this->database->query( $query );
}
// Did we need to base64 encode the redirect URL before save?
$location = urldecode( $args['email_click_url'] );
$decoded = base64_decode( $location, true );
if ( false !== $decoded && $decoded !== $location ) {
$location = $decoded;
}
// Redirect.
$response->header( 'Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0' );
$response->header( 'Pragma', 'no-cache' );
$response->set_status( 301 );
$response->header( 'Location', $location );
return $response;
}
/**
* Updates the open count for the email.
*
* @param WP_REST_Request $request The request object.
*/
public function update_opens( WP_REST_Request $request ) {
// Get the email ID.
$data = $request['data'];
$data = base64_decode( $data );
$args = array();
parse_str( $data, $args );
if ( ! isset( $args['email_id'] ) || ! isset( $args['hash'] ) ) {
return new WP_Error(
'invalid_request',
__( 'The request you made was invalid.', 'wp-offload-ses' ),
array( 'status' => 404 )
);
}
// Verify the HMAC hash matches what we expect.
if ( ! $this->verify_hmac_hash( $args['email_id'], $args['hash'] ) ) {
return new WP_Error(
'invalid_request',
__( 'The request you made was invalid.', 'wp-offload-ses' ),
array( 'status' => 404 )
);
}
// Log the open.
$time = current_time( 'mysql' );
$query = $this->database->prepare(
"UPDATE {$this->emails_table} SET email_open_count = email_open_count + 1, email_first_open_date = IFNULL(email_first_open_date, %s), email_last_open_date = %s WHERE email_id = %d",
$time,
$time,
$args['email_id']
);
$result = $this->database->query( $query );
// Display the tracking pixel used in the email.
header( 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0' );
header( 'Pragma: no-cache' );
header( 'Content-Type: image/gif' );
echo base64_decode( 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==' ); // phpcs:ignore
exit;
}
/**
* Generates a HMAC hash.
*
* @param string $data The data to be hashed.
*
* @return string
*/
private function generate_hmac_hash( $data ) {
return hash_hmac( 'sha256', $data, $this->get_secret_key(), false );
}
/**
* Compare the HMAC hashes and verify they are the same.
*
* @param string $data The data to store in the correct hash.
* @param string $hash The hash to compare against.
*
* @return bool
*/
private function verify_hmac_hash( $data, $hash ) {
$expected = $this->generate_hmac_hash( $data );
$actual = $hash;
/**
* The `hash_equals()` function only exists on PHP 5.6+,
* but WordPress creates it for us if it doesn't exist.
*/
if ( ! hash_equals( $expected, $actual ) ) {
return false;
}
return true;
}
/**
* Gets the secret key used in HMAC hashes.
*
* @return string
*/
private function get_secret_key() {
if ( ! is_null( $this->secret_key ) ) {
return $this->secret_key;
}
$this->secret_key = get_site_option( 'wposes_tracking_key' );
return $this->secret_key;
}
/**
* Install/update the email events table(s) if necessary.
*/
public function install_tables() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$wpdb->hide_errors();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE {$this->clicks_table} (
`email_click_id` bigint(20) NOT NULL AUTO_INCREMENT,
`email_id` bigint(20) NOT NULL,
`email_click_url` VARCHAR(255),
`email_click_count` INT DEFAULT '0',
`email_first_click_date` datetime,
`email_last_click_date` datetime,
PRIMARY KEY (email_click_id),
INDEX email_id (email_id)
) $charset_collate;";
dbDelta( $sql );
}
}