PageRenderTime 42ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/mailpoet/lib/Newsletter/Links/Links.php

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