statistics.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <?php
  2. defined('ABSPATH') || exit;
  3. class NewsletterStatistics extends NewsletterModule {
  4. static $instance;
  5. const SENT_READ = 1;
  6. const SENT_CLICK = 2;
  7. /**
  8. * @return NewsletterStatistics
  9. */
  10. static function instance() {
  11. if (self::$instance == null) {
  12. self::$instance = new NewsletterStatistics();
  13. }
  14. return self::$instance;
  15. }
  16. function __construct() {
  17. parent::__construct('statistics', '1.1.8');
  18. add_action('wp_loaded', array($this, 'hook_wp_loaded'));
  19. if (is_admin()) {
  20. add_action('admin_enqueue_scripts', array($this, 'hook_admin_enqueue_scripts'));
  21. }
  22. }
  23. function hook_admin_enqueue_scripts() {
  24. if (isset($_GET['page']) && (strpos($_GET['page'], 'newsletter_statistics') === 0 || strpos($_GET['page'], 'newsletter_reports') === 0)) {
  25. wp_enqueue_style('newsletter-admin-statistics', plugins_url('newsletter') . '/statistics/css/tnp-statistics.css', array('tnp-admin'), time());
  26. }
  27. }
  28. /**
  29. *
  30. * @global wpdb $wpdb
  31. */
  32. function hook_wp_loaded() {
  33. global $wpdb;
  34. // Newsletter Link Tracking
  35. if (isset($_GET['nltr'])) {
  36. // Patch for links with ;
  37. $parts = explode(';', base64_decode($_GET['nltr']));
  38. $email_id = (int) array_shift($parts);
  39. $user_id = (int) array_shift($parts);
  40. $signature = array_pop($parts);
  41. $anchor = array_pop($parts); // No more used
  42. // The remaining elements are the url splitted when it contains
  43. $url = implode(';', $parts);
  44. if (empty($user_id) || empty($url)) {
  45. header("HTTP/1.0 404 Not Found");
  46. die('Invalid data');
  47. }
  48. $parts = parse_url($url);
  49. $verified = $signature == md5($email_id . ';' . $user_id . ';' . $url . ';' . $anchor . $this->options['key']);
  50. if (!$verified) {
  51. header("HTTP/1.0 404 Not Found");
  52. die('Url not verified');
  53. }
  54. $user = Newsletter::instance()->get_user($user_id);
  55. if (!$user) {
  56. header("HTTP/1.0 404 Not Found");
  57. die('Invalid subscriber');
  58. }
  59. // Test emails
  60. if (empty($email_id)) {
  61. header('Location: ' . esc_url_raw($url));
  62. die();
  63. }
  64. $email = $this->get_email($email_id);
  65. if (!$email) {
  66. header("HTTP/1.0 404 Not Found");
  67. die('Invalid newsletter');
  68. }
  69. setcookie('newsletter', $user->id . '-' . $user->token, time() + 60 * 60 * 24 * 365, '/');
  70. $is_action = strpos($url, '?na=');
  71. $ip = $this->get_remote_ip();
  72. $ip = $this->process_ip($ip);
  73. if (!$is_action) {
  74. $this->add_click($url, $user_id, $email_id, $ip);
  75. $this->update_open_value(self::SENT_CLICK, $user_id, $email_id, $ip);
  76. } else {
  77. // Track an action as an email read and not a click
  78. $this->update_open_value(self::SENT_READ, $user_id, $email_id, $ip);
  79. }
  80. $this->update_user_ip($user, $ip);
  81. $this->update_user_last_activity($user);
  82. header('Location: ' . apply_filters('newsletter_redirect_url', $url, $email, $user));
  83. die();
  84. }
  85. // Newsletter Open Traking Image
  86. if (isset($_GET['noti'])) {
  87. $this->logger->debug('Open tracking: ' . $_GET['noti']);
  88. list($email_id, $user_id, $signature) = explode(';', base64_decode($_GET['noti']), 3);
  89. $email = $this->get_email($email_id);
  90. if (!$email) {
  91. $this->logger->error('Open tracking request for unexistant email');
  92. die();
  93. }
  94. $user = $this->get_user($user_id);
  95. if (!$user) {
  96. $this->logger->error('Open tracking request for unexistant subscriber');
  97. die();
  98. }
  99. if ($email->token) {
  100. $this->logger->debug('Signature: ' . $signature);
  101. $s = md5($email_id . $user_id . $email->token);
  102. if ($s != $signature) {
  103. $this->logger->error('Open tracking request with wrong signature. Email token: ' . $email->token);
  104. die();
  105. }
  106. } else {
  107. $this->logger->info('Email with no token hence not signature to check');
  108. }
  109. $ip = $this->get_remote_ip();
  110. $ip = $this->process_ip($ip);
  111. $this->add_click('', $user_id, $email_id, $ip);
  112. $this->update_open_value(self::SENT_READ, $user_id, $email_id, $ip);
  113. $this->update_user_last_activity($user);
  114. header('Content-Type: image/gif', true);
  115. echo base64_decode('_R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
  116. die();
  117. }
  118. }
  119. function upgrade() {
  120. global $wpdb, $charset_collate;
  121. parent::upgrade();
  122. $sql = "CREATE TABLE `" . $wpdb->prefix . "newsletter_stats` (
  123. `id` int(11) NOT NULL AUTO_INCREMENT,
  124. `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  125. `url` varchar(255) NOT NULL DEFAULT '',
  126. `user_id` int(11) NOT NULL DEFAULT '0',
  127. `email_id` varchar(10) NOT NULL DEFAULT '0',
  128. `link_id` int(11) NOT NULL DEFAULT '0',
  129. `ip` varchar(20) NOT NULL DEFAULT '',
  130. `country` varchar(4) NOT NULL DEFAULT '',
  131. PRIMARY KEY (`id`),
  132. KEY `email_id` (`email_id`),
  133. KEY `user_id` (`user_id`)
  134. ) $charset_collate;";
  135. dbDelta($sql);
  136. if (empty($this->options['key'])) {
  137. $this->options['key'] = md5($_SERVER['REMOTE_ADDR'] . rand(100000, 999999) . time());
  138. $this->save_options($this->options);
  139. }
  140. }
  141. function admin_menu() {
  142. $this->add_admin_page('index', 'Statistics');
  143. $this->add_admin_page('view', 'Statistics');
  144. $this->add_admin_page('newsletters', 'Statistics');
  145. $this->add_admin_page('settings', 'Statistics');
  146. $this->add_admin_page('view_retarget', 'Statistics');
  147. $this->add_admin_page('view_urls', 'Statistics');
  148. $this->add_admin_page('view_users', 'Statistics');
  149. }
  150. function relink($text, $email_id, $user_id, $email_token = '') {
  151. $this->relink_email_id = $email_id;
  152. $this->relink_user_id = $user_id;
  153. $this->relink_email_token = $email_token;
  154. $this->logger->debug('Relink with token: ' . $email_token);
  155. $text = preg_replace_callback('/(<[aA][^>]+href[\s]*=[\s]*["\'])([^>"\']+)(["\'][^>]*>)(.*?)(<\/[Aa]>)/is', array($this, 'relink_callback'), $text);
  156. $signature = md5($email_id . $user_id . $email_token);
  157. $text = str_replace('</body>', '<img width="1" height="1" alt="" src="' . home_url('/') . '?noti=' . urlencode(base64_encode($email_id . ';' . $user_id . ';' . $signature)) . '"/></body>', $text);
  158. return $text;
  159. }
  160. function relink_callback($matches) {
  161. $href = trim(str_replace('&amp;', '&', $matches[2]));
  162. // Do not replace the tracking or subscription/unsubscription links.
  163. //if (strpos($href, '/newsletter/') !== false) {
  164. // return $matches[0];
  165. //}
  166. // Do not replace URL which are tags (special case for ElasticEmail)
  167. if (strpos($href, '{') === 0) {
  168. return $matches[0];
  169. }
  170. // if (strpos($href, '?na=') !== false) {
  171. // return $matches[0];
  172. // }
  173. // Do not relink anchors
  174. if (substr($href, 0, 1) == '#') {
  175. return $matches[0];
  176. }
  177. // Do not relink mailto:
  178. if (substr($href, 0, 7) == 'mailto:') {
  179. return $matches[0];
  180. }
  181. // This is the link text which is added to the tracking data
  182. $anchor = '';
  183. // if ($this->options['anchor'] == 1) {
  184. // $anchor = trim(str_replace(';', ' ', $matches[4]));
  185. // // Keep images but not other tags
  186. // $anchor = strip_tags($anchor, '<img>');
  187. //
  188. // // Truncate if needed to avoid to much long URLs
  189. // if (stripos($anchor, '<img') === false && strlen($anchor) > 100) {
  190. // $anchor = substr($anchor, 0, 100);
  191. // }
  192. // }
  193. $r = $this->relink_email_id . ';' . $this->relink_user_id . ';' . $href . ';' . $anchor;
  194. $r = $r . ';' . md5($r . $this->options['key']);
  195. $r = base64_encode($r);
  196. $r = urlencode($r);
  197. $url = home_url('/') . '?nltr=' . $r;
  198. return $matches[1] . $url . $matches[3] . $matches[4] . $matches[5];
  199. }
  200. function get_statistics_url($email_id) {
  201. $page = apply_filters('newsletter_statistics_view', 'newsletter_statistics_view');
  202. return 'admin.php?page=' . $page . '&amp;id=' . $email_id;
  203. }
  204. function maybe_fix_sent_stats($email) {
  205. global $wpdb;
  206. // Very old emails was missing the send_on
  207. if ($email->send_on == 0) {
  208. $this->query($wpdb->prepare("update " . NEWSLETTER_EMAILS_TABLE . " set send_on=unix_timestamp(created) where id=%d limit 1", $email->id));
  209. $email = $this->get_email($email->id);
  210. }
  211. if ($email->status == 'sending') {
  212. return;
  213. }
  214. if ($email->type == 'followup') {
  215. return;
  216. }
  217. $count = $wpdb->get_var($wpdb->prepare("select count(*) from " . NEWSLETTER_SENT_TABLE . " where email_id=%d", $email->id));
  218. if ($count) {
  219. return;
  220. }
  221. if (empty($email->query)) {
  222. $email->query = "select * from " . NEWSLETTER_USERS_TABLE . " where status='C'";
  223. }
  224. $query = $email->query . " and unix_timestamp(created)<" . $email->send_on;
  225. $query = str_replace('*', 'id, ' . $email->id . ', ' . $email->send_on, $query);
  226. $this->query("insert ignore into " . NEWSLETTER_SENT_TABLE . " (user_id, email_id, time) " . $query);
  227. }
  228. function update_stats($email) {
  229. global $wpdb;
  230. $wpdb->query($wpdb->prepare("update " . $wpdb->prefix . "newsletter_sent 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));
  231. $wpdb->query($wpdb->prepare("update " . $wpdb->prefix . "newsletter_sent 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));
  232. }
  233. function reset_stats($email) {
  234. global $wpdb;
  235. $email_id = $this->to_int_id($email);
  236. $this->query("delete from " . $wpdb->prefix . "newsletter_sent where email_id=" . $email_id);
  237. $this->query("delete from " . $wpdb->prefix . "newsletter_stats where email_id=" . $email_id);
  238. }
  239. function get_total_count($email_id) {
  240. global $wpdb;
  241. return (int) $wpdb->get_var($wpdb->prepare("select count(*) from " . NEWSLETTER_SENT_TABLE . " where email_id=%d", $this->to_int_id($email_id)));
  242. }
  243. function get_open_count($email_id) {
  244. global $wpdb;
  245. return (int) $wpdb->get_var($wpdb->prepare("select count(*) from " . NEWSLETTER_SENT_TABLE . " where open>0 and email_id=%d", $this->to_int_id($email_id)));
  246. }
  247. function get_click_count($email_id) {
  248. global $wpdb;
  249. return (int) $wpdb->get_var($wpdb->prepare("select count(*) from " . NEWSLETTER_SENT_TABLE . " where open>1 and email_id=%d", $this->to_int_id($email_id)));
  250. }
  251. function get_error_count($email_id) {
  252. global $wpdb;
  253. return (int) $wpdb->get_var($wpdb->prepare("select count(*) from " . NEWSLETTER_SENT_TABLE . " where status>0 and email_id=%d", $this->to_int_id($email_id)));
  254. }
  255. function add_click($url, $user_id, $email_id, $ip = null) {
  256. global $wpdb;
  257. if (is_null($ip)) {
  258. $ip = $this->get_remote_ip();
  259. }
  260. $ip = $this->process_ip($ip);
  261. $this->insert(NEWSLETTER_STATS_TABLE, array(
  262. 'email_id' => $email_id,
  263. 'user_id' => $user_id,
  264. 'url' => $url,
  265. 'ip' => $ip
  266. )
  267. );
  268. }
  269. function update_open_value($value, $user_id, $email_id, $ip = null) {
  270. global $wpdb;
  271. if (is_null($ip)) {
  272. $ip = $this->get_remote_ip();
  273. }
  274. $ip = $this->process_ip($ip);
  275. $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));
  276. }
  277. }
  278. NewsletterStatistics::instance();