Clicks.php
4 days ago
Opens.php
4 days ago
PageViewCookie.php
2 months ago
SubscriberActivityTracker.php
4 days ago
SubscriberCookie.php
2 months ago
SubscriberHandler.php
1 year ago
Unsubscribes.php
2 months ago
WooCommercePurchases.php
2 years ago
index.php
3 years ago
Clicks.php
213 lines
| 1 | <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing |
| 2 | |
| 3 | namespace MailPoet\Statistics\Track; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Entities\NewsletterEntity; |
| 9 | use MailPoet\Entities\NewsletterLinkEntity; |
| 10 | use MailPoet\Entities\SendingQueueEntity; |
| 11 | use MailPoet\Entities\StatisticsClickEntity; |
| 12 | use MailPoet\Entities\SubscriberEntity; |
| 13 | use MailPoet\Entities\UserAgentEntity; |
| 14 | use MailPoet\Newsletter\Shortcodes\Categories\Link as LinkShortcodeCategory; |
| 15 | use MailPoet\Newsletter\Shortcodes\Shortcodes; |
| 16 | use MailPoet\Settings\TrackingConfig; |
| 17 | use MailPoet\Statistics\StatisticsClicksRepository; |
| 18 | use MailPoet\Statistics\UserAgentsRepository; |
| 19 | use MailPoet\Subscribers\SubscribersRepository; |
| 20 | use MailPoet\Subscribers\TrackingConsentController; |
| 21 | use MailPoet\Util\Cookies; |
| 22 | use MailPoet\Util\Request; |
| 23 | use MailPoet\WP\Functions as WPFunctions; |
| 24 | |
| 25 | class Clicks { |
| 26 | |
| 27 | const REVENUE_TRACKING_COOKIE_NAME = 'mailpoet_revenue_tracking'; |
| 28 | const REVENUE_TRACKING_COOKIE_EXPIRY = 60 * 60 * 24 * 14; |
| 29 | |
| 30 | /** @var Cookies */ |
| 31 | private $cookies; |
| 32 | |
| 33 | /** @var SubscriberCookie */ |
| 34 | private $subscriberCookie; |
| 35 | |
| 36 | /** @var Shortcodes */ |
| 37 | private $shortcodes; |
| 38 | |
| 39 | /** @var LinkShortcodeCategory */ |
| 40 | private $linkShortcodeCategory; |
| 41 | |
| 42 | /** @var Opens */ |
| 43 | private $opens; |
| 44 | |
| 45 | /** @var StatisticsClicksRepository */ |
| 46 | private $statisticsClicksRepository; |
| 47 | |
| 48 | /** @var UserAgentsRepository */ |
| 49 | private $userAgentsRepository; |
| 50 | |
| 51 | /** @var SubscribersRepository */ |
| 52 | private $subscribersRepository; |
| 53 | |
| 54 | /** @var TrackingConfig */ |
| 55 | private $trackingConfig; |
| 56 | |
| 57 | /** @var Request */ |
| 58 | private $request; |
| 59 | |
| 60 | /** @var TrackingConsentController */ |
| 61 | private $trackingConsentController; |
| 62 | |
| 63 | public function __construct( |
| 64 | Cookies $cookies, |
| 65 | SubscriberCookie $subscriberCookie, |
| 66 | Shortcodes $shortcodes, |
| 67 | Opens $opens, |
| 68 | StatisticsClicksRepository $statisticsClicksRepository, |
| 69 | UserAgentsRepository $userAgentsRepository, |
| 70 | LinkShortcodeCategory $linkShortcodeCategory, |
| 71 | SubscribersRepository $subscribersRepository, |
| 72 | TrackingConfig $trackingConfig, |
| 73 | Request $request, |
| 74 | TrackingConsentController $trackingConsentController |
| 75 | ) { |
| 76 | $this->cookies = $cookies; |
| 77 | $this->subscriberCookie = $subscriberCookie; |
| 78 | $this->shortcodes = $shortcodes; |
| 79 | $this->linkShortcodeCategory = $linkShortcodeCategory; |
| 80 | $this->opens = $opens; |
| 81 | $this->statisticsClicksRepository = $statisticsClicksRepository; |
| 82 | $this->userAgentsRepository = $userAgentsRepository; |
| 83 | $this->subscribersRepository = $subscribersRepository; |
| 84 | $this->trackingConfig = $trackingConfig; |
| 85 | $this->request = $request; |
| 86 | $this->trackingConsentController = $trackingConsentController; |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * @param \stdClass|null $data |
| 91 | */ |
| 92 | public function track($data) { |
| 93 | if (!$data || empty($data->link)) { |
| 94 | return $this->abort(); |
| 95 | } |
| 96 | /** @var SubscriberEntity $subscriber */ |
| 97 | $subscriber = $data->subscriber; |
| 98 | /** @var SendingQueueEntity $queue */ |
| 99 | $queue = $data->queue; |
| 100 | /** @var NewsletterEntity $newsletter */ |
| 101 | $newsletter = $data->newsletter; |
| 102 | /** @var NewsletterLinkEntity $link */ |
| 103 | $link = $data->link; |
| 104 | $wpUserPreview = ($data->preview && ($subscriber->isWPUser())); |
| 105 | $trackingAllowed = $this->trackingConsentController->isTrackingAllowed($subscriber); |
| 106 | // log statistics only if the action did not come from |
| 107 | // a WP user previewing the newsletter |
| 108 | // No tracking consent (CNIL/Garante): skip all recording (stats, cookies, |
| 109 | // engagement) but keep the redirect below. |
| 110 | if (!$wpUserPreview && $trackingAllowed) { |
| 111 | $userAgent = !empty($data->userAgent) ? $this->userAgentsRepository->findOrCreate($data->userAgent) : null; |
| 112 | $statisticsClicks = $this->statisticsClicksRepository->createOrUpdateClickCount( |
| 113 | $link, |
| 114 | $subscriber, |
| 115 | $newsletter, |
| 116 | $queue, |
| 117 | $userAgent |
| 118 | ); |
| 119 | if ( |
| 120 | $userAgent instanceof UserAgentEntity && |
| 121 | ($userAgent->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_HUMAN |
| 122 | || $statisticsClicks->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_MACHINE) |
| 123 | ) { |
| 124 | $statisticsClicks->setUserAgent($userAgent); |
| 125 | $statisticsClicks->setUserAgentType($userAgent->getUserAgentType()); |
| 126 | } |
| 127 | $this->statisticsClicksRepository->flush(); |
| 128 | $this->sendRevenueCookie($statisticsClicks); |
| 129 | |
| 130 | $subscriberId = $subscriber->getId(); |
| 131 | if ($subscriberId) { |
| 132 | $this->subscriberCookie->setSubscriberId($subscriberId); |
| 133 | } |
| 134 | |
| 135 | // track open event |
| 136 | $this->opens->track($data, $displayImage = false); |
| 137 | // Update engagement date |
| 138 | $this->subscribersRepository->maybeUpdateLastClickAt($subscriber); |
| 139 | } |
| 140 | $url = $this->processUrl($link->getUrl(), $newsletter, $subscriber, $queue, $wpUserPreview); |
| 141 | if ($trackingAllowed) { |
| 142 | // Consumers of this hook (e.g. automation "clicked link" triggers) use |
| 143 | // clicks for follow-up personalization — exactly what consent covers. |
| 144 | do_action('mailpoet_link_clicked', $link, $subscriber, $wpUserPreview); |
| 145 | } |
| 146 | $this->redirectToUrl($url); |
| 147 | } |
| 148 | |
| 149 | private function sendRevenueCookie(StatisticsClickEntity $clicks) { |
| 150 | if ($this->trackingConfig->isCookieTrackingEnabled()) { |
| 151 | $this->cookies->set( |
| 152 | self::REVENUE_TRACKING_COOKIE_NAME, |
| 153 | [ |
| 154 | 'statistics_clicks' => $clicks->getId(), |
| 155 | 'created_at' => time(), |
| 156 | ], |
| 157 | [ |
| 158 | 'expires' => time() + self::REVENUE_TRACKING_COOKIE_EXPIRY, |
| 159 | 'path' => '/', |
| 160 | ] |
| 161 | ); |
| 162 | } |
| 163 | } |
| 164 | |
| 165 | public function processUrl( |
| 166 | string $url, |
| 167 | NewsletterEntity $newsletter, |
| 168 | SubscriberEntity $subscriber, |
| 169 | SendingQueueEntity $queue, |
| 170 | bool $wpUserPreview |
| 171 | ) { |
| 172 | if (preg_match('/\[link:(?P<action>.*?)\]/', $url, $shortcode)) { |
| 173 | if (empty($shortcode['action'])) $this->abort(); |
| 174 | $processedUrl = $this->linkShortcodeCategory->processShortcodeAction( |
| 175 | $shortcode[0], |
| 176 | $newsletter, |
| 177 | $subscriber, |
| 178 | $queue, |
| 179 | $wpUserPreview |
| 180 | ); |
| 181 | // If shortcode was not processed, return original shortcode unchanged |
| 182 | if ($processedUrl === null) { |
| 183 | return $shortcode[0]; |
| 184 | } |
| 185 | $url = $processedUrl; |
| 186 | // We need to know the original method for unsubscribe actions |
| 187 | if ($this->request->isPost() && $url) { |
| 188 | $url = $url . (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'request_method=POST'; |
| 189 | } |
| 190 | } else { |
| 191 | $this->shortcodes->setQueue($queue); |
| 192 | $this->shortcodes->setNewsletter($newsletter); |
| 193 | $this->shortcodes->setSubscriber($subscriber); |
| 194 | $this->shortcodes->setWpUserPreview($wpUserPreview); |
| 195 | $url = $this->shortcodes->replace($url); |
| 196 | } |
| 197 | return $url; |
| 198 | } |
| 199 | |
| 200 | public function abort() { |
| 201 | global $wp_query;// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 202 | WPFunctions::get()->statusHeader(404); |
| 203 | $wp_query->set_404();// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 204 | WPFunctions::get()->getTemplatePart((string)404); |
| 205 | exit; |
| 206 | } |
| 207 | |
| 208 | public function redirectToUrl($url) { |
| 209 | header('Location: ' . $url, true, 302); |
| 210 | exit; |
| 211 | } |
| 212 | } |
| 213 |