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 / Newsletter / Links / Links.php
mailpoet / lib / Newsletter / Links Last commit date
Links.php 2 months ago index.php 3 years ago
Links.php
303 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\Newsletter\Links;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
9 use MailPoet\DI\ContainerWrapper;
10 use MailPoet\Entities\NewsletterEntity;
11 use MailPoet\Entities\NewsletterLinkEntity;
12 use MailPoet\Entities\SendingQueueEntity;
13 use MailPoet\InvalidStateException;
14 use MailPoet\Newsletter\NewslettersRepository;
15 use MailPoet\Newsletter\Sending\SendingQueuesRepository;
16 use MailPoet\Newsletter\Shortcodes\Categories\Link;
17 use MailPoet\Newsletter\Shortcodes\Shortcodes;
18 use MailPoet\Router\Endpoints\Track as TrackEndpoint;
19 use MailPoet\Router\Router;
20 use MailPoet\Subscribers\LinkTokens;
21 use MailPoet\Subscribers\SubscribersRepository;
22 use MailPoet\Util\Helpers;
23 use MailPoet\Util\pQuery\pQuery as DomParser;
24 use MailPoet\Util\Security;
25
26 class Links {
27 const DATA_TAG_CLICK = '[mailpoet_click_data]';
28 const DATA_TAG_OPEN = '[mailpoet_open_data]';
29 const LINK_TYPE_SHORTCODE = 'shortcode';
30 const LINK_TYPE_URL = 'link';
31
32 /** @var LinkTokens */
33 private $linkTokens;
34
35 /** @var SubscribersRepository */
36 private $subscribersRepository;
37
38 /** @var NewsletterLinkRepository */
39 private $newsletterLinkRepository;
40
41 /** @var NewslettersRepository */
42 private $newslettersRepository;
43
44 /** @var SendingQueuesRepository */
45 private $sendingQueueRepository;
46
47 public function __construct(
48 LinkTokens $linkTokens,
49 SubscribersRepository $subscribersRepository,
50 NewsletterLinkRepository $newsletterLinkRepository,
51 NewslettersRepository $newslettersRepository,
52 SendingQueuesRepository $sendingQueuesRepository
53 ) {
54 $this->linkTokens = $linkTokens;
55 $this->subscribersRepository = $subscribersRepository;
56 $this->newsletterLinkRepository = $newsletterLinkRepository;
57 $this->newslettersRepository = $newslettersRepository;
58 $this->sendingQueueRepository = $sendingQueuesRepository;
59 }
60
61 public function process($content, $newsletterId, $queueId) {
62 $extractedLinks = $this->extract($content);
63 $savedLinks = $this->load($newsletterId, $queueId);
64 $processedLinks = $this->hash($extractedLinks, $savedLinks);
65 return $this->replace($content, $processedLinks);
66 }
67
68 public function extract($content) {
69 $extractedLinks = [];
70 // extract link shortcodes
71 /** @var Shortcodes $shortcodes */
72 $shortcodes = ContainerWrapper::getInstance()->get(Shortcodes::class);
73 $shortcodes = $shortcodes->extract(
74 $content,
75 $categories = [Link::CATEGORY_NAME]
76 );
77 if ($shortcodes) {
78 $extractedLinks = array_map(function($shortcode) {
79 return [
80 'type' => Links::LINK_TYPE_SHORTCODE,
81 'link' => $shortcode,
82 ];
83 }, $shortcodes);
84 }
85 // extract HTML anchor tags
86 $DOM = DomParser::parseStr($content);
87 foreach ($DOM->query('a') as $link) {
88 if (!$link->href) continue;
89 $extractedLinks[] = [
90 'type' => self::LINK_TYPE_URL,
91 'link' => $link->href,
92 ];
93 }
94 return array_unique($extractedLinks, SORT_REGULAR);
95 }
96
97 public function replace($content, $processedLinks) {
98 // replace HTML anchor tags
99 $DOM = DomParser::parseStr($content);
100 foreach ($DOM->query('a') as $link) {
101 $linkToReplace = $link->href;
102 $replacementLink = (!empty($processedLinks[$linkToReplace]['processed_link'])) ?
103 $processedLinks[$linkToReplace]['processed_link'] :
104 null;
105 if (!$replacementLink) continue;
106 $link->setAttribute('href', $replacementLink);
107 }
108 $content = $DOM->__toString();
109 // replace link shortcodes and markdown links
110 foreach ($processedLinks as $processedLink) {
111 $linkToReplace = $processedLink['link'];
112 $replacementLink = $processedLink['processed_link'];
113 if ($processedLink['type'] == self::LINK_TYPE_SHORTCODE) {
114 $content = str_replace($linkToReplace, $replacementLink, (string)$content);
115 }
116 $content = preg_replace(
117 '/\[(.*?)\](\(' . preg_quote($linkToReplace, '/') . '\))/',
118 '[$1](' . $replacementLink . ')',
119 (string)$content
120 );
121 }
122 return [
123 $content,
124 array_values($processedLinks),
125 ];
126 }
127
128 public function replaceSubscriberData(
129 $subscriberId,
130 $queueId,
131 $content,
132 $preview = false
133 ) {
134 // match data tags
135 $subscriber = $this->subscribersRepository->findOneById($subscriberId);
136 if (!$subscriber) {
137 throw new InvalidStateException('Subscriber not found for link replacement');
138 }
139 preg_match_all($this->getLinkRegex(), $content, $matches);
140 foreach ($matches[1] as $index => $match) {
141 $hash = null;
142 if (preg_match('/-/', $match)) {
143 [, $hash] = explode('-', $match);
144 }
145 $data = $this->createUrlDataObject(
146 $subscriber->getId(),
147 $this->linkTokens->getToken($subscriber),
148 $queueId,
149 $hash,
150 $preview
151 );
152 $routerAction = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
153 TrackEndpoint::ACTION_CLICK :
154 TrackEndpoint::ACTION_OPEN;
155 $link = Router::buildRequest(
156 TrackEndpoint::ENDPOINT,
157 $routerAction,
158 $data
159 );
160 $content = str_replace($match, $link, $content);
161 }
162 return $content;
163 }
164
165 public function save(array $links, $newsletterId, $queueId) {
166 foreach ($links as $link) {
167 if (isset($link['id'])) {
168 continue;
169 }
170
171 if (empty($link['hash']) || empty($link['link'])) {
172 continue;
173 }
174
175 $newsletter = $this->newslettersRepository->getReference($newsletterId);
176 $sendingQueue = $this->sendingQueueRepository->getReference($queueId);
177
178 if (!$newsletter instanceof NewsletterEntity || !$sendingQueue instanceof SendingQueueEntity) {
179 continue;
180 }
181
182 $newsletterLink = new NewsletterLinkEntity($newsletter, $sendingQueue, $link['link'], $link['hash']);
183 $this->newsletterLinkRepository->persist($newsletterLink);
184 }
185
186 $this->newsletterLinkRepository->flush();
187 }
188
189 public function ensureInstantUnsubscribeLink(array $processedLinks) {
190 if (
191 in_array(
192 NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
193 array_column($processedLinks, 'link')
194 )
195 ) {
196 return $processedLinks;
197 }
198 $processedLinks[] = $this->hashLink(
199 NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
200 Links::LINK_TYPE_SHORTCODE
201 );
202 return $processedLinks;
203 }
204
205 public function convertHashedLinksToShortcodesAndUrls($content, $queueId, $convertAll = false) {
206 preg_match_all($this->getLinkRegex(), $content, $links);
207 $links = array_unique(Helpers::flattenArray($links));
208 foreach ($links as $link) {
209 $linkHash = explode('-', $link);
210
211 if (!isset($linkHash[1])) {
212 continue;
213 }
214
215 $newsletterLink = $this->newsletterLinkRepository->findOneBy(['hash' => $linkHash[1], 'queue' => $queueId]);
216
217 // convert either only link shortcodes or all hashes links if "convert all"
218 // option is specified
219 if (
220 ($newsletterLink instanceof NewsletterLinkEntity) &&
221 (preg_match('/\[link:/', $newsletterLink->getUrl()) || $convertAll)
222 ) {
223 $content = str_replace($link, $newsletterLink->getUrl(), $content);
224 }
225 }
226 return $content;
227 }
228
229 public function getLinkRegex() {
230 return sprintf(
231 '/((%s|%s)(?:-\w+)?)/',
232 preg_quote(self::DATA_TAG_CLICK),
233 preg_quote(self::DATA_TAG_OPEN)
234 );
235 }
236
237 public function createUrlDataObject(
238 $subscriberId, $subscriberLinkToken, $queueId, $linkHash, $preview
239 ) {
240 return [
241 (string)$subscriberId,
242 $subscriberLinkToken,
243 (string)$queueId,
244 $linkHash,
245 $preview,
246 ];
247 }
248
249 public function transformUrlDataObject($data) {
250 reset($data);
251 if (!is_int(key($data))) return $data;
252 $transformedData = [];
253 $transformedData['subscriber_id'] = (!empty($data[0])) ? $data[0] : false;
254 $transformedData['subscriber_token'] = (!empty($data[1])) ? $data[1] : false;
255 $transformedData['queue_id'] = (!empty($data[2])) ? $data[2] : false;
256 $transformedData['link_hash'] = (!empty($data[3])) ? $data[3] : false;
257 $transformedData['preview'] = (!empty($data[4])) ? $data[4] : false;
258 return $transformedData;
259 }
260
261 private static function hashLink($link, $type) {
262 $hash = Security::generateHash();
263 return [
264 'type' => $type,
265 'hash' => $hash,
266 'link' => $link,
267 // replace link with a temporary data tag + hash
268 // it will be further replaced with the proper track API URL during sending
269 'processed_link' => self::DATA_TAG_CLICK . '-' . $hash,
270 ];
271 }
272
273 private function hash($extractedLinks, $savedLinks) {
274 $processedLinks = array_map(function($link) {
275 $link['type'] = Links::LINK_TYPE_URL;
276 $link['link'] = $link['url'];
277 $link['processed_link'] = self::DATA_TAG_CLICK . '-' . $link['hash'];
278 return $link;
279 }, $savedLinks);
280 foreach ($extractedLinks as $extractedLink) {
281 $link = $extractedLink['link'];
282 if (array_key_exists($link, $processedLinks))
283 continue;
284 // Use URL as a key to map between extracted and processed links
285 // regardless of their sequential position (useful for link skips etc.)
286 $processedLinks[$link] = $this->hashLink($link, $extractedLink['type']);
287 }
288 return $processedLinks;
289 }
290
291 private function load($newsletterId, $queueId) {
292 $links = $this->newsletterLinkRepository->findBy(
293 ['newsletter' => $newsletterId, 'queue' => $queueId]
294 );
295
296 $savedLinks = [];
297 foreach ($links as $link) {
298 $savedLinks[$link->getUrl()] = $link->toArray();
299 }
300 return $savedLinks;
301 }
302 }
303