This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -0,0 +1,114 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Util\Helpers;
use MailPoet\Util\SecondLevelDomainNames;
use MailPoet\WP\Functions;
class GATracking {
/** @var SecondLevelDomainNames */
private $secondLevelDomainNames;
/** @var NewsletterLinks */
private $newsletterLinks;
/** @var Functions */
private $wp;
/** @var TrackingConfig */
private $tackingConfig;
public function __construct(
NewsletterLinks $newsletterLinks,
Functions $wp,
TrackingConfig $trackingConfig
) {
$this->secondLevelDomainNames = new SecondLevelDomainNames();
$this->newsletterLinks = $newsletterLinks;
$this->wp = $wp;
$this->tackingConfig = $trackingConfig;
}
public function applyGATracking($renderedNewsletter, NewsletterEntity $newsletter, $internalHost = null) {
if (!$this->tackingConfig->isEmailTrackingEnabled()) {
return $renderedNewsletter;
}
if ($newsletter->getType() == NewsletterEntity::TYPE_NOTIFICATION_HISTORY && $newsletter->getParent() instanceof NewsletterEntity) {
$parentNewsletter = $newsletter->getParent();
$field = $parentNewsletter->getGaCampaign();
} else {
$field = $newsletter->getGaCampaign();
}
return $this->addGAParamsToLinks($renderedNewsletter, $field, $internalHost);
}
private function addGAParamsToLinks($renderedNewsletter, $gaCampaign, $internalHost = null) {
// join HTML and TEXT rendered body into a text string
$content = Helpers::joinObject($renderedNewsletter);
$extractedLinks = $this->newsletterLinks->extract($content);
$processedLinks = $this->addParams($extractedLinks, $gaCampaign, $internalHost);
list($content, $links) = $this->newsletterLinks->replace($content, $processedLinks);
// split the processed body with hashed links back to HTML and TEXT
list($renderedNewsletter['html'], $renderedNewsletter['text'])
= Helpers::splitObject($content);
return $renderedNewsletter;
}
private function addParams($extractedLinks, $gaCampaign, $internalHost = null) {
$processedLinks = [];
$params = [
'utm_source' => 'mailpoet',
'utm_medium' => 'email',
'utm_source_platform' => 'mailpoet',
];
if ($gaCampaign) {
$params['utm_campaign'] = $gaCampaign;
}
$internalHost = $internalHost ?: parse_url(home_url(), PHP_URL_HOST);
$internalHost = $this->secondLevelDomainNames->get($internalHost);
foreach ($extractedLinks as $extractedLink) {
if ($extractedLink['type'] !== NewsletterLinks::LINK_TYPE_URL) {
continue;
} elseif (strpos((string)parse_url($extractedLink['link'], PHP_URL_HOST), $internalHost) === false) {
// Process only internal links (i.e. pointing to current site)
continue;
}
$link = $extractedLink['link'];
// Do not overwrite existing query parameters
$parsedUrl = parse_url($link);
$linkParams = $params;
if (isset($parsedUrl['query'])) {
foreach (array_keys($params) as $param) {
if (strpos($parsedUrl['query'], $param . '=') !== false) {
unset($linkParams[$param]);
}
}
}
$processedLink = $this->wp->applyFilters(
'mailpoet_ga_tracking_link',
$this->wp->addQueryArg($linkParams, $link),
$extractedLink['link'],
$linkParams,
$extractedLink['type']
);
$processedLinks[$link] = [
'type' => $extractedLink['type'],
'link' => $link,
'processed_link' => $processedLink,
];
}
return $processedLinks;
}
}
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\StatisticsBounceEntity;
/**
* @extends Repository<StatisticsBounceEntity>
*/
class StatisticsBouncesRepository extends Repository {
protected function getEntityClassName(): string {
return StatisticsBounceEntity::class;
}
}
@@ -0,0 +1,111 @@
<?php declare(strict_types = 1);
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
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 MailPoetVendor\Doctrine\ORM\QueryBuilder;
/**
* @extends Repository<StatisticsClickEntity>
*/
class StatisticsClicksRepository extends Repository {
protected function getEntityClassName(): string {
return StatisticsClickEntity::class;
}
public function createOrUpdateClickCount(
NewsletterLinkEntity $link,
SubscriberEntity $subscriber,
NewsletterEntity $newsletter,
SendingQueueEntity $queue,
?UserAgentEntity $userAgent
): StatisticsClickEntity {
$statistics = $this->findOneBy([
'link' => $link,
'newsletter' => $newsletter,
'subscriber' => $subscriber,
'queue' => $queue,
]);
if (!$statistics instanceof StatisticsClickEntity) {
$statistics = new StatisticsClickEntity($newsletter, $queue, $subscriber, $link, 1);
if ($userAgent) {
$statistics->setUserAgent($userAgent);
$statistics->setUserAgentType($userAgent->getUserAgentType());
}
$this->persist($statistics);
} else {
$statistics->setCount($statistics->getCount() + 1);
}
return $statistics;
}
public function getAllForSubscriber(SubscriberEntity $subscriber): QueryBuilder {
return $this->entityManager->createQueryBuilder()
->select('clicks.id id, queue.newsletterRenderedSubject, clicks.createdAt, link.url, userAgent.userAgent')
->from(StatisticsClickEntity::class, 'clicks')
->join('clicks.queue', 'queue')
->join('clicks.link', 'link')
->leftJoin('clicks.userAgent', 'userAgent')
->where('clicks.subscriber = :subscriber')
->orderBy('link.url')
->setParameter('subscriber', $subscriber->getId());
}
/**
* @param SubscriberEntity $subscriber
* @param \DateTimeInterface $from
* @param \DateTimeInterface $to
* @return StatisticsClickEntity[]
*/
public function findLatestPerNewsletterBySubscriber(SubscriberEntity $subscriber, \DateTimeInterface $from, \DateTimeInterface $to): array {
// subquery to find latest click IDs for each newsletter
$latestClickIdsPerNewsletterQuery = $this->entityManager->createQueryBuilder()
->select('MAX(clicks.id)')
->from(StatisticsClickEntity::class, 'clicks')
->where('clicks.subscriber = :subscriber')
->andWhere('clicks.updatedAt > :from')
->andWhere('clicks.updatedAt < :to')
->groupBy('clicks.newsletter');
$expr = $this->entityManager->getExpressionBuilder();
return $this->entityManager->createQueryBuilder()
->select('c')
->from(StatisticsClickEntity::class, 'c')
->where(
$expr->in(
'c.id',
$latestClickIdsPerNewsletterQuery->getDQL()
)
)
->setParameter('subscriber', $subscriber)
->setParameter('from', $from->format('Y-m-d H:i:s'))
->setParameter('to', $to->format('Y-m-d H:i:s'))
->getQuery()
->getResult();
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsClickEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsClickEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -0,0 +1,40 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\FormEntity;
use MailPoet\Entities\StatisticsFormEntity;
use MailPoet\Entities\SubscriberEntity;
/**
* @extends Repository<StatisticsFormEntity>
*/
class StatisticsFormsRepository extends Repository {
protected function getEntityClassName() {
return StatisticsFormEntity::class;
}
public function getTotalSignups(int $formId): int {
return $this->countBy(['form' => $formId]);
}
public function record(FormEntity $form, SubscriberEntity $subscriber): ?StatisticsFormEntity {
if ($form->getId() > 0 && $subscriber->getId() > 0) {
// check if we already have a record for today
$statisticsForm = $this->findOneBy(['form' => $form, 'subscriber' => $subscriber]);
if (!$statisticsForm) {
// create a new entry
$statisticsForm = new StatisticsFormEntity($form, $subscriber);
$this->persist($statisticsForm);
$this->flush();
}
return $statisticsForm;
}
return null;
}
}
@@ -0,0 +1,64 @@
<?php declare(strict_types = 1);
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsNewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoetVendor\Carbon\Carbon;
/**
* @extends Repository<StatisticsNewsletterEntity>
*/
class StatisticsNewslettersRepository extends Repository {
protected function getEntityClassName() {
return StatisticsNewsletterEntity::class;
}
public function createMultiple(array $data): void {
$entities = [];
foreach ($data as $value) {
if (!empty($value['newsletter_id']) && !empty($value['queue_id']) && !empty($value['subscriber_id'])) {
$newsletter = $this->entityManager->getReference(NewsletterEntity::class, $value['newsletter_id']);
$queue = $this->entityManager->getReference(SendingQueueEntity::class, $value['queue_id']);
$subscriber = $this->entityManager->getReference(SubscriberEntity::class, $value['subscriber_id']);
if (!$newsletter || !$queue || !$subscriber) {
continue;
}
$sentAt = Carbon::now()->millisecond(0);
$entity = new StatisticsNewsletterEntity($newsletter, $queue, $subscriber, $sentAt);
$this->entityManager->persist($entity);
$entities[] = $entity;
}
}
if (count($entities)) {
$this->entityManager->flush();
}
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsNewsletterEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsNewsletterEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -0,0 +1,112 @@
<?php declare(strict_types = 1);
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\SegmentEntity;
use MailPoet\Entities\StatisticsOpenEntity;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Subscribers\Statistics\SubscriberStatisticsRepository;
use MailPoetVendor\Carbon\Carbon;
use MailPoetVendor\Doctrine\ORM\EntityManager;
use MailPoetVendor\Doctrine\ORM\QueryBuilder;
/**
* @extends Repository<StatisticsOpenEntity>
*/
class StatisticsOpensRepository extends Repository {
/** @var SubscriberStatisticsRepository */
private $subscriberStatisticsRepository;
public function __construct(
EntityManager $entityManager,
SubscriberStatisticsRepository $subscriberStatisticsRepository
) {
parent::__construct($entityManager);
$this->entityManager = $entityManager;
$this->subscriberStatisticsRepository = $subscriberStatisticsRepository;
}
protected function getEntityClassName(): string {
return StatisticsOpenEntity::class;
}
public function recalculateSubscriberScore(SubscriberEntity $subscriber): void {
$subscriber->setEngagementScoreUpdatedAt(new \DateTimeImmutable());
$yearAgo = Carbon::now()->subYear();
$newslettersSentCount = $this->subscriberStatisticsRepository->getTotalSentCount($subscriber, $yearAgo);
if ($newslettersSentCount < 3) {
$subscriber->setEngagementScore(null);
$this->entityManager->flush();
return;
}
$opensCount = $this->subscriberStatisticsRepository->getStatisticsOpenCount($subscriber, $yearAgo);
$score = ($opensCount / $newslettersSentCount) * 100;
$subscriber->setEngagementScore($score);
$this->entityManager->flush();
}
public function resetSubscribersScoreCalculation() {
$this->entityManager->createQueryBuilder()->update(SubscriberEntity::class, 's')
->set('s.engagementScoreUpdatedAt', ':updatedAt')
->setParameter('updatedAt', null)
->getQuery()->execute();
}
public function recalculateSegmentScore(SegmentEntity $segment): void {
$segment->setAverageEngagementScoreUpdatedAt(new \DateTimeImmutable());
$avgScore = $this
->entityManager
->createQueryBuilder()
->select('avg(subscriber.engagementScore)')
->from(SubscriberEntity::class, 'subscriber')
->join('subscriber.subscriberSegments', 'subscriberSegments')
->where('subscriberSegments.segment = :segment')
->andWhere('subscriber.status = :subscribed')
->andWhere('subscriber.deletedAt IS NULL')
->andWhere('subscriberSegments.status = :subscribed')
->setParameter('segment', $segment)
->setParameter('subscribed', SubscriberEntity::STATUS_SUBSCRIBED)
->getQuery()
->getSingleScalarResult();
$segment->setAverageEngagementScore($avgScore === null ? $avgScore : (float)$avgScore);
$this->entityManager->flush();
}
public function resetSegmentsScoreCalculation(): void {
$this->entityManager->createQueryBuilder()->update(SegmentEntity::class, 's')
->set('s.averageEngagementScoreUpdatedAt', ':updatedAt')
->setParameter('updatedAt', null)
->getQuery()->execute();
}
public function getAllForSubscriber(SubscriberEntity $subscriber): QueryBuilder {
return $this->entityManager->createQueryBuilder()
->select('opens.id id, queue.newsletterRenderedSubject, opens.createdAt, userAgent.userAgent')
->from(StatisticsOpenEntity::class, 'opens')
->join('opens.queue', 'queue')
->leftJoin('opens.userAgent', 'userAgent')
->where('opens.subscriber = :subscriber')
->orderBy('queue.newsletterRenderedSubject')
->setParameter('subscriber', $subscriber->getId());
}
/** @param int[] $ids */
public function deleteByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->delete(StatisticsOpenEntity::class, 's')
->where('s.newsletter IN (:ids)')
->setParameter('ids', $ids)
->getQuery()
->execute();
// delete was done via DQL, make sure the entities are also detached from the entity manager
$this->detachAll(function (StatisticsOpenEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -0,0 +1,44 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\StatisticsUnsubscribeEntity;
use MailPoetVendor\Carbon\Carbon;
/**
* @extends Repository<StatisticsUnsubscribeEntity>
*/
class StatisticsUnsubscribesRepository extends Repository {
protected function getEntityClassName() {
return StatisticsUnsubscribeEntity::class;
}
public function getTotalForMonths(int $forMonths): int {
$from = (new Carbon())->subMonths($forMonths);
$count = $this->entityManager->createQueryBuilder()
->select('count(stats.id)')
->from(StatisticsUnsubscribeEntity::class, 'stats')
->andWhere('stats.createdAt >= :dateTime')
->setParameter('dateTime', $from)
->getQuery()
->getSingleScalarResult();
return intval($count);
}
public function getCountPerMethodForMonths(int $forMonths): array {
$from = (new Carbon())->subMonths($forMonths);
return $this->entityManager->createQueryBuilder()
->select('count(stats.id) as count, stats.method as method')
->from(StatisticsUnsubscribeEntity::class, 'stats')
->andWhere('stats.createdAt >= :dateTime')
->groupBy('stats.method')
->setParameter('dateTime', $from)
->getQuery()
->getResult();
}
}
@@ -0,0 +1,136 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SendingQueueEntity;
use MailPoet\Entities\StatisticsClickEntity;
use MailPoet\Entities\StatisticsWooCommercePurchaseEntity;
use MailPoet\WooCommerce\Helper;
use MailPoetVendor\Doctrine\DBAL\ArrayParameterType;
use MailPoetVendor\Doctrine\DBAL\ParameterType;
use MailPoetVendor\Doctrine\ORM\EntityManager;
/**
* @extends Repository<StatisticsWooCommercePurchaseEntity>
*/
class StatisticsWooCommercePurchasesRepository extends Repository {
/** @var Helper */
private $wooCommerceHelper;
public function __construct(
EntityManager $entityManager,
Helper $wooCommerceHelper
) {
parent::__construct($entityManager);
$this->wooCommerceHelper = $wooCommerceHelper;
}
protected function getEntityClassName() {
return StatisticsWooCommercePurchaseEntity::class;
}
public function createOrUpdateByClickDataAndOrder(StatisticsClickEntity $click, \WC_Order $order) {
// search by subscriber and newsletter IDs (instead of click itself) to avoid duplicities
// when a new click from the subscriber appeared since last tracking for given newsletter
// (this will keep the originally tracked click - likely the click that led to the order)
$statistics = $this->findOneBy([
'orderId' => $order->get_id(),
'subscriber' => $click->getSubscriber(),
'newsletter' => $click->getNewsletter(),
]);
if (!$statistics instanceof StatisticsWooCommercePurchaseEntity) {
$newsletter = $click->getNewsletter();
$queue = $click->getQueue();
if ((!$newsletter instanceof NewsletterEntity) || (!$queue instanceof SendingQueueEntity)) return;
$statistics = new StatisticsWooCommercePurchaseEntity(
$newsletter,
$queue,
$click,
$order->get_id(),
$order->get_currency(),
(float)$order->get_remaining_refund_amount(),
$order->get_status()
);
$this->persist($statistics);
} else {
$statistics->setOrderCurrency($order->get_currency());
$statistics->setOrderPriceTotal((float)$order->get_remaining_refund_amount());
$statistics->setStatus($order->get_status());
}
$statistics->setSubscriber($click->getSubscriber());
$this->flush();
}
public function getRevenuesByCampaigns(string $currency): array {
$revenueStatus = $this->wooCommerceHelper->getPurchaseStates();
$revenueStatsTable = $this->entityManager->getClassMetadata(StatisticsWooCommercePurchaseEntity::class)->getTableName();
$newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName();
// The "SELECT MIN(click_id)..." sub-query is used to count each purchase only once.
// In the data we track a purchase to multiple newsletters if clicks from multiple newsletters occurred.
/** @var array<int, array{revenue: float|int, campaign_id:int, orders_count:int}> $data */
$data = $this->entityManager->getConnection()->executeQuery('
SELECT
SUM(swp.order_price_total) AS revenue,
COALESCE(n.parent_id, swp.newsletter_id) AS campaign_id,
(
CASE
WHEN n.type IS NULL THEN \'unknown\'
WHEN n.type = :notification_history_type THEN :notification_type
ELSE n.type
END
) AS campaign_type,
COUNT(order_id) as orders_count
FROM ' . $revenueStatsTable . ' swp
LEFT JOIN ' . $newsletterTable . ' n ON
n.id = swp.newsletter_id
WHERE
swp.order_currency = :currency
AND swp.status IN (:revenue_status)
AND swp.click_id IN (SELECT MIN(click_id) FROM ' . $revenueStatsTable . ' ss GROUP BY order_id)
GROUP BY campaign_id, n.type;
', [
'notification_history_type' => NewsletterEntity::TYPE_NOTIFICATION_HISTORY,
'notification_type' => NewsletterEntity::TYPE_NOTIFICATION,
'currency' => $currency,
'revenue_status' => $revenueStatus,
], [
'notification_history_type' => ParameterType::STRING,
'notification_type' => ParameterType::STRING,
'currency' => ParameterType::STRING,
'revenue_status' => ArrayParameterType::STRING,
])->fetchAllAssociative();
$data = array_map(function($row) {
$row['revenue'] = round(floatval($row['revenue']), 2);
$row['orders_count'] = intval($row['orders_count']);
return $row;
}, $data);
return $data;
}
/** @param int[] $ids */
public function removeNewsletterDataByNewsletterIds(array $ids): void {
$this->entityManager->createQueryBuilder()
->update(StatisticsWooCommercePurchaseEntity::class, 'swp')
->set('swp.newsletter', ':newsletter')
->where('swp.newsletter IN (:ids)')
->setParameter('newsletter', null)
->setParameter('ids', $ids)
->getQuery()
->execute();
// update was done via DQL, make sure the entities are also refreshed in the entity manager
$this->refreshAll(function (StatisticsWooCommercePurchaseEntity $entity) use ($ids) {
$newsletter = $entity->getNewsletter();
return $newsletter && in_array($newsletter->getId(), $ids, true);
});
}
}
@@ -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
@@ -0,0 +1,40 @@
<?php declare(strict_types = 1);
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Doctrine\Repository;
use MailPoet\Entities\UserAgentEntity;
/**
* @extends Repository<UserAgentEntity>
*/
class UserAgentsRepository extends Repository {
protected function getEntityClassName() {
return UserAgentEntity::class;
}
public function findOrCreate(string $userAgent): UserAgentEntity {
$hash = (string)crc32($userAgent);
$userAgentEntity = $this->findOneBy(['hash' => $hash]);
return $userAgentEntity ?? $this->create($userAgent);
}
public function create(string $userAgent): UserAgentEntity {
$userAgentEntity = new UserAgentEntity($userAgent);
$this->entityManager->getConnection()->executeStatement(
'INSERT INTO ' . $this->getTableName() . ' (user_agent, hash) VALUES (:user_agent, :hash) ON DUPLICATE KEY UPDATE id = id',
[
'user_agent' => $userAgentEntity->getUserAgent(),
'hash' => $userAgentEntity->getHash(),
]
);
/** @var UserAgentEntity $userAgentEntity */
$userAgentEntity = $this->findOneBy(['hash' => $userAgentEntity->getHash()]);
return $userAgentEntity;
}
}
@@ -0,0 +1 @@
<?php