| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- <?php
- namespace MailPoet\Newsletter\Links;
- if (!defined('ABSPATH')) exit;
- use MailPoet\DI\ContainerWrapper;
- use MailPoet\InvalidStateException;
- use MailPoet\Models\NewsletterLink;
- 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;
- public function __construct(
- LinkTokens $linkTokens,
- SubscribersRepository $subscribersRepository
- ) {
- $this->linkTokens = $linkTokens;
- $this->subscribersRepository = $subscribersRepository;
- }
- 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;
- $newsletterLink = NewsletterLink::create();
- $newsletterLink->newsletterId = $newsletterId;
- $newsletterLink->queueId = $queueId;
- $newsletterLink->hash = $link['hash'];
- $newsletterLink->url = $link['link'];
- $newsletterLink->save();
- }
- }
- public function ensureInstantUnsubscribeLink(array $processedLinks) {
- if (in_array(
- NewsletterLink::INSTANT_UNSUBSCRIBE_LINK_SHORT_CODE,
- array_column($processedLinks, 'link'))
- ) {
- return $processedLinks;
- }
- $processedLinks[] = $this->hashLink(
- NewsletterLink::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 = NewsletterLink::where('hash', $linkHash[1])
- ->where('queue_id', $queueId)
- ->findOne();
- // convert either only link shortcodes or all hashes links if "convert all"
- // option is specified
- if (($newsletterLink instanceof NewsletterLink) &&
- (preg_match('/\[link:/', $newsletterLink->url) || $convertAll)
- ) {
- $content = str_replace($link, $newsletterLink->url, $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 [
- $subscriberId,
- $subscriberLinkToken,
- $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 = NewsletterLink::whereEqual('newsletter_id', $newsletterId)
- ->whereEqual('queue_id', $queueId)
- ->findMany();
- $savedLinks = [];
- foreach ($links as $link) {
- $savedLinks[$link->url] = $link->asArray();
- }
- return $savedLinks;
- }
- }
|