PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
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 / GATracking.php
mailpoet / lib / Statistics Last commit date
Track 4 days ago GATracking.php 6 months ago StatisticsBouncesRepository.php 3 years ago StatisticsClicksRepository.php 2 years ago StatisticsFormsRepository.php 3 years ago StatisticsNewslettersRepository.php 1 year ago StatisticsOpensRepository.php 2 days ago StatisticsUnsubscribesRepository.php 2 months ago StatisticsWooCommercePurchasesRepository.php 1 year ago UnsubscribeReasonTracker.php 2 months ago UserAgentsRepository.php 1 year ago index.php 3 years ago
GATracking.php
202 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Statistics;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Entities\NewsletterEntity;
9 use MailPoet\Newsletter\Links\Links as NewsletterLinks;
10 use MailPoet\Settings\TrackingConfig;
11 use MailPoet\Util\Helpers;
12 use MailPoet\Util\SecondLevelDomainNames;
13 use MailPoet\WP\Functions;
14
15 class GATracking {
16
17 /** @var SecondLevelDomainNames */
18 private $secondLevelDomainNames;
19
20 /** @var NewsletterLinks */
21 private $newsletterLinks;
22
23 /** @var Functions */
24 private $wp;
25
26 /** @var TrackingConfig */
27 private $tackingConfig;
28
29 public function __construct(
30 NewsletterLinks $newsletterLinks,
31 Functions $wp,
32 TrackingConfig $trackingConfig
33 ) {
34 $this->secondLevelDomainNames = new SecondLevelDomainNames();
35 $this->newsletterLinks = $newsletterLinks;
36 $this->wp = $wp;
37 $this->tackingConfig = $trackingConfig;
38 }
39
40 public function applyGATracking($renderedNewsletter, NewsletterEntity $newsletter, $internalHost = null) {
41 if (!$this->tackingConfig->isEmailTrackingEnabled()) {
42 return $renderedNewsletter;
43 }
44 if ($newsletter->getType() == NewsletterEntity::TYPE_NOTIFICATION_HISTORY && $newsletter->getParent() instanceof NewsletterEntity) {
45 $parentNewsletter = $newsletter->getParent();
46 $field = $parentNewsletter->getGaCampaign();
47 } else {
48 $field = $newsletter->getGaCampaign();
49 }
50
51 return $this->addGAParamsToLinks($renderedNewsletter, $field, $internalHost);
52 }
53
54 private function addGAParamsToLinks($renderedNewsletter, $gaCampaign, $internalHost = null) {
55 // join HTML and TEXT rendered body into a text string
56 $content = Helpers::joinObject($renderedNewsletter);
57 $extractedLinks = $this->newsletterLinks->extract($content);
58 $processedLinks = $this->addParams($extractedLinks, $gaCampaign, $internalHost);
59 list($content, $links) = $this->newsletterLinks->replace($content, $processedLinks);
60 // split the processed body with hashed links back to HTML and TEXT
61 list($renderedNewsletter['html'], $renderedNewsletter['text'])
62 = Helpers::splitObject($content);
63 return $renderedNewsletter;
64 }
65
66 private function addParams($extractedLinks, $gaCampaign, $internalHost = null) {
67 $processedLinks = [];
68 $params = [
69 'utm_source' => 'mailpoet',
70 'utm_medium' => 'email',
71 'utm_source_platform' => 'mailpoet',
72 ];
73 if ($gaCampaign) {
74 $params['utm_campaign'] = $gaCampaign;
75 }
76 $internalHost = $internalHost ?: parse_url(home_url(), PHP_URL_HOST);
77 $internalHost = $this->secondLevelDomainNames->get($internalHost);
78 foreach ($extractedLinks as $extractedLink) {
79 if ($extractedLink['type'] !== NewsletterLinks::LINK_TYPE_URL) {
80 continue;
81 } elseif (strpos((string)parse_url($extractedLink['link'], PHP_URL_HOST), $internalHost) === false) {
82 // Process only internal links (i.e. pointing to current site)
83 continue;
84 }
85
86 $link = $extractedLink['link'];
87
88 // Do not overwrite existing query parameters
89 $parsedUrl = parse_url($link);
90 $linkParams = $params;
91 if (isset($parsedUrl['query'])) {
92 foreach (array_keys($params) as $param) {
93 if (strpos($parsedUrl['query'], $param . '=') !== false) {
94 unset($linkParams[$param]);
95 }
96 }
97 }
98
99 // Extract shortcodes from query parameters to preserve them
100 list($linkWithPlaceholders, $shortcodeMap) = $this->extractShortcodes($link);
101
102 // Add GA parameters to the link with placeholders
103 $linkWithGAParams = $this->wp->addQueryArg($linkParams, $linkWithPlaceholders);
104
105 // Restore the original shortcodes
106 $linkWithGAParams = $this->restoreShortcodes($linkWithGAParams, $shortcodeMap);
107
108 $processedLink = $this->wp->applyFilters(
109 'mailpoet_ga_tracking_link',
110 $linkWithGAParams,
111 $extractedLink['link'],
112 $linkParams,
113 $extractedLink['type']
114 );
115 $processedLinks[$link] = [
116 'type' => $extractedLink['type'],
117 'link' => $link,
118 'processed_link' => $processedLink,
119 ];
120 }
121 return $processedLinks;
122 }
123
124 /**
125 * Extract shortcodes from URL query parameter values and replace them with placeholders.
126 * Shortcodes use the format [shortcode:value|option:value] and should not be URL-encoded.
127 * Only shortcodes that are actual parameter values (or part of them) are extracted.
128 *
129 * @return array [$urlWithPlaceholders, $shortcodeMap]
130 */
131 private function extractShortcodes(string $url): array {
132 $parsedUrl = parse_url($url);
133 if (!isset($parsedUrl['query'])) {
134 return [$url, []];
135 }
136
137 // Parse the query string into parameters to validate shortcodes are in parameter values
138 parse_str($parsedUrl['query'], $params);
139
140 $shortcodeMap = [];
141 $urlWithPlaceholders = $url;
142 $index = 0;
143
144 // Process each parameter value (recursively for arrays)
145 $this->processParamsForShortcodes($params, $urlWithPlaceholders, $shortcodeMap, $index);
146
147 return [$urlWithPlaceholders, $shortcodeMap];
148 }
149
150 /**
151 * Process parameter values recursively to find and replace shortcodes.
152 * Handles both string values and nested arrays.
153 *
154 * @param array $params Parameter values to process
155 * @param string $urlWithPlaceholders URL being modified (passed by reference)
156 * @param array $shortcodeMap Map of placeholders to shortcodes (passed by reference)
157 * @param int $index Current placeholder index (passed by reference)
158 */
159 private function processParamsForShortcodes(array $params, string &$urlWithPlaceholders, array &$shortcodeMap, int &$index): void {
160 foreach ($params as $value) {
161 if (is_array($value)) {
162 // Recursively process array values
163 $this->processParamsForShortcodes($value, $urlWithPlaceholders, $shortcodeMap, $index);
164 } elseif (is_string($value)) {
165 // Find shortcodes in string values
166 // Pattern matches MailPoet shortcodes in the format [name:value|option:value]
167 // - \[ matches opening bracket
168 // - [^\]]{1,400} matches 1-400 characters that are not a closing bracket
169 // (limit prevents ReDoS attacks from catastrophic backtracking)
170 // - \] matches closing bracket
171 // Examples: [subscriber:email], [subscriber:firstname|default:Guest]
172 $pattern = '/\[[^\]]{1,400}\]/';
173 if (preg_match_all($pattern, $value, $matches)) {
174 foreach ($matches[0] as $shortcode) {
175 // Create a unique placeholder
176 $placeholder = 'MPSHORTCODE' . $index . 'MPEND';
177 $shortcodeMap[$placeholder] = $shortcode;
178 // Replace shortcode with placeholder directly in the URL
179 $urlWithPlaceholders = str_replace($shortcode, $placeholder, $urlWithPlaceholders);
180 $index++;
181 }
182 }
183 }
184 }
185 }
186
187 /**
188 * Restore shortcodes in the URL by replacing placeholders with original shortcodes.
189 */
190 private function restoreShortcodes(string $url, array $shortcodeMap): string {
191 if (empty($shortcodeMap)) {
192 return $url;
193 }
194
195 foreach ($shortcodeMap as $placeholder => $shortcode) {
196 $url = str_replace($placeholder, $shortcode, $url);
197 }
198
199 return $url;
200 }
201 }
202