- Removes requirement of passing newsletter id when tracking clicks

- Extracts common tracking data processing/validation code into the Track
  class
- Refactors Clicks, Opens and View in Browser classes to enforce
  subscriber id and token check
- Allows admin users to preview newsletters without tracking statistics
This commit is contained in:
Vlad
2016-08-16 21:18:39 -04:00
parent 6ab7debb7b
commit b492bcecc0
9 changed files with 214 additions and 191 deletions

View File

@ -5,6 +5,7 @@ use MailPoet\API\Error as APIError;
use MailPoet\Listing;
use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Setting;
use MailPoet\Models\NewsletterTemplate;
use MailPoet\Models\NewsletterSegment;
@ -252,8 +253,10 @@ class Newsletters extends APIEndpoint {
$data
);
$listing_data = $listing->get();
$subscriber = Subscriber::getCurrentWPUser();
foreach($listing_data['items'] as $key => $newsletter) {
$queue = false;
if($newsletter->type === Newsletter::TYPE_STANDARD) {
$newsletter
@ -277,8 +280,15 @@ class Newsletters extends APIEndpoint {
->withStatistics();
}
if($newsletter->status === Newsletter::STATUS_SENT ||
$newsletter->status === Newsletter::STATUS_SENDING
) {
$queue = SendingQueue::where('newsletter_id', $newsletter->id)->findOne();
}
// get preview url
$newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl($newsletter);
$newsletter->preview_url = NewsletterUrl::getViewInBrowserUrl(
$newsletter, $subscriber, $queue, $preview = true);
// convert object to array
$listing_data['items'][$key] = $newsletter->asArray();

View File

@ -88,7 +88,6 @@ class Newsletter {
);
if($this->tracking_enabled) {
$prepared_newsletter = NewsletterLinks::replaceSubscriberData(
$newsletter['id'],
$subscriber['id'],
$queue['id'],
$prepared_newsletter

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Newsletter\Links;
use MailPoet\Models\Subscriber;
use MailPoet\Router\Front as FrontRouter;
use MailPoet\Router\Endpoints\Track as TrackEndpoint;
use MailPoet\Models\NewsletterLink;
@ -92,10 +93,10 @@ class Links {
}
static function replaceSubscriberData(
$newsletter_id,
$subscriber_id,
$queue_id,
$content
$content,
$preview = false
) {
// match data tags
$regex = sprintf(
@ -103,6 +104,7 @@ class Links {
preg_quote(self::DATA_TAG_CLICK),
preg_quote(self::DATA_TAG_OPEN)
);
$subscriber = Subscriber::findOne($subscriber_id);
preg_match_all($regex, $content, $matches);
foreach($matches[1] as $index => $match) {
$hash = null;
@ -110,10 +112,11 @@ class Links {
list(, $hash) = explode('-', $match);
}
$data = array(
'newsletter' => $newsletter_id,
'subscriber' => $subscriber_id,
'queue' => $queue_id,
'hash' => $hash
'subscriber_id' => $subscriber->id,
'subscriber_token' => Subscriber::generateToken($subscriber->email),
'queue_id' => $queue_id,
'link_hash' => $hash,
'preview' => $preview
);
$router_action = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
TrackEndpoint::ACTION_CLICK :

View File

@ -30,18 +30,19 @@ class Url {
$queue = ($queue) ? $queue->asArray() : false;
}
$data = array(
'newsletter' => (!empty($newsletter['id'])) ?
'newsletter_id' => (!empty($newsletter['id'])) ?
$newsletter['id'] :
$newsletter,
'subscriber' => (!empty($subscriber['id'])) ?
'subscriber_id' => (!empty($subscriber['id'])) ?
$subscriber['id'] :
$subscriber,
'subscriber_token' => (!empty($subscriber['id'])) ?
Subscriber::generateToken($subscriber['email']) :
false,
'queue' => (!empty($queue['id'])) ?
'queue_id' => (!empty($queue['id'])) ?
$queue['id'] :
$queue
$queue,
'preview' => $preview
);
return FrontRouter::buildRequest(
ViewInBrowserEndpoint::ENDPOINT,

View File

@ -12,44 +12,78 @@ use MailPoet\Newsletter\Shortcodes\Shortcodes;
class ViewInBrowser {
public $data;
function __construct($data) {
$this->data = $data;
}
function view($data = false) {
$data = ($data) ? $data : $this->data;
$newsletter = ($data['newsletter'] !== false) ?
Newsletter::findOne($data['newsletter']) :
false;
if(!$newsletter) $this->abort();
$subscriber = ($data['subscriber'] !== false) ?
$this->verifySubscriber($data['subscriber'], $data['subscriber_token']) :
false;
$queue = ($data['queue'] !== false) ?
SendingQueue::findOne($data['queue']) :
false;
static function view($data) {
$data = self::processData($data);
if(!$data) return false;
$rendered_newsletter =
$this->getAndRenderNewsletter($newsletter, $subscriber, $queue);
self::getAndRenderNewsletter(
$data['newsletter'],
$data['subscriber'],
$data['queue'],
$data['preview']
);
header('Content-Type: text/html; charset=utf-8');
echo $rendered_newsletter;
exit;
}
function verifySubscriber($subscriber_id, $subscriber_token) {
$subscriber = Subscriber::findOne($subscriber_id);
if(!$subscriber ||
!Subscriber::verifyToken($subscriber->email, $subscriber_token)
static function processData($data) {
if(empty($data['subscriber_id']) ||
empty($data['subscriber_token']) ||
empty($data['newsletter_id'])
) {
return false;
}
return $subscriber;
$data['newsletter'] = self::getNewsletter($data['newsletter_id']);
$data['subscriber'] = self::getSubscriber($data['subscriber_id']);
$data['queue'] = self::getQueue($data['queue_id']);
$data_processed_successfully =
($data['subscriber'] && $data['newsletter']);
return ($data_processed_successfully) ?
self::validateData($data) :
false;
}
function getAndRenderNewsletter($newsletter, $subscriber, $queue) {
if($queue) {
$newsletter_body = json_decode($queue->newsletter_rendered_body, true);
static function validateData($data) {
if(!$data['subscriber']) return false;
$subscriber_token_match =
Subscriber::verifyToken($data['subscriber']['email'], $data['subscriber_token']);
// return if this is an administrator user previewing the newsletter
if($data['subscriber']['wp_user_id'] && $subscriber_token_match && $data['preview']) {
return ($subscriber_token_match) ? $data : false;
}
// if queue exists, check if the newsletter was sent to the subscriber
if($data['queue'] && $data['subscriber']) {
$is_valid_subscriber =
(!empty($data['queue']['subscribers']['processed']) &&
in_array($data['subscriber']['id'], $data['queue']['subscribers']['processed']));
$data = ($is_valid_subscriber && $subscriber_token_match) ? $data : false;
} else {
$renderer = new Renderer($newsletter->asArray(), $preview = true);
$data = ($subscriber_token_match) ? $data : false;
}
return $data;
}
static function getNewsletter($newsletter_id) {
$newsletter = Newsletter::findOne($newsletter_id);
return ($newsletter) ? $newsletter->asArray() : $newsletter;
}
static function getQueue($queue_id) {
$queue = SendingQueue::findOne($queue_id);
return ($queue) ? $queue->asArray() : $queue;
}
static function getSubscriber($subscriber_id) {
$subscriber = Subscriber::findOne($subscriber_id);
return ($subscriber) ? $subscriber->asArray() : $subscriber;
}
static function getAndRenderNewsletter($newsletter, $subscriber, $queue, $preview) {
if($queue && $queue['newsletter_rendered_body']) {
$newsletter_body = json_decode($queue['newsletter_rendered_body'], true);
} else {
$renderer = new Renderer($newsletter, $preview);
$newsletter_body = $renderer->render();
}
$shortcodes = new Shortcodes(
@ -60,16 +94,16 @@ class ViewInBrowser {
$rendered_newsletter = $shortcodes->replace($newsletter_body['html']);
if($queue && (boolean)Setting::getValue('tracking.enabled')) {
$rendered_newsletter = Links::replaceSubscriberData(
$newsletter->id,
$subscriber->id,
$queue->id,
$rendered_newsletter
$subscriber['id'],
$queue['id'],
$rendered_newsletter,
$preview
);
}
return $rendered_newsletter;
}
private function abort() {
private static function abort() {
status_header(404);
exit;
}

View File

@ -1,6 +1,10 @@
<?php
namespace MailPoet\Router\Endpoints;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\Subscriber;
use MailPoet\Statistics\Track\Clicks;
use MailPoet\Statistics\Track\Opens;
@ -12,12 +16,68 @@ class Track {
const ACTION_OPEN = 'open';
static function click($data) {
$clicks = new Clicks($data);
$clicks->track();
Clicks::track(self::_processTrackData($data));
}
static function open($data) {
$opens = new Opens($data);
$opens->track();
Opens::track(self::_processTrackData($data));
}
static function _processTrackData($data) {
if(empty($data['queue_id']) ||
empty($data['subscriber_id']) ||
empty($data['subscriber_token'])
) {
return false;
}
$data['queue'] = self::_getQueue($data['queue_id']);
$data['subscriber'] = self::_getSubscriber($data['subscriber_id']);
$data['newsletter'] = (!empty($data['queue']['newsletter_id'])) ?
self::_getNewsletter($data['queue']['newsletter_id']) :
false;
if(!empty($data['link_hash'])) {
$data['link'] = self::_getLink($data['link_hash']);
}
$data_processed_successfully =
($data['queue'] && $data['subscriber'] && $data['newsletter']);
return ($data_processed_successfully) ?
self::_validateTrackData($data) :
false;
}
static function _validateTrackData($data) {
if(!$data['subscriber']) return false;
$subscriber_token_match =
Subscriber::verifyToken($data['subscriber']['email'], $data['subscriber_token']);
// return if this is an administrator user previewing the newsletter
if($data['subscriber']['wp_user_id'] && $data['preview']) {
return ($subscriber_token_match) ? $data : false;
}
// check if the newsletter was sent to the subscriber
$is_valid_subscriber =
(!empty($data['queue']['subscribers']['processed']) &&
in_array($data['subscriber']['id'], $data['queue']['subscribers']['processed']));
return ($is_valid_subscriber && $subscriber_token_match) ? $data : false;
}
static function _getNewsletter($newsletter_id) {
$newsletter = Newsletter::findOne($newsletter_id);
return ($newsletter) ? $newsletter->asArray() : $newsletter;
}
static function _getSubscriber($subscriber_id) {
$subscriber = Subscriber::findOne($subscriber_id);
return ($subscriber) ? $subscriber->asArray() : $subscriber;
}
static function _getQueue($queue_id) {
$queue = SendingQueue::findOne($queue_id);
return ($queue) ? $queue->asArray() : $queue;
}
static function _getLink($link_hash) {
$link = NewsletterLink::where('hash', $link_hash)
->findOne();
return ($link) ? $link->asArray() : $link;
}
}

View File

@ -10,7 +10,6 @@ class ViewInBrowser {
const ACTION_VIEW = 'view';
static function view($data) {
$viewer = new NewsletterViewInBrowser($data);
$viewer->view();
NewsletterViewInBrowser::view($data);
}
}

View File

@ -1,44 +1,21 @@
<?php
namespace MailPoet\Statistics\Track;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterLink;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\StatisticsClicks;
use MailPoet\Models\Subscriber;
use MailPoet\Newsletter\Shortcodes\Categories\Link;
if(!defined('ABSPATH')) exit;
class Clicks {
public $data;
function __construct($data) {
$this->data = $data;
}
function track($data = false) {
$data = ($data) ? $data : $this->data;
$newsletter = $this->getNewsletter($data['newsletter']);
$queue = $this->getQueue($data['queue']);
// verify if queue belongs to the newsletter
if($newsletter && $queue) {
$queue = ($queue['newsletter_id'] === $newsletter['id']) ?
$queue :
false;
}
$subscriber = $this->getSubscriber($data['subscriber']);
// verify if subscriber belongs to the queue
if($queue && $subscriber) {
// check if this newsletter was sent to
$subscriber = (in_array($subscriber['id'], $queue['subscribers']['processed'])) ?
$subscriber :
false;
}
$link = $this->getLink($data['hash']);
if(!$subscriber || !$newsletter || !$link || !$queue) {
$this->abort();
}
static function track($data) {
if(!$data || empty($data['link'])) return false;
$subscriber = $data['subscriber'];
$queue = $data['queue'];
$newsletter = $data['newsletter'];
$link = $data['link'];
// log statistics only if the action did not come from
// an admin user previewing the newsletter
if(!$data['preview'] && !$subscriber['wp_user_id']) {
$statistics = StatisticsClicks::where('link_id', $link['id'])
->where('subscriber_id', $subscriber['id'])
->where('newsletter_id', $newsletter['id'])
@ -46,7 +23,7 @@ class Clicks {
->findOne();
if(!$statistics) {
// track open event in case it did not register
$this->trackOpenEvent($data);
self::trackOpenEvent($data);
$statistics = StatisticsClicks::create();
$statistics->newsletter_id = $newsletter['id'];
$statistics->link_id = $link['id'];
@ -58,34 +35,14 @@ class Clicks {
$statistics->count++;
$statistics->save();
}
$url = $this->processUrl($link['url'], $newsletter, $subscriber, $queue);
$this->redirectToUrl($url);
}
$url = self::processUrl($link['url'], $newsletter, $subscriber, $queue);
self::redirectToUrl($url);
}
function getNewsletter($newsletter_id) {
$newsletter = Newsletter::findOne($newsletter_id);
return ($newsletter) ? $newsletter->asArray() : $newsletter;
}
function getSubscriber($subscriber_id) {
$subscriber = Subscriber::findOne($subscriber_id);
return ($subscriber) ? $subscriber->asArray() : $subscriber;
}
function getQueue($queue_id) {
$queue = SendingQueue::findOne($queue_id);
return ($queue) ? $queue->asArray() : $queue;
}
function getLink($hash) {
$link = NewsletterLink::where('hash', $hash)
->findOne();
return ($link) ? $link->asArray() : $link;
}
function processUrl($url, $newsletter, $subscriber, $queue) {
static function processUrl($url, $newsletter, $subscriber, $queue) {
if(preg_match('/\[link:(?P<action>.*?)\]/', $url, $shortcode)) {
if(!$shortcode['action']) $this->abort();
if(!$shortcode['action']) self::abort();
$url = Link::processShortcodeAction(
$shortcode['action'],
$newsletter,
@ -96,17 +53,16 @@ class Clicks {
return $url;
}
function trackOpenEvent($data) {
$open = new Opens($data, $display_image = false);
return $open->track();
static function trackOpenEvent($data) {
return Opens::track($data, $display_image = false);
}
function abort() {
static function abort() {
status_header(404);
exit;
}
function redirectToUrl($url) {
static function redirectToUrl($url) {
header('Location: ' . $url, true, 302);
exit;
}

View File

@ -1,76 +1,37 @@
<?php
namespace MailPoet\Statistics\Track;
use MailPoet\Models\Newsletter;
use MailPoet\Models\SendingQueue;
use MailPoet\Models\StatisticsOpens;
use MailPoet\Models\Subscriber;
if(!defined('ABSPATH')) exit;
class Opens {
public $data;
public $return_image;
function __construct($data, $return_image = true) {
$this->data = $data;
$this->return_image = $return_image;
}
function track($data = false) {
$data = ($data) ? $data : $this->data;
$newsletter = $this->getNewsletter($data['newsletter']);
$queue = $this->getQueue($data['queue']);
// verify if queue belongs to the newsletter
if($newsletter && $queue) {
$queue = ($queue['newsletter_id'] === $newsletter['id']) ?
$queue :
false;
}
$subscriber = $this->getSubscriber($data['subscriber']);
// verify if subscriber belongs to the queue
if($queue && $subscriber) {
if(empty($queue['']))
$subscriber = (in_array($subscriber['id'], $queue['subscribers']['processed'])) ?
$subscriber :
false;
}
if(!$subscriber || !$newsletter || !$queue) {
return false;
}
$statistics = StatisticsOpens::where('subscriber_id', $data['subscriber'])
->where('newsletter_id', $data['newsletter'])
->where('queue_id', $data['queue'])
static function track($data, $display_image = true) {
if(!$data) return self::displayImage();
$subscriber = $data['subscriber'];
$queue = $data['queue'];
$newsletter = $data['newsletter'];
// log statistics only if the action did not come from
// an admin user previewing the newsletter
if (!$data['preview'] && !$subscriber['wp_user_id']) {
$statistics = StatisticsOpens::where('subscriber_id', $subscriber['id'])
->where('newsletter_id', $newsletter['id'])
->where('queue_id', $queue['id'])
->findOne();
if(!$statistics) {
$statistics = StatisticsOpens::create();
$statistics->newsletter_id = $data['newsletter'];
$statistics->subscriber_id = $data['subscriber'];
$statistics->queue_id = $data['queue'];
$statistics->newsletter_id = $newsletter['id'];
$statistics->subscriber_id = $subscriber['id'];
$statistics->queue_id = $queue['id'];
$statistics->save();
}
if($this->return_image) {
$this->returnImage();
}
return true;
return ($display_image) ?
self::displayImage() :
true;
}
function getNewsletter($newsletter_id) {
$newsletter = Newsletter::findOne($newsletter_id);
return ($newsletter) ? $newsletter->asArray() : $newsletter;
}
function getSubscriber($subscriber_id) {
$subscriber = Subscriber::findOne($subscriber_id);
return ($subscriber) ? $subscriber->asArray() : $subscriber;
}
function getQueue($queue_id) {
$queue = SendingQueue::findOne($queue_id);
return ($queue) ? $queue->asArray() : $queue;
}
function returnImage() {
static function displayImage() {
// return 1x1 pixel transparent gif image
header('Content-Type: image/gif');
echo "\x47\x49\x46\x38\x37\x61\x1\x0\x1\x0\x80\x0\x0\xfc\x6a\x6c\x0\x0\x0\x2c\x0\x0\x0\x0\x1\x0\x1\x0\x0\x2\x2\x44\x1\x0\x3b";