✏️ 编辑:statistics.php
路径:
/home/forge/kingkrunch.com/wp/wp-content/plugins/newsletter/statistics/statistics.php
大小:28 KB · 修改:2026-09-30 14:36:39 · 权限:0644 · 可写
← 返回目录
👁 查看
⬇ 下载
<?php defined('ABSPATH') || exit; /** * Manages the clicks and opens tracking. */ class NewsletterStatistics extends NewsletterModule { static $instance; const SENT_NONE = 0; const SENT_READ = 1; const SENT_CLICK = 2; const TRACKING_STANDARD = 1; const TRACKING_ANONYMOUS = 2; // Internal variables for the relink callback var $relink_email_id = 0; var $relink_user_id = 0; var $relink_message_id = 0; var $relink_key = ''; var $relink_url = ''; var $relink_url_type = ''; var $relink_type = 0; var $relink_tracking_type = self::TRACKING_STANDARD; /** * @return NewsletterStatistics */ static function instance() { if (self::$instance == null) { self::$instance = new self(); } return self::$instance; } function __construct() { parent::__construct('statistics'); add_action('wp_loaded', [$this, 'hook_wp_loaded']); add_action('rest_api_init', [$this, 'hook_rest_api_init']); } function hook_wp_loaded() { if (defined('DOING_AJAX') && DOING_AJAX) { add_action('wp_ajax_tnptr', [$this, 'tracking']); add_action('wp_ajax_nopriv_tnptr', [$this, 'tracking']); return; // Important to avoid double tracking by the "tracking" method when ajax is activated } $this->tracking(); } function hook_rest_api_init() { // Open tracking route register_rest_route('tnp', '/o/(?P<email_id>[\d]+)/(?P<user_id>-?[\d]+)/(?P<signature>.+).gif', array( 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'open_callback'], 'permission_callback' => '__return_true', )); // Click tracking route register_rest_route('tnp', '/l/(?P<email_id>[\d]+)/(?P<user_id>-?[\d]+)/(?P<url>.+)/(?P<signature>.+)', [ 'methods' => WP_REST_Server::READABLE, 'permission_callback' => '__return_true', 'callback' => [$this, 'link_callback'] ]); } function open_callback($request) { $this->logger->debug('REST open tracking'); $text = $request['email_id'] . '/' . $request['user_id']; // Standard signature $verified = $this->check_signature('o/' . $text, $request['signature']); if (!$verified) { // Old style signature (to be removed) $verified = $this->check_signature($text, $request['signature']); } // Old key signature if (!$verified && !$this->is_old_key_expired()) { $verified = $this->check_old_signature('o/' . $text, $request['signature']); if (!$verified) { $verified = $this->check_old_signature($text, $request['signature']); } } if (!$verified) { $this->logger->debug('Invalid open signature'); //return new WP_Error('signature', '', ['status' => 400]); //wp_send_json_error([], 400); http_response_code(400); die('Invalid signature'); } $email = $this->get_email($request['email_id']); if ($email) { $user_id = intval($request['user_id']); if ($user_id < 0) { $this->register_anonymous_open($email->id, abs($user_id)); } else { $user = $this->get_user($user_id); if ($user) { $this->register_open($email->id, $user->id); } } } // Send an image anyway $this->send_tracking_image(); } function link_callback($request) { $this->logger->debug('REST link tracking'); $url = $this->base64url_decode($request['url']); $email = $this->get_email($request['email_id']); $user_id = intval($request['user_id']); $user = null; if ($user_id > 0) { $user = $this->get_user($user_id); } // Two types of signature for compatibility; can be removed in a year $text = $request['email_id'] . '/' . $request['user_id'] . '/' . $request['url']; // Standard signature $verified = $this->verify_signature('l/' . $text, $request['signature']); if (!$verified) { // Old style signature (to be removed) $verified = $this->verify_signature($text, $request['signature']); } // Try to verify with the old key if (!$verified) { $verified = $this->check_old_signature('l/' . $text, $request['signature']); if (!$verified) { $verified = $this->check_old_signature($text, $request['signature']); } // Manage stats and redirect when the old key signature is valid if ($verified) { // Here when verified with the old key if ($email) { if ($user_id < 0) { $this->register_anonymous_click($email->id, abs($user_id), $url); } else { // Don't set the cookie $this->register_click($email, $user, $url); } } // If the old key grace period is ended, accept only redirects to the site domain if ($this->is_old_key_expired()) { $url = wp_validate_redirect($url, home_url()); } $this->send_redirect($url, $email, $user); } } if (!$verified) { $this->logger->debug('Invalid link signature'); http_response_code(400); die('Invalid signature'); } // Here when verified with the current key if ($email) { if ($user_id < 0) { $this->register_anonymous_click($email->id, abs($user_id), $url); } else { if ($verified === 1) { // Key not expired $this->set_user_cookie($user); } } $this->register_click($email, $user, $url); } // We keep the tracking link active even if the subscriber or the email are now missing... // Could be changed in the future $this->send_redirect($url, $email, $user); } /** * * @param int $email_id * @param int $user_id */ function register_open($email_id, $user_id) { $this->add_open($email_id, $user_id); $this->update_open_value(self::SENT_READ, $user_id, $email_id); $this->reset_stats_time($email_id); $this->update_user_last_activity($user_id); } function register_anonymous_open($email_id, $message_id) { $this->add_anonymous_open($email_id, $message_id); $this->reset_stats_time($email_id); } /** * * @param object $email * @param object $user * @param string $url */ function register_click($email, $user, $url) { $this->logger->debug(__METHOD__); if (!$email) { $this->logger->debug('Invalid email'); return; } if (!$user) { $this->logger->debug('Invalid user'); return; } $ip = $this->process_ip($this->get_remote_ip()); $this->update_user_ip($user, $ip); $this->update_user_last_activity($user); $is_action = $this->is_action_url($url); if ($is_action) { // TODO: register placeholder URLs representing an action // Track a Newsletter action as an email open and not a click $this->update_open_value(self::SENT_READ, $user->id, $email->id, $ip); $this->logger->debug('Click on action link'); } else { //$url = apply_filters('newsletter_pre_save_url', $url, $email, $user); if ($email) { $this->add_click($url, $email->id, $user->id, $ip); $this->update_open_value(self::SENT_CLICK, $user->id, $email->id, $ip); $this->logger->debug('Click registered'); } } $this->reset_stats_time($email->id); } function register_anonymous_click($email_id, $message_id, $url) { $is_action = $this->is_action_url($url); if ($is_action) { // TODO: register placeholder URLs representing an action $this->add_anonymous_open($email_id, $message_id); } else { $this->add_anonymous_click($url, $email_id, $message_id); } $this->reset_stats_time($email_id); } function send_tracking_image() { $this->logger->debug(__METHOD__); header('Content-Type: image/gif', true); echo base64_decode('_R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'); die(); } /** * Filter the URL, redirect and die. * * @param string $url * @param object $email * @param object $user */ function send_redirect($url, $email, $user) { //$this->logger->debug(__METHOD__); header("Referrer-Policy: no-referrer"); header('Location: ' . sanitize_url(apply_filters('newsletter_redirect_url', $url, $email, $user))); die(); } /** * Tracking for standard and ajax tracking URLs */ function tracking() { // Click tracking if (isset($_GET['nltr'])) { //$this->logger->debug('Click tracking'); //$this->logger->debug('Code: ' . $_GET['nltr']); //$this->logger->debug('Decoded: ' . base64_decode($_GET['nltr'])); // Patch for links with ; $parts = explode(';', base64_decode($_GET['nltr'])); // Shifts and pops since the URL can contains ";"... $email_id = (int) array_shift($parts); $user_id = (int) array_shift($parts); $signature = array_pop($parts); $anchor = array_pop($parts); // No more used // The remaining elements are the url splitted when it contains ";" $url = implode(';', $parts); //$this->logger->debug('Email: ' . $email_id . ', User: ' . $user_id . ', Signature: ' . $signature . ', Url: ' . $url . // ', Anchor: ' . $anchor); $verified = $this->verify_signature($email_id . ';' . $user_id . ';' . $url . ';' . $anchor, $signature); // Try the old signature with custom management; all that can be removed in a year (?) if (!$verified) { $this->logger->debug('Not verified by current key'); $verified = $this->check_old_signature($email_id . ';' . $user_id . ';' . $url . ';' . $anchor, $signature); if ($verified) { // Here when verified with the old key $user = $this->get_user($user_id); $email = $this->get_email($email_id); $this->register_click($email, $user, $url); // Don't set the cookie // If the old key grace period is ended, accept only redirects to the site domain if ($this->is_old_key_expired()) { $url = wp_validate_redirect($url, home_url()); } $this->send_redirect($url, $email, $user); } else { $this->logger->debug('Not verified by the old key'); $url = wp_validate_redirect($url, home_url()); $this->send_redirect($url, $email, $user); } } if (!$verified) { $this->logger->debug('Invalid link signature'); http_response_code(400); die('Invalid signature'); } // Here when verified with the current key $user = $this->get_user($user_id); if ($verified === 1) { $this->set_user_cookie($user); } $email = $this->get_email($email_id); //$this->set_email_cookie($email); // Obsolete $this->register_click($email, $user, $url); $this->send_redirect($url, $email, $user); } if (isset($_GET['noti'])) { //$this->logger->debug('Open tracking'); //$this->logger->debug('Code: ' . $_GET['noti']); //$this->logger->debug('Decoded: ' . base64_decode($_GET['noti'])); list($email_id, $user_id, $signature) = explode(';', base64_decode($_GET['noti']), 3); //$this->logger->debug('Email: ' . $email_id . ', User: ' . $user_id . ', Signature: ' . $signature); $email = $this->get_email($email_id); if (!$email) { $this->logger->error('Email not found, stop'); $this->send_tracking_image(); } $user = $this->get_user($user_id); if (!$user) { $this->logger->debug('User not found, stop'); $this->send_tracking_image(); } $verified = false; // Old signature, to be removed if ($email->token) { $verified = md5($email->id . $user->id . $email->token) === $signature; } // The first signature check if for compatibility, it'll be removed $text = $email->id . '/' . $user->id; $verified = $verified || $this->check_signature($text, $signature) !== 0 || $this->check_signature('o/' . $text, $signature) !== 0; // User the old key, if not expired if (!$verified && !$this->is_old_key_expired()) { $verified = $this->check_old_signature($text, $signature) || $this->check_old_signature('o/' . $text, $signature); } if (!$verified) { $this->logger->error('Wrong signature, stop'); $this->send_tracking_image(); } // Should be care about expired signature for open tracking? No... $this->register_open($email->id, $user->id); $this->send_tracking_image(); } } function get_key() { if (defined('NEWSLETTER_RELINK_KEY')) { return NEWSLETTER_RELINK_KEY; } return $this->get_main_option('key'); } function get_old_key() { if (defined('NEWSLETTER_RELINK_KEY')) { return NEWSLETTER_RELINK_KEY; } return $this->get_main_option('old_key'); } function get_key_time() { return (int) $this->get_main_option('key_time'); } function get_signature($text, $duration = 7 * DAY_IN_SECONDS) { // TODO: Add key caching $time = (string) (time() + $duration); return $time . '.' . md5($text . $time . $this->get_key()); } function get_old_signature($text) { // TODO: Add key caching return md5($text . $this->get_old_key()); } function check_signature($text, $signature) { $signature = trim($signature); if (!$signature) { return false; } return $this->get_signature($text) === $signature; } /** * * @param type $text * @param type $signature * @return int 0 - invalid; 1 - valid; 2 - valid but expired or without the timestamp */ function verify_signature($text, $signature) { $signature = trim($signature); if (!$signature) { return 0; } $parts = explode('.', $signature, 2); if (count($parts) === 2) { $time = (int) $parts[0]; $verified = hash_equals(md5($text . $time . $this->get_key()), $parts[1]); if (!$verified) { return 0; } return $time >= time() ? 1 : 2; } else { return (md5($text . $this->get_key()) === $signature) ? 2 : 0; //return $this->check_signature($text, $signature) ? 2 : 0; } } function check_old_signature($text, $signature) { $signature = trim($signature); if (!$signature) { return false; } return $this->get_old_signature($text) === $signature; } function is_old_key_expired() { return $this->get_key_time() < time() - MONTH_IN_SECONDS; } function is_action_url($url) { return strpos($url, '?na=') !== false || strpos($url, '&na=') !== false; } function get_ip() { return $this->process_ip($this->get_remote_ip()); } /** * Reset the timestamp which indicates the specific email stats must be recalculated. * * @global wpdb $wpdb * @param stdClass|int $email */ function reset_stats_time($email) { global $wpdb; $email_id = $this->to_int_id($email); $wpdb->update(NEWSLETTER_EMAILS_TABLE, ['stats_time' => 0], ['id' => $email_id]); } /** * * @param string $text * @param mixed $email * @param mixed $user * @param TNP_Mailer_Message $message * @return string */ function relink($text, $email, $user, $message) { $this->logger->debug(__METHOD__); // Should be checked externally if (empty($email->track)) { return $text; } $this->relink_tracking_type = $email->track; // 1 or 2 // If we have no consent for full subscriber tracking we change to anonymous tracking. // The subscriber cannot control to total tracking removal. if (!$user->track) { $this->relink_tracking_type = self::TRACKING_ANONYMOUS; } $this->relink_email_id = $this->to_int_id($email); $this->relink_user_id = $this->to_int_id($user); $this->relink_message_id = $message->id; // URL format. This will be changed when even the action links can use the REST format // We support only "rest" link rewriting with anonymous tracking if ($this->relink_tracking_type == self::TRACKING_ANONYMOUS) { $this->relink_url_type = 'rest'; } else { $this->relink_url_type = Newsletter::instance()->get_main_option('tracking_links') ?? ''; if (!$this->relink_url_type) { $this->relink_url_type = Newsletter::instance()->get_main_option('links') ?? ''; } } $this->logger->debug('URL type: ' . $this->relink_url_type); if (empty($this->relink_key)) { $this->relink_key = $this->get_key(); } if ($this->relink_url_type === 'ajax') { $this->relink_url = admin_url('admin-ajax.php?action=tnptr&nltr='); } else { $this->relink_url = home_url('/') . '?nltr='; } $text = preg_replace_callback('/(<[aA][^>]+href[\s]*=[\s]*["\'])([^>"\']+)(["\'][^>]*>)(.*?)(<\/[Aa]>)/is', [$this, 'relink_callback'], $text); // Open tracking image if ($this->relink_tracking_type == self::TRACKING_ANONYMOUS) { $uri = 'o/' . $this->relink_email_id . '/-' . $this->relink_message_id; $signature = $this->get_signature($uri, 6 * MONTH_IN_SECONDS); $src = rest_url('tnp/' . $uri . '/' . $signature . '.gif'); $img = '<img width="1" height="1" style="display: none !important; width:1px; height:1px; border:0; outline:none;" alt="" src="' . esc_attr($src) . '">'; $text = str_replace('</body>', "\n" . $img . "\n</body>", $text); } else { $uri = 'o/' . $this->relink_email_id . '/' . $this->relink_user_id; $signature = $this->get_signature($uri, 6 * MONTH_IN_SECONDS); $img1 = ''; if ($this->relink_url_type === 'ajax') { $url = admin_url('admin-ajax.php?action=tnptr¬i=') . rawurlencode(base64_encode($this->relink_email_id . ';' . $this->relink_user_id . ';' . $signature)); $img1 = '<img width="1" height="1" style="display: none !important; width:1px; height:1px; border:0; outline:none;" alt="" src="' . esc_attr($url) . '"/>'; } else { $url = home_url('/') . '?noti=' . rawurlencode(base64_encode($this->relink_email_id . ';' . $this->relink_user_id . ';' . $signature)); $img1 = '<img width="1" height="1" style="display: none !important; width:1px; height:1px; border:0; outline:none;" alt="" src="' . esc_attr($url) . '"/>'; } // New REST tracking (always added) $src = rest_url('tnp/' . $uri . '/' . $signature . '.gif'); $img3 = '<img width="1" height="1" style="display: none !important; width:1px; height:1px; border:0; outline:none;" alt="" src="' . esc_attr($src) . '">'; $text = str_replace('</body>', "\n" . $img1 . "\n" . $img3 . "\n</body>", $text); } return $text; } function can_relink($url) { if (strpos($url, '{') === 0) { return false; } // Do not relink anchors if (substr($url, 0, 1) === '#') { return false; } // Do not relink mailto: if (substr($url, 0, 7) === 'mailto:') { return false; } return true; } /** * $matches[0] contains the full A tag. * * @param array $matches * @return string */ function relink_callback($matches) { $this->logger->debug(__METHOD__); $this->logger->debug('URL: ' . $matches[2]); $href = trim(str_replace('&', '&', $matches[2])); if (!$this->can_relink($href)) { return $matches[0]; } if ($this->relink_url_type === 'rest') { $encoded_href = $this->base64url_encode($href); if ($this->relink_tracking_type == self::TRACKING_STANDARD) { $uri = 'l/' . $this->relink_email_id . '/' . $this->relink_user_id . '/' . $encoded_href; } else { $uri = 'l/' . $this->relink_email_id . '/-' . $this->relink_message_id . '/' . $encoded_href; } $signature = $this->get_signature($uri); $url = rest_url('/tnp/' . $uri . '/' . $signature); $this->logger->debug('Relinked URL: ' . $url); return $matches[1] . $url . $matches[3] . $matches[4] . $matches[5]; } // Anonymous tracking cannot use non-rest URLs $anchor = ''; // No more used $r = $this->relink_email_id . ';' . $this->relink_user_id . ';' . $href . ';' . $anchor; $r = $r . ';' . $this->get_signature($r); $r = base64_encode($r); $r = rawurlencode($r); $url = $this->relink_url . $r; $this->logger->debug('Relinked URL: ' . $url); return $matches[1] . $url . $matches[3] . $matches[4] . $matches[5]; } function update_stats($email) { global $wpdb; $wpdb->query($wpdb->prepare("update " . NEWSLETTER_SENT_TABLE . " s1 join " . $wpdb->prefix . "newsletter_stats s2 on s1.user_id=s2.user_id and s1.email_id=s2.email_id and s1.email_id=%d set s1.open=1, s1.ip=s2.ip", $email->id)); $wpdb->query($wpdb->prepare("update " . NEWSLETTER_SENT_TABLE . " s1 join " . $wpdb->prefix . "newsletter_stats s2 on s1.user_id=s2.user_id and s1.email_id=s2.email_id and s2.url<>'' and s1.email_id=%d set s1.open=2, s1.ip=s2.ip", $email->id)); } /** * Deleted all collected stats for an email. * * @global wpdb $wpdb * @param stdClass|int $email */ function reset_stats($email) { global $wpdb; $email_id = $this->to_int_id($email); $this->query("delete from " . NEWSLETTER_SENT_TABLE . " where email_id=" . $email_id); $this->query("delete from " . NEWSLETTER_STATS_TABLE . " where email_id=" . $email_id); $wpdb->update(NEWSLETTER_EMAILS_TABLE, [ 'sent' => 0, 'error_count' => 0, 'unsub_count' => 0, 'open_count' => 0, 'click_count' => 0, 'stats_time' => 0 ], ['id' => $email_id]); } /** * * @param int $user_id * @param int $email_id * @param string $ip */ function add_open($email_id, $user_id, $ip = null) { $this->add_click('', $email_id, $user_id, $ip); } function add_click($url, $email_id, $user_id, $ip = null) { global $wpdb; $this->logger->debug(__METHOD__); if (is_null($ip)) { $this->logger->debug('IP not provided'); $ip = $this->get_ip(); } if (strlen($url) > 254) { $this->logger->debug('IP longer than 254 characters, trimming'); $url = substr($url, 0, 254); } $url = sanitize_url($url); $this->logger->debug([ 'email_id' => (int) $email_id, 'user_id' => (int) $user_id, 'url' => $url, 'ip' => $ip ]); $this->insert(NEWSLETTER_STATS_TABLE, [ 'email_id' => (int) $email_id, 'user_id' => (int) $user_id, 'url' => $url, 'ip' => $ip ] ); } /** * Update the "open" columns of the sent table. * * @global wpdb $wpdb * @param int $user_id * @param int $email_id */ function add_anonymous_open($email_id, $message_id) { $this->add_anonymous_click('', $email_id, $message_id); } /** * * @global wpdb $wpdb * @param string $url * @param int $email_id * @param int $message_id */ function add_anonymous_click($url, $email_id, $message_id) { global $wpdb; if (strlen($url) > 254) { $url = substr($url, 0, 254); } $url = sanitize_url($url); $this->insert(NEWSLETTER_STATS_TABLE, [ 'email_id' => (int) $email_id, 'message_id' => (int) $message_id, 'url' => $url ]); $this->reset_stats_time($email_id); // $count = $wpdb->get_var($wpdb->prepare('select count(*) from ' . $wpdb->prefix . 'newsletter_tracking where email_id=%d and message_id=%d and url=%s', // $email_id, $message_id, $url)); // // if (!$count) { // $this->insert($wpdb->prefix . 'newsletter_tracking', [ // 'email_id' => $email_id, // 'message_id' => $message_id, // 'url' => $url, // ] // ); // } } function update_open_value($value, $user_id, $email_id, $ip = null) { global $wpdb; if (is_null($ip)) { $ip = $this->get_ip(); } $this->query($wpdb->prepare("update " . NEWSLETTER_SENT_TABLE . " set open=%d, ip=%s where email_id=%d and user_id=%d and open<%d limit 1", $value, $ip, $email_id, $user_id, $value)); } /** For compatibility */ function get_statistics_url($email_id) { $page = apply_filters('newsletter_statistics_view', 'newsletter_statistics_view'); return 'admin.php?page=' . $page . '&id=' . $email_id; } /** For compatibility */ function get_index_url() { $page = apply_filters('newsletter_statistics_index', 'newsletter_statistics_index'); return 'admin.php?page=' . $page; } /** * Used by Automated. * * @deprecated * * @param type $email_id * @return type */ function get_total_count($email_id) { $report = $this->get_statistics($email_id); return $report->total; } function get_statistics($email) { return NewsletterStatisticsAdmin::instance()->get_statistics($email); } /** * Used by Automated. * * @deprecated * * @param type $email_id * @return type */ function get_open_count($email_id) { $report = $this->get_statistics($email_id); return $report->open_count; } /** * Used by Automated. * * @deprecated * * @param type $email_id * @return type */ function get_click_count($email_id) { $report = $this->get_statistics($email_id); return $report->click_count; } function base64url_encode($data) { return rtrim(strtr(base64_encode($data), '+/', '-_'), '='); } function base64url_decode($data) { return base64_decode(strtr($data, '-_', '+/')); } } NewsletterStatistics::instance();
💾 保存
取消
保存为 UTF-8,换行统一为 LF