File: /storage/v6964/vividhitystore/public_html/wp-content/plugins/mailpoet/lib/Statistics/GATracking.php
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Statistics;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Newsletter\Links\Links as NewsletterLinks;
use MailPoet\Settings\TrackingConfig;
use MailPoet\Util\Helpers;
use MailPoet\Util\SecondLevelDomainNames;
use MailPoet\WP\Functions;
class GATracking {
/** @var SecondLevelDomainNames */
private $secondLevelDomainNames;
/** @var NewsletterLinks */
private $newsletterLinks;
/** @var Functions */
private $wp;
/** @var TrackingConfig */
private $tackingConfig;
public function __construct(
NewsletterLinks $newsletterLinks,
Functions $wp,
TrackingConfig $trackingConfig
) {
$this->secondLevelDomainNames = new SecondLevelDomainNames();
$this->newsletterLinks = $newsletterLinks;
$this->wp = $wp;
$this->tackingConfig = $trackingConfig;
}
public function applyGATracking($renderedNewsletter, NewsletterEntity $newsletter, $internalHost = null) {
if (!$this->tackingConfig->isEmailTrackingEnabled()) {
return $renderedNewsletter;
}
if ($newsletter->getType() == NewsletterEntity::TYPE_NOTIFICATION_HISTORY && $newsletter->getParent() instanceof NewsletterEntity) {
$parentNewsletter = $newsletter->getParent();
$field = $parentNewsletter->getGaCampaign();
} else {
$field = $newsletter->getGaCampaign();
}
return $this->addGAParamsToLinks($renderedNewsletter, $field, $internalHost);
}
private function addGAParamsToLinks($renderedNewsletter, $gaCampaign, $internalHost = null) {
// join HTML and TEXT rendered body into a text string
$content = Helpers::joinObject($renderedNewsletter);
$extractedLinks = $this->newsletterLinks->extract($content);
$processedLinks = $this->addParams($extractedLinks, $gaCampaign, $internalHost);
list($content, $links) = $this->newsletterLinks->replace($content, $processedLinks);
// split the processed body with hashed links back to HTML and TEXT
list($renderedNewsletter['html'], $renderedNewsletter['text'])
= Helpers::splitObject($content);
return $renderedNewsletter;
}
private function addParams($extractedLinks, $gaCampaign, $internalHost = null) {
$processedLinks = [];
$params = [
'utm_source' => 'mailpoet',
'utm_medium' => 'email',
'utm_source_platform' => 'mailpoet',
];
if ($gaCampaign) {
$params['utm_campaign'] = $gaCampaign;
}
$internalHost = $internalHost ?: parse_url(home_url(), PHP_URL_HOST);
$internalHost = $this->secondLevelDomainNames->get($internalHost);
foreach ($extractedLinks as $extractedLink) {
if ($extractedLink['type'] !== NewsletterLinks::LINK_TYPE_URL) {
continue;
} elseif (strpos((string)parse_url($extractedLink['link'], PHP_URL_HOST), $internalHost) === false) {
// Process only internal links (i.e. pointing to current site)
continue;
}
$link = $extractedLink['link'];
// Do not overwrite existing query parameters
$parsedUrl = parse_url($link);
$linkParams = $params;
if (isset($parsedUrl['query'])) {
foreach (array_keys($params) as $param) {
if (strpos($parsedUrl['query'], $param . '=') !== false) {
unset($linkParams[$param]);
}
}
}
// Extract shortcodes from query parameters to preserve them
list($linkWithPlaceholders, $shortcodeMap) = $this->extractShortcodes($link);
// Add GA parameters to the link with placeholders
$linkWithGAParams = $this->wp->addQueryArg($linkParams, $linkWithPlaceholders);
// Restore the original shortcodes
$linkWithGAParams = $this->restoreShortcodes($linkWithGAParams, $shortcodeMap);
$processedLink = $this->wp->applyFilters(
'mailpoet_ga_tracking_link',
$linkWithGAParams,
$extractedLink['link'],
$linkParams,
$extractedLink['type']
);
$processedLinks[$link] = [
'type' => $extractedLink['type'],
'link' => $link,
'processed_link' => $processedLink,
];
}
return $processedLinks;
}
/**
* Extract shortcodes from URL query parameter values and replace them with placeholders.
* Shortcodes use the format [shortcode:value|option:value] and should not be URL-encoded.
* Only shortcodes that are actual parameter values (or part of them) are extracted.
*
* @return array [$urlWithPlaceholders, $shortcodeMap]
*/
private function extractShortcodes(string $url): array {
$parsedUrl = parse_url($url);
if (!isset($parsedUrl['query'])) {
return [$url, []];
}
// Parse the query string into parameters to validate shortcodes are in parameter values
parse_str($parsedUrl['query'], $params);
$shortcodeMap = [];
$urlWithPlaceholders = $url;
$index = 0;
// Process each parameter value (recursively for arrays)
$this->processParamsForShortcodes($params, $urlWithPlaceholders, $shortcodeMap, $index);
return [$urlWithPlaceholders, $shortcodeMap];
}
/**
* Process parameter values recursively to find and replace shortcodes.
* Handles both string values and nested arrays.
*
* @param array $params Parameter values to process
* @param string $urlWithPlaceholders URL being modified (passed by reference)
* @param array $shortcodeMap Map of placeholders to shortcodes (passed by reference)
* @param int $index Current placeholder index (passed by reference)
*/
private function processParamsForShortcodes(array $params, string &$urlWithPlaceholders, array &$shortcodeMap, int &$index): void {
foreach ($params as $value) {
if (is_array($value)) {
// Recursively process array values
$this->processParamsForShortcodes($value, $urlWithPlaceholders, $shortcodeMap, $index);
} elseif (is_string($value)) {
// Find shortcodes in string values
// Pattern matches MailPoet shortcodes in the format [name:value|option:value]
// - \[ matches opening bracket
// - [^\]]{1,400} matches 1-400 characters that are not a closing bracket
// (limit prevents ReDoS attacks from catastrophic backtracking)
// - \] matches closing bracket
// Examples: [subscriber:email], [subscriber:firstname|default:Guest]
$pattern = '/\[[^\]]{1,400}\]/';
if (preg_match_all($pattern, $value, $matches)) {
foreach ($matches[0] as $shortcode) {
// Create a unique placeholder
$placeholder = 'MPSHORTCODE' . $index . 'MPEND';
$shortcodeMap[$placeholder] = $shortcode;
// Replace shortcode with placeholder directly in the URL
$urlWithPlaceholders = str_replace($shortcode, $placeholder, $urlWithPlaceholders);
$index++;
}
}
}
}
}
/**
* Restore shortcodes in the URL by replacing placeholders with original shortcodes.
*/
private function restoreShortcodes(string $url, array $shortcodeMap): string {
if (empty($shortcodeMap)) {
return $url;
}
foreach ($shortcodeMap as $placeholder => $shortcode) {
$url = str_replace($placeholder, $shortcode, $url);
}
return $url;
}
}