init
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\NewsletterLinkEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Newsletter\Shortcodes\Categories\Link as LinkShortcodeCategory;
|
||||
use MailPoet\Newsletter\Shortcodes\Shortcodes;
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\Statistics\StatisticsClicksRepository;
|
||||
use MailPoet\Statistics\UserAgentsRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\Util\Cookies;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class Clicks {
|
||||
|
||||
const REVENUE_TRACKING_COOKIE_NAME = 'mailpoet_revenue_tracking';
|
||||
const REVENUE_TRACKING_COOKIE_EXPIRY = 60 * 60 * 24 * 14;
|
||||
|
||||
/** @var Cookies */
|
||||
private $cookies;
|
||||
|
||||
/** @var SubscriberCookie */
|
||||
private $subscriberCookie;
|
||||
|
||||
/** @var Shortcodes */
|
||||
private $shortcodes;
|
||||
|
||||
/** @var LinkShortcodeCategory */
|
||||
private $linkShortcodeCategory;
|
||||
|
||||
/** @var Opens */
|
||||
private $opens;
|
||||
|
||||
/** @var StatisticsClicksRepository */
|
||||
private $statisticsClicksRepository;
|
||||
|
||||
/** @var UserAgentsRepository */
|
||||
private $userAgentsRepository;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
public function __construct(
|
||||
Cookies $cookies,
|
||||
SubscriberCookie $subscriberCookie,
|
||||
Shortcodes $shortcodes,
|
||||
Opens $opens,
|
||||
StatisticsClicksRepository $statisticsClicksRepository,
|
||||
UserAgentsRepository $userAgentsRepository,
|
||||
LinkShortcodeCategory $linkShortcodeCategory,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
TrackingConfig $trackingConfig
|
||||
) {
|
||||
$this->cookies = $cookies;
|
||||
$this->subscriberCookie = $subscriberCookie;
|
||||
$this->shortcodes = $shortcodes;
|
||||
$this->linkShortcodeCategory = $linkShortcodeCategory;
|
||||
$this->opens = $opens;
|
||||
$this->statisticsClicksRepository = $statisticsClicksRepository;
|
||||
$this->userAgentsRepository = $userAgentsRepository;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \stdClass|null $data
|
||||
*/
|
||||
public function track($data) {
|
||||
if (!$data || empty($data->link)) {
|
||||
return $this->abort();
|
||||
}
|
||||
/** @var SubscriberEntity $subscriber */
|
||||
$subscriber = $data->subscriber;
|
||||
/** @var SendingQueueEntity $queue */
|
||||
$queue = $data->queue;
|
||||
/** @var NewsletterEntity $newsletter */
|
||||
$newsletter = $data->newsletter;
|
||||
/** @var NewsletterLinkEntity $link */
|
||||
$link = $data->link;
|
||||
$wpUserPreview = ($data->preview && ($subscriber->isWPUser()));
|
||||
// log statistics only if the action did not come from
|
||||
// a WP user previewing the newsletter
|
||||
if (!$wpUserPreview) {
|
||||
$userAgent = !empty($data->userAgent) ? $this->userAgentsRepository->findOrCreate($data->userAgent) : null;
|
||||
$statisticsClicks = $this->statisticsClicksRepository->createOrUpdateClickCount(
|
||||
$link,
|
||||
$subscriber,
|
||||
$newsletter,
|
||||
$queue,
|
||||
$userAgent
|
||||
);
|
||||
if (
|
||||
$userAgent instanceof UserAgentEntity &&
|
||||
($userAgent->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_HUMAN
|
||||
|| $statisticsClicks->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_MACHINE)
|
||||
) {
|
||||
$statisticsClicks->setUserAgent($userAgent);
|
||||
$statisticsClicks->setUserAgentType($userAgent->getUserAgentType());
|
||||
}
|
||||
$this->statisticsClicksRepository->flush();
|
||||
$this->sendRevenueCookie($statisticsClicks);
|
||||
|
||||
$subscriberId = $subscriber->getId();
|
||||
if ($subscriberId) {
|
||||
$this->subscriberCookie->setSubscriberId($subscriberId);
|
||||
}
|
||||
|
||||
// track open event
|
||||
$this->opens->track($data, $displayImage = false);
|
||||
// Update engagement date
|
||||
$this->subscribersRepository->maybeUpdateLastClickAt($subscriber);
|
||||
}
|
||||
$url = $this->processUrl($link->getUrl(), $newsletter, $subscriber, $queue, $wpUserPreview);
|
||||
do_action('mailpoet_link_clicked', $link, $subscriber, $wpUserPreview);
|
||||
$this->redirectToUrl($url);
|
||||
}
|
||||
|
||||
private function sendRevenueCookie(StatisticsClickEntity $clicks) {
|
||||
if ($this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
$this->cookies->set(
|
||||
self::REVENUE_TRACKING_COOKIE_NAME,
|
||||
[
|
||||
'statistics_clicks' => $clicks->getId(),
|
||||
'created_at' => time(),
|
||||
],
|
||||
[
|
||||
'expires' => time() + self::REVENUE_TRACKING_COOKIE_EXPIRY,
|
||||
'path' => '/',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function processUrl(
|
||||
string $url,
|
||||
NewsletterEntity $newsletter,
|
||||
SubscriberEntity $subscriber,
|
||||
SendingQueueEntity $queue,
|
||||
bool $wpUserPreview
|
||||
) {
|
||||
if (preg_match('/\[link:(?P<action>.*?)\]/', $url, $shortcode)) {
|
||||
if (!$shortcode['action']) $this->abort();
|
||||
$url = $this->linkShortcodeCategory->processShortcodeAction(
|
||||
$shortcode['action'],
|
||||
$newsletter,
|
||||
$subscriber,
|
||||
$queue,
|
||||
$wpUserPreview
|
||||
);
|
||||
} else {
|
||||
$this->shortcodes->setQueue($queue);
|
||||
$this->shortcodes->setNewsletter($newsletter);
|
||||
$this->shortcodes->setSubscriber($subscriber);
|
||||
$this->shortcodes->setWpUserPreview($wpUserPreview);
|
||||
$url = $this->shortcodes->replace($url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function abort() {
|
||||
global $wp_query;// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
WPFunctions::get()->statusHeader(404);
|
||||
$wp_query->set_404();// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
WPFunctions::get()->getTemplatePart((string)404);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function redirectToUrl($url) {
|
||||
header('Location: ' . $url, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Entities\StatisticsOpenEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Entities\UserAgentEntity;
|
||||
use MailPoet\Statistics\StatisticsOpensRepository;
|
||||
use MailPoet\Statistics\UserAgentsRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
|
||||
class Opens {
|
||||
/** @var StatisticsOpensRepository */
|
||||
private $statisticsOpensRepository;
|
||||
|
||||
/** @var UserAgentsRepository */
|
||||
private $userAgentsRepository;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
public function __construct(
|
||||
StatisticsOpensRepository $statisticsOpensRepository,
|
||||
UserAgentsRepository $userAgentsRepository,
|
||||
SubscribersRepository $subscribersRepository
|
||||
) {
|
||||
$this->statisticsOpensRepository = $statisticsOpensRepository;
|
||||
$this->userAgentsRepository = $userAgentsRepository;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
}
|
||||
|
||||
public function track($data, $displayImage = true) {
|
||||
if (!$data) {
|
||||
return $this->returnResponse($displayImage);
|
||||
}
|
||||
/** @var SubscriberEntity $subscriber */
|
||||
$subscriber = $data->subscriber;
|
||||
/** @var SendingQueueEntity $queue */
|
||||
$queue = $data->queue;
|
||||
/** @var NewsletterEntity $newsletter */
|
||||
$newsletter = $data->newsletter;
|
||||
$wpUserPreview = ($data->preview && ($subscriber->isWPUser()));
|
||||
// log statistics only if the action did not come from
|
||||
// a WP user previewing the newsletter
|
||||
if (!$wpUserPreview) {
|
||||
$oldStatistics = $this->statisticsOpensRepository->findOneBy([
|
||||
'subscriber' => $subscriber->getId(),
|
||||
'newsletter' => $newsletter->getId(),
|
||||
'queue' => $queue->getId(),
|
||||
]);
|
||||
// Open was already tracked
|
||||
if ($oldStatistics) {
|
||||
if (!empty($data->userAgent)) {
|
||||
$userAgent = $this->userAgentsRepository->findOrCreate($data->userAgent);
|
||||
if (
|
||||
$userAgent->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_HUMAN
|
||||
|| $oldStatistics->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_MACHINE
|
||||
) {
|
||||
$oldStatistics->setUserAgent($userAgent);
|
||||
$oldStatistics->setUserAgentType($userAgent->getUserAgentType());
|
||||
$this->statisticsOpensRepository->flush();
|
||||
}
|
||||
}
|
||||
$this->subscribersRepository->maybeUpdateLastOpenAt($subscriber);
|
||||
return $this->returnResponse($displayImage);
|
||||
}
|
||||
$statistics = new StatisticsOpenEntity($newsletter, $queue, $subscriber);
|
||||
if (!empty($data->userAgent)) {
|
||||
$userAgent = $this->userAgentsRepository->findOrCreate($data->userAgent);
|
||||
$statistics->setUserAgent($userAgent);
|
||||
$statistics->setUserAgentType($userAgent->getUserAgentType());
|
||||
}
|
||||
$this->statisticsOpensRepository->persist($statistics);
|
||||
$this->statisticsOpensRepository->flush();
|
||||
$this->subscribersRepository->maybeUpdateLastOpenAt($subscriber);
|
||||
$this->statisticsOpensRepository->recalculateSubscriberScore($subscriber);
|
||||
}
|
||||
return $this->returnResponse($displayImage);
|
||||
}
|
||||
|
||||
public function returnResponse($displayImage) {
|
||||
if (!$displayImage) return;
|
||||
// return 1x1 pixel transparent gif image
|
||||
header('Content-Type: image/gif');
|
||||
|
||||
// Output of base64_decode is predetermined and safe in this case
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo base64_decode('R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\Util\Cookies;
|
||||
|
||||
class PageViewCookie {
|
||||
const COOKIE_NAME = 'mailpoet_page_view';
|
||||
const COOKIE_EXPIRY = 10 * 365 * 24 * 60 * 60; // 10 years (~ no expiry)
|
||||
|
||||
/** @var Cookies */
|
||||
private $cookies;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
public function __construct(
|
||||
Cookies $cookies,
|
||||
TrackingConfig $trackingConfig
|
||||
) {
|
||||
$this->cookies = $cookies;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
}
|
||||
|
||||
public function getPageViewTimestamp(): ?int {
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getTimestampCookie(self::COOKIE_NAME);
|
||||
}
|
||||
|
||||
public function setPageViewTimestamp(int $timestamp): void {
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cookies->set(
|
||||
self::COOKIE_NAME,
|
||||
['timestamp' => $timestamp],
|
||||
[
|
||||
'expires' => time() + self::COOKIE_EXPIRY,
|
||||
'path' => '/',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getTimestampCookie(string $cookieName): ?int {
|
||||
$data = $this->cookies->get($cookieName);
|
||||
return is_array($data) && $data['timestamp']
|
||||
? (int)$data['timestamp']
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\WooCommerce\Helper as WooCommerceHelper;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class SubscriberActivityTracker {
|
||||
|
||||
const TRACK_INTERVAL = 60; // 1 minute
|
||||
|
||||
/** @var PageViewCookie */
|
||||
private $pageViewCookie;
|
||||
|
||||
/** @var SubscriberCookie */
|
||||
private $subscriberCookie;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
/** @var callable[] */
|
||||
private $callbacks = [];
|
||||
|
||||
/** @var WooCommerceHelper */
|
||||
private $wooCommerceHelper;
|
||||
|
||||
public function __construct(
|
||||
PageViewCookie $pageViewCookie,
|
||||
SubscriberCookie $subscriberCookie,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
WPFunctions $wp,
|
||||
WooCommerceHelper $wooCommerceHelper,
|
||||
TrackingConfig $trackingConfig
|
||||
) {
|
||||
$this->pageViewCookie = $pageViewCookie;
|
||||
$this->subscriberCookie = $subscriberCookie;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->wp = $wp;
|
||||
$this->wooCommerceHelper = $wooCommerceHelper;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
}
|
||||
|
||||
public function trackActivity(): bool {
|
||||
// Don't track in admin interface
|
||||
if ($this->wp->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$subscriber = null;
|
||||
$latestTimestamp = $this->getLatestTimestampFromCookie();
|
||||
|
||||
// If cookie tracking is not allowed try use last activity from subscriber data
|
||||
if ($latestTimestamp === null) {
|
||||
$subscriber = $this->getSubscriber();
|
||||
if (!$subscriber) {
|
||||
return false; // Can't determine timestamp
|
||||
}
|
||||
$latestTimestamp = $this->getLatestTimestampFromSubscriber($subscriber);
|
||||
}
|
||||
|
||||
if ($latestTimestamp + self::TRACK_INTERVAL > $this->wp->currentTime('timestamp', true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($subscriber === null) {
|
||||
$subscriber = $this->getSubscriber();
|
||||
}
|
||||
|
||||
if (!$subscriber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->processTracking($subscriber);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function registerCallback(string $slug, callable $callback): void {
|
||||
$this->callbacks[$slug] = $callback;
|
||||
}
|
||||
|
||||
public function unregisterCallback(string $slug): void {
|
||||
unset($this->callbacks[$slug]);
|
||||
}
|
||||
|
||||
private function processTracking(SubscriberEntity $subscriber): void {
|
||||
$this->subscribersRepository->maybeUpdateLastPageViewAt($subscriber);
|
||||
$this->pageViewCookie->setPageViewTimestamp($this->wp->currentTime('timestamp', true));
|
||||
foreach ($this->callbacks as $callback) {
|
||||
$callback($subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
private function getLatestTimestampFromCookie(): ?int {
|
||||
if ($this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return $this->pageViewCookie->getPageViewTimestamp() ?? 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getLatestTimestampFromSubscriber(SubscriberEntity $subscriber): int {
|
||||
return $subscriber->getLastEngagementAt() ? $subscriber->getLastEngagementAt()->getTimestamp() : 0;
|
||||
}
|
||||
|
||||
private function getSubscriber(): ?SubscriberEntity {
|
||||
$wpUser = $this->wp->wpGetCurrentUser();
|
||||
if ($wpUser->exists()) {
|
||||
return $this->subscribersRepository->findOneBy(['wpUserId' => $wpUser->ID]);
|
||||
}
|
||||
|
||||
$subscriberId = $this->subscriberCookie->getSubscriberId();
|
||||
if ($subscriberId) {
|
||||
return $this->subscribersRepository->findOneById($subscriberId);
|
||||
}
|
||||
|
||||
if (!$this->wooCommerceHelper->isWooCommerceActive()) {
|
||||
return null;
|
||||
}
|
||||
$wooCommerce = $this->wooCommerceHelper->WC();
|
||||
if (!$wooCommerce || !$wooCommerce->session) {
|
||||
return null;
|
||||
}
|
||||
$customer = $wooCommerce->session->get('customer');
|
||||
if (!is_array($customer) || empty($customer['email'])) {
|
||||
return null;
|
||||
}
|
||||
return $this->subscribersRepository->findOneBy(['email' => $customer['email']]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\Util\Cookies;
|
||||
|
||||
class SubscriberCookie {
|
||||
const COOKIE_NAME = 'mailpoet_subscriber';
|
||||
const COOKIE_NAME_LEGACY = 'mailpoet_abandoned_cart_tracking';
|
||||
const COOKIE_EXPIRY = 10 * 365 * 24 * 60 * 60; // 10 years (~ no expiry)
|
||||
|
||||
/** @var Cookies */
|
||||
private $cookies;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
public function __construct(
|
||||
Cookies $cookies,
|
||||
TrackingConfig $trackingConfig
|
||||
) {
|
||||
$this->cookies = $cookies;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
}
|
||||
|
||||
public function getSubscriberId(): ?int {
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$subscriberId = $this->getSubscriberIdFromCookie(self::COOKIE_NAME);
|
||||
if ($subscriberId) {
|
||||
return $subscriberId;
|
||||
}
|
||||
|
||||
$subscriberId = $this->getSubscriberIdFromCookie(self::COOKIE_NAME_LEGACY);
|
||||
if ($subscriberId) {
|
||||
$this->setSubscriberId($subscriberId);
|
||||
$this->cookies->delete(self::COOKIE_NAME_LEGACY);
|
||||
return $subscriberId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setSubscriberId(int $subscriberId): void {
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->cookies->set(
|
||||
self::COOKIE_NAME,
|
||||
['subscriber_id' => $subscriberId],
|
||||
[
|
||||
'expires' => time() + self::COOKIE_EXPIRY,
|
||||
'path' => '/',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getSubscriberIdFromCookie(string $cookieName): ?int {
|
||||
$data = $this->cookies->get($cookieName);
|
||||
return is_array($data) && $data['subscriber_id']
|
||||
? (int)$data['subscriber_id']
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Settings\TrackingConfig;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class SubscriberHandler {
|
||||
/** @var SubscriberCookie */
|
||||
private $subscriberCookie;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var TrackingConfig */
|
||||
private $trackingConfig;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
SubscriberCookie $subscriberCookie,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
TrackingConfig $trackingConfig,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->subscriberCookie = $subscriberCookie;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->trackingConfig = $trackingConfig;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function identifyByLogin(?string $login): void {
|
||||
if (is_null($login)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wpUser = $this->wp->getUserBy('login', $login);
|
||||
if ($wpUser) {
|
||||
$this->identifyByEmail($wpUser->user_email); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
|
||||
}
|
||||
}
|
||||
|
||||
public function identifyByEmail(string $email): void {
|
||||
if (!$this->trackingConfig->isCookieTrackingEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['email' => $email]);
|
||||
if ($subscriber) {
|
||||
$this->setCookieBySubscriber($subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
private function setCookieBySubscriber(SubscriberEntity $subscriber): void {
|
||||
$subscriberId = $subscriber->getId();
|
||||
if ($subscriberId) {
|
||||
$this->subscriberCookie->setSubscriberId($subscriberId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\SendingQueueEntity;
|
||||
use MailPoet\Entities\StatisticsUnsubscribeEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Newsletter\Sending\SendingQueuesRepository;
|
||||
use MailPoet\Statistics\StatisticsUnsubscribesRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
|
||||
class Unsubscribes {
|
||||
/** @var SendingQueuesRepository */
|
||||
private $sendingQueuesRepository;
|
||||
|
||||
/** @var StatisticsUnsubscribesRepository */
|
||||
private $statisticsUnsubscribesRepository;
|
||||
|
||||
/**
|
||||
* @var SubscribersRepository
|
||||
*/
|
||||
private $subscribersRepository;
|
||||
|
||||
public function __construct(
|
||||
SendingQueuesRepository $sendingQueuesRepository,
|
||||
StatisticsUnsubscribesRepository $statisticsUnsubscribesRepository,
|
||||
SubscribersRepository $subscribersRepository
|
||||
) {
|
||||
$this->sendingQueuesRepository = $sendingQueuesRepository;
|
||||
$this->statisticsUnsubscribesRepository = $statisticsUnsubscribesRepository;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
}
|
||||
|
||||
public function track(
|
||||
int $subscriberId,
|
||||
string $source,
|
||||
int $queueId = null,
|
||||
string $meta = null,
|
||||
string $method = StatisticsUnsubscribeEntity::METHOD_UNKNOWN
|
||||
) {
|
||||
$queue = null;
|
||||
$statistics = null;
|
||||
if ($queueId) {
|
||||
$queue = $this->sendingQueuesRepository->findOneById($queueId);
|
||||
}
|
||||
$subscriber = $this->subscribersRepository->findOneById($subscriberId);
|
||||
if (!$subscriber instanceof SubscriberEntity) {
|
||||
return;
|
||||
}
|
||||
if (($queue instanceof SendingQueueEntity)) {
|
||||
$newsletter = $queue->getNewsletter();
|
||||
if ($newsletter instanceof NewsletterEntity) {
|
||||
$statistics = $this->statisticsUnsubscribesRepository->findOneBy(
|
||||
[
|
||||
'queue' => $queue,
|
||||
'newsletter' => $newsletter,
|
||||
'subscriber' => $subscriber,
|
||||
]
|
||||
);
|
||||
if (!$statistics) {
|
||||
$statistics = new StatisticsUnsubscribeEntity($newsletter, $queue, $subscriber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($statistics === null) {
|
||||
$statistics = new StatisticsUnsubscribeEntity(null, null, $subscriber);
|
||||
}
|
||||
if ($meta !== null) {
|
||||
$statistics->setMeta($meta);
|
||||
}
|
||||
$statistics->setSource($source);
|
||||
$statistics->setMethod($method);
|
||||
$this->statisticsUnsubscribesRepository->persist($statistics);
|
||||
$this->statisticsUnsubscribesRepository->flush();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Statistics\Track;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Entities\StatisticsClickEntity;
|
||||
use MailPoet\Entities\SubscriberEntity;
|
||||
use MailPoet\Statistics\StatisticsClicksRepository;
|
||||
use MailPoet\Statistics\StatisticsWooCommercePurchasesRepository;
|
||||
use MailPoet\Subscribers\SubscribersRepository;
|
||||
use MailPoet\Util\Cookies;
|
||||
use MailPoet\WooCommerce\Helper;
|
||||
use WC_Order;
|
||||
|
||||
class WooCommercePurchases {
|
||||
const USE_CLICKS_SINCE_DAYS_AGO = 14;
|
||||
|
||||
/** @var Helper */
|
||||
private $woocommerceHelper;
|
||||
|
||||
/** @var Cookies */
|
||||
private $cookies;
|
||||
|
||||
/** @var StatisticsWooCommercePurchasesRepository */
|
||||
private $statisticsWooCommercePurchasesRepository;
|
||||
|
||||
/** @var StatisticsClicksRepository */
|
||||
private $statisticsClicksRepository;
|
||||
|
||||
/** @var SubscribersRepository */
|
||||
private $subscribersRepository;
|
||||
|
||||
/** @var SubscriberHandler */
|
||||
private $subscriberHandler;
|
||||
|
||||
public function __construct(
|
||||
Helper $woocommerceHelper,
|
||||
StatisticsWooCommercePurchasesRepository $statisticsWooCommercePurchasesRepository,
|
||||
StatisticsClicksRepository $statisticsClicksRepository,
|
||||
SubscribersRepository $subscribersRepository,
|
||||
Cookies $cookies,
|
||||
SubscriberHandler $subscriberHandler
|
||||
) {
|
||||
$this->woocommerceHelper = $woocommerceHelper;
|
||||
$this->cookies = $cookies;
|
||||
$this->statisticsWooCommercePurchasesRepository = $statisticsWooCommercePurchasesRepository;
|
||||
$this->statisticsClicksRepository = $statisticsClicksRepository;
|
||||
$this->subscribersRepository = $subscribersRepository;
|
||||
$this->subscriberHandler = $subscriberHandler;
|
||||
}
|
||||
|
||||
public function trackPurchase($id, $useCookies = true) {
|
||||
|
||||
$order = $this->woocommerceHelper->wcGetOrder($id);
|
||||
if (!$order instanceof WC_Order || $this->trackExistingStatistics($order)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$from = $this->getFromDate($order);
|
||||
$to = $order->get_date_created();
|
||||
if (is_null($to) || is_null($from)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// track purchases from all clicks matched by order email
|
||||
$processedNewsletterIdsMap = [];
|
||||
$orderEmailClicks = $this->getClicks($order->get_billing_email(), $from, $to);
|
||||
foreach ($orderEmailClicks as $click) {
|
||||
$this->statisticsWooCommercePurchasesRepository->createOrUpdateByClickDataAndOrder($click, $order);
|
||||
$newsletter = $click->getNewsletter();
|
||||
if (!$newsletter instanceof NewsletterEntity) continue;
|
||||
$processedNewsletterIdsMap[$newsletter->getId()] = true;
|
||||
}
|
||||
|
||||
// try to find a subscriber by order email and start tracking
|
||||
$this->subscriberHandler->identifyByEmail($order->get_billing_email());
|
||||
|
||||
if (!$useCookies) {
|
||||
return;
|
||||
}
|
||||
|
||||
// track purchases from clicks matched by cookie email (only for newsletters not tracked by order)
|
||||
$cookieEmailClicks = $this->getClicks($this->getSubscriberEmailFromCookie(), $from, $to);
|
||||
foreach ($cookieEmailClicks as $click) {
|
||||
$newsletter = $click->getNewsletter();
|
||||
if (!$newsletter instanceof NewsletterEntity) continue;
|
||||
if (isset($processedNewsletterIdsMap[$newsletter->getId()])) {
|
||||
continue; // do not track click for newsletters that were already tracked by order email
|
||||
}
|
||||
$this->statisticsWooCommercePurchasesRepository->createOrUpdateByClickDataAndOrder($click, $order);
|
||||
}
|
||||
}
|
||||
|
||||
public function trackRefund($id) {
|
||||
$order = $this->woocommerceHelper->wcGetOrder($id);
|
||||
if (!$order instanceof WC_Order) {
|
||||
return;
|
||||
}
|
||||
$this->trackExistingStatistics($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when valid purchase statistics for an order were found.
|
||||
*
|
||||
* @param WC_Order $order
|
||||
* @return bool
|
||||
*/
|
||||
private function trackExistingStatistics(\WC_Order $order): bool {
|
||||
$statistics = $this->statisticsWooCommercePurchasesRepository->findBy(['orderId' => $order->get_id()]);
|
||||
if ($statistics) {
|
||||
foreach ($statistics as $statistic) {
|
||||
if (!$statistic->getClick()) {
|
||||
continue;
|
||||
}
|
||||
$this->statisticsWooCommercePurchasesRepository->createOrUpdateByClickDataAndOrder(
|
||||
$statistic->getClick(),
|
||||
$order
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit clicks to 'USE_CLICKS_SINCE_DAYS_AGO' range before order has been created.
|
||||
*
|
||||
* @param WC_Order $order
|
||||
* @return \WC_DateTime|null
|
||||
*/
|
||||
private function getFromDate(\WC_Order $order) {
|
||||
$fromDate = $order->get_date_created();
|
||||
if (is_null($fromDate)) {
|
||||
return null;
|
||||
}
|
||||
$from = clone $fromDate;
|
||||
$from->modify(-self::USE_CLICKS_SINCE_DAYS_AGO . ' days');
|
||||
return $from;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $email
|
||||
* @param \DateTimeInterface $from
|
||||
* @param \DateTimeInterface $to
|
||||
* @return StatisticsClickEntity[]
|
||||
*/
|
||||
private function getClicks(?string $email, \DateTimeInterface $from, \DateTimeInterface $to): array {
|
||||
if (!$email) return [];
|
||||
$subscriber = $this->subscribersRepository->findOneBy(['email' => $email]);
|
||||
if (!$subscriber instanceof SubscriberEntity) {
|
||||
return [];
|
||||
}
|
||||
return $this->statisticsClicksRepository->findLatestPerNewsletterBySubscriber($subscriber, $from, $to);
|
||||
}
|
||||
|
||||
private function getSubscriberEmailFromCookie(): ?string {
|
||||
$cookieData = $this->cookies->get(Clicks::REVENUE_TRACKING_COOKIE_NAME);
|
||||
if (!$cookieData) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$click = $this->statisticsClicksRepository->findOneById($cookieData['statistics_clicks']);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
if (!$click instanceof StatisticsClickEntity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$subscriber = $click->getSubscriber();
|
||||
if ($subscriber instanceof SubscriberEntity) {
|
||||
return $subscriber->getEmail();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user