PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.37.0
MailPoet – Newsletters, Email Marketing, and Automation v5.37.0
5.37.0 5.36.1 5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / Statistics / Track / Clicks.php
mailpoet / lib / Statistics / Track Last commit date
Clicks.php 2 days ago Opens.php 1 month ago PageViewCookie.php 3 months ago SubscriberActivityTracker.php 1 month ago SubscriberCookie.php 3 months ago SubscriberHandler.php 1 year ago Unsubscribes.php 3 months ago WooCommercePurchases.php 2 years ago index.php 3 years ago
Clicks.php
246 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\EmailEditor\Integrations\MailPoet\PersonalizationTags\PersonalizationTagLinkResolver;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\Entities\NewsletterLinkEntity;
11 use MailPoet\Entities\SendingQueueEntity;
12 use MailPoet\Entities\StatisticsClickEntity;
13 use MailPoet\Entities\SubscriberEntity;
14 use MailPoet\Entities\UserAgentEntity;
15 use MailPoet\Newsletter\Shortcodes\Categories\Link as LinkShortcodeCategory;
16 use MailPoet\Newsletter\Shortcodes\Shortcodes;
17 use MailPoet\Settings\TrackingConfig;
18 use MailPoet\Statistics\StatisticsClicksRepository;
19 use MailPoet\Statistics\UserAgentsRepository;
20 use MailPoet\Subscribers\SubscribersRepository;
21 use MailPoet\Subscribers\TrackingConsentController;
22 use MailPoet\Util\Cookies;
23 use MailPoet\Util\Request;
24 use MailPoet\WP\Functions as WPFunctions;
25
26 class Clicks {
27
28 /** The link whose whole purpose is to switch tracking off. Its own click is never recorded. */
29 const TRACKING_OPT_OUT_SHORTCODE = '[link:subscription_tracking_opt_out_url]';
30
31 const REVENUE_TRACKING_COOKIE_NAME = 'mailpoet_revenue_tracking';
32 const REVENUE_TRACKING_COOKIE_EXPIRY = 60 * 60 * 24 * 14;
33
34 /** @var Cookies */
35 private $cookies;
36
37 /** @var SubscriberCookie */
38 private $subscriberCookie;
39
40 /** @var Shortcodes */
41 private $shortcodes;
42
43 /** @var LinkShortcodeCategory */
44 private $linkShortcodeCategory;
45
46 /** @var Opens */
47 private $opens;
48
49 /** @var StatisticsClicksRepository */
50 private $statisticsClicksRepository;
51
52 /** @var UserAgentsRepository */
53 private $userAgentsRepository;
54
55 /** @var SubscribersRepository */
56 private $subscribersRepository;
57
58 /** @var TrackingConfig */
59 private $trackingConfig;
60
61 /** @var Request */
62 private $request;
63
64 /** @var TrackingConsentController */
65 private $trackingConsentController;
66
67 private PersonalizationTagLinkResolver $linkResolver;
68
69 public function __construct(
70 Cookies $cookies,
71 SubscriberCookie $subscriberCookie,
72 Shortcodes $shortcodes,
73 Opens $opens,
74 StatisticsClicksRepository $statisticsClicksRepository,
75 UserAgentsRepository $userAgentsRepository,
76 LinkShortcodeCategory $linkShortcodeCategory,
77 SubscribersRepository $subscribersRepository,
78 TrackingConfig $trackingConfig,
79 Request $request,
80 TrackingConsentController $trackingConsentController,
81 PersonalizationTagLinkResolver $linkResolver
82 ) {
83 $this->cookies = $cookies;
84 $this->subscriberCookie = $subscriberCookie;
85 $this->shortcodes = $shortcodes;
86 $this->linkShortcodeCategory = $linkShortcodeCategory;
87 $this->opens = $opens;
88 $this->statisticsClicksRepository = $statisticsClicksRepository;
89 $this->userAgentsRepository = $userAgentsRepository;
90 $this->subscribersRepository = $subscribersRepository;
91 $this->trackingConfig = $trackingConfig;
92 $this->request = $request;
93 $this->trackingConsentController = $trackingConsentController;
94 $this->linkResolver = $linkResolver;
95 }
96
97 /**
98 * @param \stdClass|null $data
99 */
100 public function track($data) {
101 if (!$data || empty($data->link)) {
102 return $this->abort();
103 }
104 /** @var SubscriberEntity $subscriber */
105 $subscriber = $data->subscriber;
106 /** @var SendingQueueEntity $queue */
107 $queue = $data->queue;
108 /** @var NewsletterEntity $newsletter */
109 $newsletter = $data->newsletter;
110 /** @var NewsletterLinkEntity $link */
111 $link = $data->link;
112 $wpUserPreview = ($data->preview && ($subscriber->isWPUser()));
113 $trackingAllowed = $this->trackingConsentController->isTrackingAllowed($subscriber);
114 // The opt-out link exists to stop tracking, so clicking it is never itself
115 // recorded, even for a subscriber we are otherwise allowed to track. Only
116 // the recording is skipped; the redirect below still runs.
117 $isTrackingOptOutLink = $link->getUrl() === self::TRACKING_OPT_OUT_SHORTCODE;
118 // log statistics only if the action did not come from
119 // a WP user previewing the newsletter
120 // No tracking consent (CNIL/Garante): skip all recording (stats, cookies,
121 // engagement) but keep the redirect below.
122 if (!$wpUserPreview && $trackingAllowed && !$isTrackingOptOutLink) {
123 $userAgent = !empty($data->userAgent) ? $this->userAgentsRepository->findOrCreate($data->userAgent) : null;
124 $statisticsClicks = $this->statisticsClicksRepository->createOrUpdateClickCount(
125 $link,
126 $subscriber,
127 $newsletter,
128 $queue,
129 $userAgent
130 );
131 if (
132 $userAgent instanceof UserAgentEntity &&
133 ($userAgent->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_HUMAN
134 || $statisticsClicks->getUserAgentType() === UserAgentEntity::USER_AGENT_TYPE_MACHINE)
135 ) {
136 $statisticsClicks->setUserAgent($userAgent);
137 $statisticsClicks->setUserAgentType($userAgent->getUserAgentType());
138 }
139 $this->statisticsClicksRepository->flush();
140 $this->sendRevenueCookie($statisticsClicks);
141
142 $subscriberId = $subscriber->getId();
143 if ($subscriberId) {
144 $this->subscriberCookie->setSubscriberId($subscriberId);
145 }
146
147 // track open event
148 $this->opens->track($data, $displayImage = false);
149 // Update engagement date
150 $this->subscribersRepository->maybeUpdateLastClickAt($subscriber);
151 }
152 $url = $this->processUrl($link->getUrl(), $newsletter, $subscriber, $queue, $wpUserPreview);
153 if ($trackingAllowed) {
154 // Consumers of this hook (e.g. automation "clicked link" triggers) use
155 // clicks for follow-up personalization — exactly what consent covers.
156 do_action('mailpoet_link_clicked', $link, $subscriber, $wpUserPreview);
157 }
158 $this->redirectToUrl($url);
159 }
160
161 private function sendRevenueCookie(StatisticsClickEntity $clicks) {
162 if ($this->trackingConfig->isCookieTrackingEnabled()) {
163 $this->cookies->set(
164 self::REVENUE_TRACKING_COOKIE_NAME,
165 [
166 'statistics_clicks' => $clicks->getId(),
167 'created_at' => time(),
168 ],
169 [
170 'expires' => time() + self::REVENUE_TRACKING_COOKIE_EXPIRY,
171 'path' => '/',
172 ]
173 );
174 }
175 }
176
177 public function processUrl(
178 string $url,
179 NewsletterEntity $newsletter,
180 SubscriberEntity $subscriber,
181 SendingQueueEntity $queue,
182 bool $wpUserPreview
183 ) {
184 if ($this->linkResolver->isTokenUrl($url)) {
185 // A link stored as a personalization tag token; its destination only exists per recipient.
186 $resolvedUrl = $this->linkResolver->resolve($url, $newsletter, $subscriber, $queue, $wpUserPreview);
187 if ($resolvedUrl === null) {
188 $this->abort();
189 return $url;
190 }
191 return $this->appendRequestMethod($resolvedUrl);
192 }
193 if (preg_match('/\[link:(?P<action>.*?)\]/', $url, $shortcode)) {
194 if (empty($shortcode['action'])) $this->abort();
195 $processedUrl = $this->linkShortcodeCategory->processShortcodeAction(
196 $shortcode[0],
197 $newsletter,
198 $subscriber,
199 $queue,
200 $wpUserPreview
201 );
202 // If shortcode was not processed, return original shortcode unchanged
203 if ($processedUrl === null) {
204 return $shortcode[0];
205 }
206 $url = $this->appendRequestMethod($processedUrl);
207 } else {
208 $this->shortcodes->setQueue($queue);
209 $this->shortcodes->setNewsletter($newsletter);
210 $this->shortcodes->setSubscriber($subscriber);
211 $this->shortcodes->setWpUserPreview($wpUserPreview);
212 $url = $this->shortcodes->replace($url);
213 }
214 return $url;
215 }
216
217 /**
218 * The unsubscribe actions need to know the original request method.
219 */
220 private function appendRequestMethod(string $url): string {
221 if (!$this->request->isPost() || !$url) {
222 return $url;
223 }
224 $fragment = '';
225 $hashPosition = strpos($url, '#');
226 if ($hashPosition !== false) {
227 $fragment = substr($url, $hashPosition);
228 $url = substr($url, 0, $hashPosition);
229 }
230 return $url . (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . 'request_method=POST' . $fragment;
231 }
232
233 public function abort() {
234 global $wp_query;// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
235 WPFunctions::get()->statusHeader(404);
236 $wp_query->set_404();// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
237 WPFunctions::get()->getTemplatePart((string)404);
238 exit;
239 }
240
241 public function redirectToUrl($url) {
242 header('Location: ' . $url, true, 302);
243 exit;
244 }
245 }
246