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 / Newsletter / Links / Links.php
mailpoet / lib / Newsletter / Links Last commit date
Links.php 4 days ago index.php 3 years ago
Links.php
307 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 /**
206 * @param (callable(string): string)|null $urlMapper Applied to every restored URL before it is put back
207 */
208 public function convertHashedLinksToShortcodesAndUrls($content, $queueId, $convertAll = false, ?callable $urlMapper = null) {
209 preg_match_all($this->getLinkRegex(), $content, $links);
210 $links = array_unique(Helpers::flattenArray($links));
211 foreach ($links as $link) {
212 $linkHash = explode('-', $link);
213
214 if (!isset($linkHash[1])) {
215 continue;
216 }
217
218 $newsletterLink = $this->newsletterLinkRepository->findOneBy(['hash' => $linkHash[1], 'queue' => $queueId]);
219
220 // convert either only link shortcodes or all hashes links if "convert all"
221 // option is specified
222 if (
223 ($newsletterLink instanceof NewsletterLinkEntity) &&
224 (preg_match('/\[link:/', $newsletterLink->getUrl()) || $convertAll)
225 ) {
226 $url = $urlMapper ? $urlMapper($newsletterLink->getUrl()) : $newsletterLink->getUrl();
227 $content = str_replace($link, $url, $content);
228 }
229 }
230 return $content;
231 }
232
233 public function getLinkRegex() {
234 return sprintf(
235 '/((%s|%s)(?:-\w+)?)/',
236 preg_quote(self::DATA_TAG_CLICK),
237 preg_quote(self::DATA_TAG_OPEN)
238 );
239 }
240
241 public function createUrlDataObject(
242 $subscriberId, $subscriberLinkToken, $queueId, $linkHash, $preview
243 ) {
244 return [
245 (string)$subscriberId,
246 $subscriberLinkToken,
247 (string)$queueId,
248 $linkHash,
249 $preview,
250 ];
251 }
252
253 public function transformUrlDataObject($data) {
254 reset($data);
255 if (!is_int(key($data))) return $data;
256 $transformedData = [];
257 $transformedData['subscriber_id'] = (!empty($data[0])) ? $data[0] : false;
258 $transformedData['subscriber_token'] = (!empty($data[1])) ? $data[1] : false;
259 $transformedData['queue_id'] = (!empty($data[2])) ? $data[2] : false;
260 $transformedData['link_hash'] = (!empty($data[3])) ? $data[3] : false;
261 $transformedData['preview'] = (!empty($data[4])) ? $data[4] : false;
262 return $transformedData;
263 }
264
265 private static function hashLink($link, $type) {
266 $hash = Security::generateHash();
267 return [
268 'type' => $type,
269 'hash' => $hash,
270 'link' => $link,
271 // replace link with a temporary data tag + hash
272 // it will be further replaced with the proper track API URL during sending
273 'processed_link' => self::DATA_TAG_CLICK . '-' . $hash,
274 ];
275 }
276
277 private function hash($extractedLinks, $savedLinks) {
278 $processedLinks = array_map(function($link) {
279 $link['type'] = Links::LINK_TYPE_URL;
280 $link['link'] = $link['url'];
281 $link['processed_link'] = self::DATA_TAG_CLICK . '-' . $link['hash'];
282 return $link;
283 }, $savedLinks);
284 foreach ($extractedLinks as $extractedLink) {
285 $link = $extractedLink['link'];
286 if (array_key_exists($link, $processedLinks))
287 continue;
288 // Use URL as a key to map between extracted and processed links
289 // regardless of their sequential position (useful for link skips etc.)
290 $processedLinks[$link] = $this->hashLink($link, $extractedLink['type']);
291 }
292 return $processedLinks;
293 }
294
295 private function load($newsletterId, $queueId) {
296 $links = $this->newsletterLinkRepository->findBy(
297 ['newsletter' => $newsletterId, 'queue' => $queueId]
298 );
299
300 $savedLinks = [];
301 foreach ($links as $link) {
302 $savedLinks[$link->getUrl()] = $link->toArray();
303 }
304 return $savedLinks;
305 }
306 }
307