diff --git a/lib/Models/NewsletterLink.php b/lib/Models/NewsletterLink.php index ceca83b8dc..c2c4a7cd18 100644 --- a/lib/Models/NewsletterLink.php +++ b/lib/Models/NewsletterLink.php @@ -10,6 +10,7 @@ namespace MailPoet\Models; */ class NewsletterLink extends Model { public static $_table = MP_NEWSLETTER_LINKS_TABLE; + const UNSUBSCRIBE_LINK_SHORT_CODE = '[link:subscription_unsubscribe_url]'; /** * @param Newsletter $newsletter diff --git a/lib/Newsletter/Links/Links.php b/lib/Newsletter/Links/Links.php index 19417a01a5..cfbe11beba 100644 --- a/lib/Newsletter/Links/Links.php +++ b/lib/Newsletter/Links/Links.php @@ -75,17 +75,9 @@ class Links { $link = $extracted_link['link']; if (array_key_exists($link, $processed_links)) continue; - $hash = Security::generateHash(); // Use URL as a key to map between extracted and processed links // regardless of their sequential position (useful for link skips etc.) - $processed_links[$link] = [ - 'type' => $extracted_link['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, - ]; + $processed_links[$link] = self::hashLink($link, $extracted_link['type']); } return $processed_links; } @@ -170,6 +162,20 @@ class Links { } } + static function ensureUnsubscribeLink(array $processed_links) { + if (in_array( + NewsletterLink::UNSUBSCRIBE_LINK_SHORT_CODE, + \MailPoet\Util\array_column($processed_links, 'link')) + ) { + return $processed_links; + } + $processed_links[] = self::hashLink( + NewsletterLink::UNSUBSCRIBE_LINK_SHORT_CODE, + Links::LINK_TYPE_SHORTCODE + ); + return $processed_links; + } + static function convertHashedLinksToShortcodesAndUrls($content, $queue_id, $convert_all = false) { preg_match_all(self::getLinkRegex(), $content, $links); $links = array_unique(Helpers::flattenArray($links)); @@ -221,4 +227,16 @@ class Links { $transformed_data['preview'] = (!empty($data[4])) ? $data[4] : false; return $transformed_data; } + + 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, + ]; + } } diff --git a/tests/integration/Newsletter/Links/LinksTest.php b/tests/integration/Newsletter/Links/LinksTest.php index 365592295f..dc8f6fa7e3 100644 --- a/tests/integration/Newsletter/Links/LinksTest.php +++ b/tests/integration/Newsletter/Links/LinksTest.php @@ -8,6 +8,7 @@ use MailPoet\Models\Subscriber; use MailPoet\Newsletter\Links\Links; use MailPoet\Newsletter\Shortcodes\Categories\Link; use MailPoet\Router\Router; +use function MailPoetVendor\Symfony\Component\DependencyInjection\Loader\Configurator\expr; class LinksTest extends \MailPoetTest { function testItOnlyExtractsLinksFromAnchorTags() { @@ -237,6 +238,25 @@ class LinksTest extends \MailPoetTest { expect($result)->contains('[mailpoet_click_data]-123'); } + function testItCanEnsureThatUnsubscribeLinkIsAmongProcessedLinks() { + $links = [ + [ + 'link' => 'http://example.com', + 'type' => Links::LINK_TYPE_URL, + 'processed_link' => '[mailpoet_click_data]-123', + 'hash' => 'abcdfgh', + ], + ]; + $links = Links::ensureUnsubscribeLink($links); + expect(count($links))->equals(2); + expect($links[1]['link'])->equals(NewsletterLink::UNSUBSCRIBE_LINK_SHORT_CODE); + expect($links[1]['type'])->equals(Links::LINK_TYPE_SHORTCODE); + expect($links[1])->hasKey('processed_link'); + expect($links[1])->hasKey('hash'); + $links = Links::ensureUnsubscribeLink($links); + expect(count($links))->equals(2); + } + function testItCanConvertAllHashedLinksToUrls() { // create newsletter link associations $queue_id = 1;