diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index a7b9010a0d..10959839a3 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -156,6 +156,7 @@ class ContainerConfigurator implements IContainerConfigurator { // Newsletter $container->autowire(\MailPoet\Newsletter\AutomatedLatestContent::class)->setPublic(true); // Util + $container->autowire(\MailPoet\Util\Cookies::class); $container->autowire(\MailPoet\Util\Url::class)->setPublic(true); $container->autowire(\MailPoet\Util\Installation::class); // WooCommerce diff --git a/lib/Statistics/Track/Clicks.php b/lib/Statistics/Track/Clicks.php index ea6f2ac2e7..d830aaef24 100644 --- a/lib/Statistics/Track/Clicks.php +++ b/lib/Statistics/Track/Clicks.php @@ -5,6 +5,7 @@ use MailPoet\Models\StatisticsClicks; use MailPoet\Newsletter\Shortcodes\Categories\Link; use MailPoet\Newsletter\Shortcodes\Shortcodes; use MailPoet\Settings\SettingsController; +use MailPoet\Util\Cookies; use MailPoet\WP\Functions as WPFunctions; if (!defined('ABSPATH')) exit; @@ -20,8 +21,12 @@ class Clicks { /** @var SettingsController */ private $settings_controller; - public function __construct(SettingsController $settings_controller) { + /** @var Cookies */ + private $cookies; + + public function __construct(SettingsController $settings_controller, Cookies $cookies) { $this->settings_controller = $settings_controller; + $this->cookies = $cookies; } /** @@ -57,27 +62,31 @@ class Clicks { private function sendRevenueCookie(StatisticsClicks $clicks) { if ($this->settings_controller->get('woocommerce.accept_cookie_revenue_tracking.enabled')) { - setcookie( + $this->cookies->set( self::REVENUE_TRACKING_COOKIE_NAME, - serialize([ + [ 'statistics_clicks' => $clicks->id, 'created_at' => time(), - ]), - time() + self::REVENUE_TRACKING_COOKIE_EXPIRY, - '/' + ], + [ + 'expires' => time() + self::REVENUE_TRACKING_COOKIE_EXPIRY, + 'path' => '/', + ] ); } } private function sendAbandonedCartCookie($subscriber) { if ($this->settings_controller->get('woocommerce.accept_cookie_revenue_tracking.enabled')) { - setcookie( + $this->cookies->set( self::ABANDONED_CART_COOKIE_NAME, - serialize([ + [ 'subscriber_id' => $subscriber->id, - ]), - time() + self::ABANDONED_CART_COOKIE_EXPIRY, - '/' + ], + [ + 'expires' => time() + self::ABANDONED_CART_COOKIE_EXPIRY, + 'path' => '/', + ] ); } } diff --git a/lib/Statistics/Track/WooCommercePurchases.php b/lib/Statistics/Track/WooCommercePurchases.php index 523207bef8..a869d949cd 100644 --- a/lib/Statistics/Track/WooCommercePurchases.php +++ b/lib/Statistics/Track/WooCommercePurchases.php @@ -4,6 +4,7 @@ namespace MailPoet\Statistics\Track; use MailPoet\Models\StatisticsClicks; use MailPoet\Models\StatisticsWooCommercePurchases; use MailPoet\Models\Subscriber; +use MailPoet\Util\Cookies; use MailPoet\WooCommerce\Helper; use WC_Order; @@ -15,8 +16,12 @@ class WooCommercePurchases { /** @var Helper */ private $woocommerce_helper; - function __construct(Helper $woocommerce_helper) { + /** @var Cookies */ + private $cookies; + + function __construct(Helper $woocommerce_helper, Cookies $cookies) { $this->woocommerce_helper = $woocommerce_helper; + $this->cookies = $cookies; } function trackPurchase($id, $use_cookies = true) { @@ -61,12 +66,12 @@ class WooCommercePurchases { } private function getSubscriberEmailFromCookie() { - $click_cookie = $this->getClickCookie(); - if (!$click_cookie) { + $cookie_data = $this->cookies->get(Clicks::REVENUE_TRACKING_COOKIE_NAME); + if (!$cookie_data) { return null; } - $click = StatisticsClicks::findOne($click_cookie['statistics_clicks']); + $click = StatisticsClicks::findOne($cookie_data['statistics_clicks']); if (!$click) { return null; } @@ -77,11 +82,4 @@ class WooCommercePurchases { } return null; } - - private function getClickCookie() { - if (empty($_COOKIE[Clicks::REVENUE_TRACKING_COOKIE_NAME])) { - return null; - } - return unserialize($_COOKIE[Clicks::REVENUE_TRACKING_COOKIE_NAME]); - } } diff --git a/lib/Util/Cookies.php b/lib/Util/Cookies.php new file mode 100644 index 0000000000..5bf50a23e4 --- /dev/null +++ b/lib/Util/Cookies.php @@ -0,0 +1,47 @@ + 0, + 'path' => '', + 'domain' => '', + 'secure' => false, + 'httponly' => false, + ]; + + function set($name, $value, array $options = []) { + $options = $options + self::DEFAULT_OPTIONS; + $value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $error = json_last_error(); + if ($error) { + throw new InvalidArgumentException(); + } + + // on PHP_VERSION_ID >= 70300 we'll be able to simply setcookie($name, $value, $options); + setcookie( + $name, + $value, + $options['expires'], + $options['path'], + $options['domain'], + $options['secure'], + $options['httponly'] + ); + } + + function get($name) { + if (!array_key_exists($name, $_COOKIE)) { + return null; + } + $value = json_decode(stripslashes($_COOKIE[$name]), true); + $error = json_last_error(); + if ($error) { + return null; + } + return $value; + } +} diff --git a/tests/integration/Router/Endpoints/TrackTest.php b/tests/integration/Router/Endpoints/TrackTest.php index 3b94dc1a52..5029e1ba25 100644 --- a/tests/integration/Router/Endpoints/TrackTest.php +++ b/tests/integration/Router/Endpoints/TrackTest.php @@ -13,6 +13,7 @@ use MailPoet\Settings\SettingsController; use MailPoet\Statistics\Track\Clicks; use MailPoet\Statistics\Track\Opens; use MailPoet\Tasks\Sending as SendingTask; +use MailPoet\Util\Cookies; class TrackTest extends \MailPoetTest { function _before() { @@ -50,7 +51,7 @@ class TrackTest extends \MailPoetTest { 'preview' => false, ]; // instantiate class - $this->track = new Track(new Clicks(new SettingsController()), new Opens()); + $this->track = new Track(new Clicks(new SettingsController(), new Cookies()), new Opens()); } function testItReturnsFalseWhenTrackDataIsMissing() { diff --git a/tests/integration/Statistics/Track/ClicksTest.php b/tests/integration/Statistics/Track/ClicksTest.php index af30adbae5..07a18a463a 100644 --- a/tests/integration/Statistics/Track/ClicksTest.php +++ b/tests/integration/Statistics/Track/ClicksTest.php @@ -13,6 +13,7 @@ use MailPoet\Models\Subscriber; use MailPoet\Settings\SettingsController; use MailPoet\Statistics\Track\Clicks; use MailPoet\Tasks\Sending as SendingTask; +use MailPoet\Util\Cookies; class ClicksTest extends \MailPoetTest { @@ -60,12 +61,12 @@ class ClicksTest extends \MailPoetTest { $this->settings_controller = Stub::makeEmpty(SettingsController::class, [ 'get' => false, ], $this); - $this->clicks = new Clicks($this->settings_controller); + $this->clicks = new Clicks($this->settings_controller, new Cookies()); } function testItAbortsWhenTrackDataIsEmptyOrMissingLink() { // abort function should be called twice: - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'abort' => Expected::exactly(2), ], $this); $data = $this->track_data; @@ -80,7 +81,7 @@ class ClicksTest extends \MailPoetTest { $data = $this->track_data; $data->subscriber->wp_user_id = 99; $data->preview = true; - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'redirectToUrl' => null, ], $this); $clicks->track($data); @@ -90,7 +91,7 @@ class ClicksTest extends \MailPoetTest { function testItTracksClickAndOpenEvent() { $data = $this->track_data; - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'redirectToUrl' => null, ], $this); $clicks->track($data); @@ -99,14 +100,14 @@ class ClicksTest extends \MailPoetTest { } function testItRedirectsToUrlAfterTracking() { - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'redirectToUrl' => Expected::exactly(1), ], $this); $clicks->track($this->track_data); } function testItIncrementsClickEventCount() { - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'redirectToUrl' => null, ], $this); $clicks->track($this->track_data); @@ -127,7 +128,7 @@ class ClicksTest extends \MailPoetTest { } function testItFailsToConvertsInvalidShortcodeToUrl() { - $clicks = Stub::construct($this->clicks, [$this->settings_controller], [ + $clicks = Stub::construct($this->clicks, [$this->settings_controller, new Cookies()], [ 'abort' => Expected::exactly(1), ], $this); // should call abort() method if shortcode action does not exist diff --git a/tests/integration/Statistics/Track/WooCommercePurchasesTest.php b/tests/integration/Statistics/Track/WooCommercePurchasesTest.php index 5fbbf9d16f..395c6dd5ce 100644 --- a/tests/integration/Statistics/Track/WooCommercePurchasesTest.php +++ b/tests/integration/Statistics/Track/WooCommercePurchasesTest.php @@ -10,6 +10,7 @@ use MailPoet\Models\StatisticsWooCommercePurchases; use MailPoet\Models\Subscriber; use MailPoet\Statistics\Track\WooCommercePurchases; use MailPoet\Tasks\Sending; +use MailPoet\Util\Cookies; use MailPoet\WooCommerce\Helper as WooCommerceHelper; use PHPUnit_Framework_MockObject_MockObject; use WC_Order; @@ -27,6 +28,9 @@ class WooCommercePurchasesTest extends \MailPoetTest { /** @var NewsletterLink */ private $link; + /** @var Cookies */ + private $cookies; + function _before() { parent::_before(); $this->cleanup(); @@ -35,12 +39,13 @@ class WooCommercePurchasesTest extends \MailPoetTest { $this->newsletter = $this->createNewsletter(); $this->queue = $this->createQueue($this->newsletter, $this->subscriber); $this->link = $this->createLink($this->newsletter, $this->queue); + $this->cookies = new Cookies(); } function testItTracksPayment() { $click = $this->createClick($this->link, $this->subscriber); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); expect(count($purchase_stats))->equals(1); @@ -63,7 +68,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { $click_2 = $this->createClick($link, $this->subscriber, 1); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); expect(count($purchase_stats))->equals(2); @@ -84,12 +89,12 @@ class WooCommercePurchasesTest extends \MailPoetTest { // first order $order_mock = $this->createOrderMock($this->subscriber->email, 10.0, 123); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); // second order $order_mock = $this->createOrderMock($this->subscriber->email, 20.0, 456); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); expect(count(StatisticsWooCommercePurchases::findMany()))->equals(2); @@ -100,7 +105,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { $this->createClick($this->link, $this->subscriber, 3); $this->createClick($this->link, $this->subscriber, 5); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); @@ -111,7 +116,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { function testItTracksPaymentOnlyOnce() { $this->createClick($this->link, $this->subscriber); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $woocommerce_purchases->trackPurchase($order_mock->get_id()); expect(count(StatisticsWooCommercePurchases::findMany()))->equals(1); @@ -120,7 +125,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { function testItDoesNotTrackPaymentWhenClickTooOld() { $this->createClick($this->link, $this->subscriber, 20); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); expect(count(StatisticsWooCommercePurchases::findMany()))->equals(0); } @@ -128,7 +133,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { function testItDoesNotTrackPaymentForDifferentEmail() { $this->createClick($this->link, $this->subscriber); $order_mock = $this->createOrderMock('different.email@example.com'); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); expect(count(StatisticsWooCommercePurchases::findMany()))->equals(0); } @@ -136,7 +141,7 @@ class WooCommercePurchasesTest extends \MailPoetTest { function testItDoesNotTrackPaymentWhenClickNewerThanOrder() { $this->createClick($this->link, $this->subscriber, 0); $order_mock = $this->createOrderMock($this->subscriber->email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); expect(count(StatisticsWooCommercePurchases::findMany()))->equals(0); } @@ -148,13 +153,13 @@ class WooCommercePurchasesTest extends \MailPoetTest { $cookie_email_subscriber = $this->createSubscriber($cookie_email); $click = $this->createClick($this->link, $cookie_email_subscriber); - $_COOKIE['mailpoet_revenue_tracking'] = serialize([ + $_COOKIE['mailpoet_revenue_tracking'] = json_encode([ 'statistics_clicks' => $click->id, 'created_at' => time(), ]); $order_mock = $this->createOrderMock($order_email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); expect(count($purchase_stats))->equals(1); @@ -171,13 +176,13 @@ class WooCommercePurchasesTest extends \MailPoetTest { $order_email_click = $this->createClick($this->link, $order_email_subscriber); $cookie_email_click = $this->createClick($this->link, $cookie_email_subscriber); - $_COOKIE['mailpoet_revenue_tracking'] = serialize([ + $_COOKIE['mailpoet_revenue_tracking'] = json_encode([ 'statistics_clicks' => $cookie_email_click->id, 'created_at' => time(), ]); $order_mock = $this->createOrderMock($order_email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); expect(count($purchase_stats))->equals(1); @@ -199,13 +204,13 @@ class WooCommercePurchasesTest extends \MailPoetTest { $link = $this->createLink($newsletter, $queue); $cookie_email_click = $this->createClick($link, $cookie_email_subscriber); - $_COOKIE['mailpoet_revenue_tracking'] = serialize([ + $_COOKIE['mailpoet_revenue_tracking'] = json_encode([ 'statistics_clicks' => $cookie_email_click->id, 'created_at' => time(), ]); $order_mock = $this->createOrderMock($order_email); - $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock)); + $woocommerce_purchases = new WooCommercePurchases($this->createWooCommerceHelperMock($order_mock), $this->cookies); $woocommerce_purchases->trackPurchase($order_mock->get_id()); $purchase_stats = StatisticsWooCommercePurchases::findMany(); expect(count($purchase_stats))->equals(2);