diff --git a/lib/Newsletter/Shortcodes/Categories/Link.php b/lib/Newsletter/Shortcodes/Categories/Link.php index 1f60fffb34..7f5c0da88b 100644 --- a/lib/Newsletter/Shortcodes/Categories/Link.php +++ b/lib/Newsletter/Shortcodes/Categories/Link.php @@ -94,8 +94,9 @@ class Link { switch($shortcode_action) { case 'subscription_unsubscribe_url': // track unsubscribe event - if((boolean)Setting::getValue('tracking.enabled')) { - Unsubscribes::track($newsletter, $subscriber, $queue, $wp_user_preview); + if((boolean)Setting::getValue('tracking.enabled') && !$wp_user_preview) { + $unsubscribe_event = new Unsubscribes(); + $unsubscribe_event->track($newsletter->id, $subscriber->id, $queue->id); } $url = SubscriptionUrl::getUnsubscribeUrl($subscriber); break; diff --git a/lib/Router/Endpoints/Track.php b/lib/Router/Endpoints/Track.php index 8289d35d1f..90745a98a1 100644 --- a/lib/Router/Endpoints/Track.php +++ b/lib/Router/Endpoints/Track.php @@ -16,18 +16,20 @@ class Track { const ACTION_OPEN = 'open'; static function click($data) { - Clicks::track(self::_processTrackData($data)); + $click_event = new Clicks(); + return $click_event->track(self::_processTrackData($data)); } static function open($data) { - Opens::track(self::_processTrackData($data)); + $open_event = new Opens(); + return $open_event->track(self::_processTrackData($data)); } static function _processTrackData($data) { $data = (object)$data; if(empty($data->queue_id) || - empty($data->subscriber_id) || - empty($data->subscriber_token) + empty($data->subscriber_id) || + empty($data->subscriber_token) ) { return false; } diff --git a/lib/Statistics/Track/Clicks.php b/lib/Statistics/Track/Clicks.php index 1443ca86fb..82a72dc9c6 100644 --- a/lib/Statistics/Track/Clicks.php +++ b/lib/Statistics/Track/Clicks.php @@ -7,8 +7,10 @@ use MailPoet\Newsletter\Shortcodes\Categories\Link; if(!defined('ABSPATH')) exit; class Clicks { - static function track($data) { - if(!$data || empty($data->link)) self::abort(); + function track($data) { + if(!$data || empty($data->link)) { + return $this->abort(); + } $subscriber = $data->subscriber; $queue = $data->queue; $newsletter = $data->newsletter; @@ -17,22 +19,23 @@ class Clicks { // log statistics only if the action did not come from // a WP user previewing the newsletter if(!$wp_user_preview) { - $statistics = StatisticsClicks::createOrUpdateClickCount( + StatisticsClicks::createOrUpdateClickCount( $link->id, $subscriber->id, $newsletter->id, $queue->id ); // track open event - Opens::track($data, $display_image = false); + $open_event = new Opens(); + $open_event->track($data, $display_image = false); } - $url = self::processUrl($link->url, $newsletter, $subscriber, $queue, $wp_user_preview); - self::redirectToUrl($url); + $url = $this->processUrl($link->url, $newsletter, $subscriber, $queue, $wp_user_preview); + $this->redirectToUrl($url); } - static function processUrl($url, $newsletter, $subscriber, $queue, $wp_user_preview) { + function processUrl($url, $newsletter, $subscriber, $queue, $wp_user_preview) { if(preg_match('/\[link:(?P.*?)\]/', $url, $shortcode)) { - if(!$shortcode['action']) self::abort(); + if(!$shortcode['action']) $this->abort(); $url = Link::processShortcodeAction( $shortcode['action'], $newsletter, @@ -44,12 +47,12 @@ class Clicks { return $url; } - static function abort() { + function abort() { status_header(404); exit; } - static function redirectToUrl($url) { + function redirectToUrl($url) { header('Location: ' . $url, true, 302); exit; } diff --git a/lib/Statistics/Track/Opens.php b/lib/Statistics/Track/Opens.php index 5ee1be8725..8b85e2da84 100644 --- a/lib/Statistics/Track/Opens.php +++ b/lib/Statistics/Track/Opens.php @@ -6,8 +6,10 @@ use MailPoet\Models\StatisticsOpens; if(!defined('ABSPATH')) exit; class Opens { - static function track($data, $display_image = true) { - if(!$data) return self::returnResponse($display_image); + function track($data, $display_image = true) { + if(!$data) { + return $this->returnResponse($display_image); + } $subscriber = $data->subscriber; $queue = $data->queue; $newsletter = $data->newsletter; @@ -21,10 +23,10 @@ class Opens { $queue->id ); } - return self::returnResponse($display_image); + return $this->returnResponse($display_image); } - static function returnResponse($display_image) { + function returnResponse($display_image) { if(!$display_image) return; // return 1x1 pixel transparent gif image header('Content-Type: image/gif'); diff --git a/lib/Statistics/Track/Unsubscribes.php b/lib/Statistics/Track/Unsubscribes.php index 3fc6a1046f..e5e36f3926 100644 --- a/lib/Statistics/Track/Unsubscribes.php +++ b/lib/Statistics/Track/Unsubscribes.php @@ -6,12 +6,17 @@ use MailPoet\Models\StatisticsUnsubscribes; if(!defined('ABSPATH')) exit; class Unsubscribes { - static function track($newsletter, $subscriber, $queue, $wp_user_preview) { - if($wp_user_preview) return; - StatisticsUnsubscribes::getOrCreate( - $subscriber->id, - $newsletter->id, - $queue->id - ); + function track($newsletter_id, $subscriber_id, $queue_id) { + $statistics = StatisticsUnsubscribes::where('subscriber_id', $subscriber_id) + ->where('newsletter_id', $newsletter_id) + ->where('queue_id', $queue_id) + ->findOne(); + if(!$statistics) { + $statistics = StatisticsUnsubscribes::create(); + $statistics->newsletter_id = $newsletter_id; + $statistics->subscriber_id = $subscriber_id; + $statistics->queue_id = $queue_id; + $statistics->save(); + } } } \ No newline at end of file diff --git a/tests/unit/Router/Endpoints/TrackTest.php b/tests/unit/Router/Endpoints/TrackTest.php new file mode 100644 index 0000000000..8bd196fe65 --- /dev/null +++ b/tests/unit/Router/Endpoints/TrackTest.php @@ -0,0 +1,157 @@ +type = 'type'; + $this->newsletter = $newsletter->save(); + // create subscriber + $subscriber = Subscriber::create(); + $subscriber->email = 'test@example.com'; + $subscriber->first_name = 'First'; + $subscriber->last_name = 'Last'; + $this->subscriber = $subscriber->save(); + // create queue + $queue = SendingQueue::create(); + $queue->newsletter_id = $newsletter->id; + $queue->subscribers = array('processed' => array($subscriber->id)); + $this->queue = $queue->save(); + // create link + $link = NewsletterLink::create(); + $link->hash = 'hash'; + $link->url = 'url'; + $link->newsletter_id = $newsletter->id; + $link->queue_id = $queue->id; + $this->link = $link->save(); + // build track data + $this->track_data = (object)array( + 'queue' => $queue, + 'subscriber' => $subscriber, + 'newsletter' => $newsletter, + 'subscriber_token' => Subscriber::generateToken($subscriber->email), + 'link' => $link, + 'preview' => false + ); + // instantiate class + $this->clicks = new Clicks(); + } + + function testItAbortsWhenTrackDataIsEmptyOrMissingLink() { + // abort function should be called twice: + $clicks = Stub::make($this->clicks, array( + 'abort' => Stub::exactly(2, function() { }) + ), $this); + $data = $this->track_data; + // 1. when tracking data does not exist + $clicks->track(false); + // 2. when link model object is missing + unset($data->link); + $clicks->track($data); + } + + function testItDoesNotTrackEventsFromWpUserWhenPreviewIsEnabled() { + $data = $this->track_data; + $data->subscriber->wp_user_id = 99; + $data->preview = true; + $clicks = Stub::make($this->clicks, array( + 'redirectToUrl' => function() { } + ), $this); + $clicks->track($data); + expect(StatisticsClicks::findMany())->isEmpty(); + expect(StatisticsOpens::findMany())->isEmpty(); + } + + function testItTracksClickAndOpenEvent() { + $data = $this->track_data; + $clicks = Stub::make($this->clicks, array( + 'redirectToUrl' => function() { } + ), $this); + $clicks->track($data); + expect(StatisticsClicks::findMany())->notEmpty(); + expect(StatisticsOpens::findMany())->notEmpty(); + } + + function testItRedirectsToUrlAfterTracking() { + $clicks = Stub::make($this->clicks, array( + 'redirectToUrl' => Stub::exactly(1, function() { }) + ), $this); + $clicks->track($this->track_data); + } + + function testItIncrementsClickEventCount() { + $clicks = Stub::make($this->clicks, array( + 'redirectToUrl' => function() { } + ), $this); + $clicks->track($this->track_data); + expect(StatisticsClicks::findMany()[0]->count)->equals(1); + $clicks->track($this->track_data); + expect(StatisticsClicks::findMany()[0]->count)->equals(2); + } + + function testItConvertsShortcodesToUrl() { + $link = $this->clicks->processUrl( + '[link:newsletter_view_in_browser_url]', + $this->newsletter, + $this->subscriber, + $this->queue, + $preview = false + ); + expect($link)->contains('&endpoint=view_in_browser'); + } + + function testItFailsToConvertsInvalidShortcodeToUrl() { + $clicks = Stub::make($this->clicks, array( + 'abort' => Stub::exactly(1, function() { }) + ), $this); + // should call abort() method if shortcode action does not exist + $link = $clicks->processUrl( + '[link:]', + $this->newsletter, + $this->subscriber, + $this->queue, + $preview = false + ); + } + + function testItDoesNotConvertNonexistentShortcodeToUrl() { + $link = $this->clicks->processUrl( + '[link:unknown_shortcode]', + $this->newsletter, + $this->subscriber, + $this->queue, + $preview = false + ); + expect($link)->equals('[link:unknown_shortcode]'); + } + + + function testItDoesNotConvertRegulaUrls() { + $link = $this->clicks->processUrl( + 'http://example.com', + $this->newsletter, + $this->subscriber, + $this->queue, + $preview = false + ); + expect($link)->equals('http://example.com'); + } + + function _after() { + ORM::raw_execute('TRUNCATE ' . Newsletter::$_table); + ORM::raw_execute('TRUNCATE ' . Subscriber::$_table); + ORM::raw_execute('TRUNCATE ' . NewsletterLink::$_table); + ORM::raw_execute('TRUNCATE ' . SendingQueue::$_table); + ORM::raw_execute('TRUNCATE ' . StatisticsOpens::$_table); + ORM::raw_execute('TRUNCATE ' . StatisticsClicks::$_table); + } +} \ No newline at end of file