/wp-content/plugins/mailpoet/lib/Newsletter/Links/Links.php
PHP | 301 lines | 251 code | 33 blank | 17 comment | 19 complexity | 1668ea9d86048e94c3fa12ca4cef5815 MD5 | raw file
- <?php
- namespace MailPoet\Newsletter\Links;
- if (!defined('ABSPATH')) exit;
- use MailPoet\Cron\Workers\StatsNotifications\NewsletterLinkRepository;
- use MailPoet\DI\ContainerWrapper;
- use MailPoet\Entities\NewsletterEntity;
- use MailPoet\Entities\NewsletterLinkEntity;
- use MailPoet\Entities\SendingQueueEntity;
- use MailPoet\InvalidStateException;
- use MailPoet\Newsletter\NewslettersRepository;
- use MailPoet\Newsletter\Sending\SendingQueuesRepository;
- use MailPoet\Newsletter\Shortcodes\Categories\Link;
- use MailPoet\Newsletter\Shortcodes\Shortcodes;
- use MailPoet\Router\Endpoints\Track as TrackEndpoint;
- use MailPoet\Router\Router;
- use MailPoet\Subscribers\LinkTokens;
- use MailPoet\Subscribers\SubscribersRepository;
- use MailPoet\Util\Helpers;
- use MailPoet\Util\pQuery\pQuery as DomParser;
- use MailPoet\Util\Security;
- class Links {
- const DATA_TAG_CLICK = '[mailpoet_click_data]';
- const DATA_TAG_OPEN = '[mailpoet_open_data]';
- const LINK_TYPE_SHORTCODE = 'shortcode';
- const LINK_TYPE_URL = 'link';
- /** @var LinkTokens */
- private $linkTokens;
- /** @var SubscribersRepository */
- private $subscribersRepository;
- /** @var NewsletterLinkRepository */
- private $newsletterLinkRepository;
- /** @var NewslettersRepository */
- private $newslettersRepository;
- /** @var SendingQueuesRepository */
- private $sendingQueueRepository;
- public function __construct(
- LinkTokens $linkTokens,
- SubscribersRepository $subscribersRepository,
- NewsletterLinkRepository $newsletterLinkRepository,
- NewslettersRepository $newslettersRepository,
- SendingQueuesRepository $sendingQueuesRepository
- ) {
- $this->linkTokens = $linkTokens;
- $this->subscribersRepository = $subscribersRepository;
- $this->newsletterLinkRepository = $newsletterLinkRepository;
- $this->newslettersRepository = $newslettersRepository;
- $this->sendingQueueRepository = $sendingQueuesRepository;
- }
- public function process($content, $newsletterId, $queueId) {
- $extractedLinks = $this->extract($content);
- $savedLinks = $this->load($newsletterId, $queueId);
- $processedLinks = $this->hash($extractedLinks, $savedLinks);
- return $this->replace($content, $processedLinks);
- }
- public function extract($content) {
- $extractedLinks = [];
- // extract link shortcodes
- /** @var Shortcodes $shortcodes */
- $shortcodes = ContainerWrapper::getInstance()->get(Shortcodes::class);
- $shortcodes = $shortcodes->extract(
- $content,
- $categories = [Link::CATEGORY_NAME]
- );
- if ($shortcodes) {
- $extractedLinks = array_map(function($shortcode) {
- return [
- 'type' => Links::LINK_TYPE_SHORTCODE,
- 'link' => $shortcode,
- ];
- }, $shortcodes);
- }
- // extract HTML anchor tags
- $DOM = DomParser::parseStr($content);
- foreach ($DOM->query('a') as $link) {
- if (!$link->href) continue;
- $extractedLinks[] = [
- 'type' => self::LINK_TYPE_URL,
- 'link' => $link->href,
- ];
- }
- return array_unique($extractedLinks, SORT_REGULAR);
- }
- public function replace($content, $processedLinks) {
- // replace HTML anchor tags
- $DOM = DomParser::parseStr($content);
- foreach ($DOM->query('a') as $link) {
- $linkToReplace = $link->href;
- $replacementLink = (!empty($processedLinks[$linkToReplace]['processed_link'])) ?
- $processedLinks[$linkToReplace]['processed_link'] :
- null;
- if (!$replacementLink) continue;
- $link->setAttribute('href', $replacementLink);
- }
- $content = $DOM->__toString();
- // replace link shortcodes and markdown links
- foreach ($processedLinks as $processedLink) {
- $linkToReplace = $processedLink['link'];
- $replacementLink = $processedLink['processed_link'];
- if ($processedLink['type'] == self::LINK_TYPE_SHORTCODE) {
- $content = str_replace($linkToReplace, $replacementLink, (string)$content);
- }
- $content = preg_replace(
- '/\[(.*?)\](\(' . preg_quote($linkToReplace, '/') . '\))/',
- '[$1](' . $replacementLink . ')',
- (string)$content
- );
- }
- return [
- $content,
- array_values($processedLinks),
- ];
- }
- public function replaceSubscriberData(
- $subscriberId,
- $queueId,
- $content,
- $preview = false
- ) {
- // match data tags
- $subscriber = $this->subscribersRepository->findOneById($subscriberId);
- if (!$subscriber) {
- throw new InvalidStateException();
- }
- preg_match_all($this->getLinkRegex(), $content, $matches);
- foreach ($matches[1] as $index => $match) {
- $hash = null;
- if (preg_match('/-/', $match)) {
- [, $hash] = explode('-', $match);
- }
- $data = $this->createUrlDataObject(
- $subscriber->getId(),
- $this->linkTokens->getToken($subscriber),
- $queueId,
- $hash,
- $preview
- );
- $routerAction = ($matches[2][$index] === self::DATA_TAG_CLICK) ?
- TrackEndpoint::ACTION_CLICK :
- TrackEndpoint::ACTION_OPEN;
- $link = Router::buildRequest(
- TrackEndpoint::ENDPOINT,
- $routerAction,
- $data
- );
- $content = str_replace($match, $link, $content);
- }
- return $content;
- }
- public function save(array $links, $newsletterId, $queueId) {
- foreach ($links as $link) {
- if (isset($link['id'])) {
- continue;
- }
- if (empty($link['hash']) || empty($link['link'])) {
- continue;
- }
- $newsletter = $this->newslettersRepository->getReference($newsletterId);
- $sendingQueue = $this->sendingQueueRepository->getReference($queueId);
- if (!$newsletter instanceof NewsletterEntity || !$sendingQueue instanceof SendingQueueEntity) {
- continue;
- }
- $newsletterLink = new NewsletterLinkEntity($newsletter, $sendingQueue, $link['link'], $link['hash']);
- $this->newsletterLinkRepository->persist($newsletterLink);
- }
- $this->newsletterLinkRepository->flush();
- }
- public function ensureInstantUnsubscribeLink(array $processedLinks) {
- if (
- in_array(
- NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
- array_column($processedLinks, 'link'))
- ) {
- return $processedLinks;
- }
- $processedLinks[] = $this->hashLink(
- NewsletterLinkEntity::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
- Links::LINK_TYPE_SHORTCODE
- );
- return $processedLinks;
- }
- public function convertHashedLinksToShortcodesAndUrls($content, $queueId, $convertAll = false) {
- preg_match_all($this->getLinkRegex(), $content, $links);
- $links = array_unique(Helpers::flattenArray($links));
- foreach ($links as $link) {
- $linkHash = explode('-', $link);
- if (!isset($linkHash[1])) {
- continue;
- }
- $newsletterLink = $this->newsletterLinkRepository->findOneBy(['hash' => $linkHash[1], 'queue' => $queueId]);
- // convert either only link shortcodes or all hashes links if "convert all"
- // option is specified
- if (
- ($newsletterLink instanceof NewsletterLinkEntity) &&
- (preg_match('/\[link:/', $newsletterLink->getUrl()) || $convertAll)
- ) {
- $content = str_replace($link, $newsletterLink->getUrl(), $content);
- }
- }
- return $content;
- }
- public function getLinkRegex() {
- return sprintf(
- '/((%s|%s)(?:-\w+)?)/',
- preg_quote(self::DATA_TAG_CLICK),
- preg_quote(self::DATA_TAG_OPEN)
- );
- }
- public function createUrlDataObject(
- $subscriberId, $subscriberLinkToken, $queueId, $linkHash, $preview
- ) {
- return [
- (string)$subscriberId,
- $subscriberLinkToken,
- (string)$queueId,
- $linkHash,
- $preview,
- ];
- }
- public function transformUrlDataObject($data) {
- reset($data);
- if (!is_int(key($data))) return $data;
- $transformedData = [];
- $transformedData['subscriber_id'] = (!empty($data[0])) ? $data[0] : false;
- $transformedData['subscriber_token'] = (!empty($data[1])) ? $data[1] : false;
- $transformedData['queue_id'] = (!empty($data[2])) ? $data[2] : false;
- $transformedData['link_hash'] = (!empty($data[3])) ? $data[3] : false;
- $transformedData['preview'] = (!empty($data[4])) ? $data[4] : false;
- return $transformedData;
- }
- private static function hashLink($link, $type) {
- $hash = Security::generateHash();
- return [
- 'type' => $type,
- 'hash' => $hash,
- 'link' => $link,
- // replace link with a temporary data tag + hash
- // it will be further replaced with the proper track API URL during sending
- 'processed_link' => self::DATA_TAG_CLICK . '-' . $hash,
- ];
- }
- private function hash($extractedLinks, $savedLinks) {
- $processedLinks = array_map(function($link) {
- $link['type'] = Links::LINK_TYPE_URL;
- $link['link'] = $link['url'];
- $link['processed_link'] = self::DATA_TAG_CLICK . '-' . $link['hash'];
- return $link;
- }, $savedLinks);
- foreach ($extractedLinks as $extractedLink) {
- $link = $extractedLink['link'];
- if (array_key_exists($link, $processedLinks))
- continue;
- // Use URL as a key to map between extracted and processed links
- // regardless of their sequential position (useful for link skips etc.)
- $processedLinks[$link] = $this->hashLink($link, $extractedLink['type']);
- }
- return $processedLinks;
- }
- private function load($newsletterId, $queueId) {
- $links = $this->newsletterLinkRepository->findBy(
- ['newsletter' => $newsletterId, 'queue' => $queueId]
- );
- $savedLinks = [];
- foreach ($links as $link) {
- $savedLinks[$link->getUrl()] = $link->toArray();
- }
- return $savedLinks;
- }
- }