Spaces:
No application file
No application file
namespace Mautic\PageBundle\Model; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Mautic\CoreBundle\Helper\CoreParametersHelper; | |
use Mautic\CoreBundle\Helper\UrlHelper; | |
use Mautic\CoreBundle\Helper\UserHelper; | |
use Mautic\CoreBundle\Model\AbstractCommonModel; | |
use Mautic\CoreBundle\Security\Permissions\CorePermissions; | |
use Mautic\CoreBundle\Translation\Translator; | |
use Mautic\LeadBundle\Entity\LeadFieldRepository; | |
use Mautic\LeadBundle\Helper\TokenHelper; | |
use Mautic\PageBundle\Entity\Redirect; | |
use Mautic\PageBundle\Entity\Trackable; | |
use Mautic\PageBundle\Event\UntrackableUrlsEvent; | |
use Mautic\PageBundle\PageEvents; | |
use Psr\Log\LoggerInterface; | |
use Symfony\Component\EventDispatcher\EventDispatcherInterface; | |
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; | |
/** | |
* @extends AbstractCommonModel<Trackable> | |
*/ | |
class TrackableModel extends AbstractCommonModel | |
{ | |
/** | |
* Array of URLs and/or tokens that should not be converted to trackables. | |
* | |
* @var array | |
*/ | |
protected $doNotTrack = []; | |
/** | |
* Tokens with values that could be used as URLs. | |
* | |
* @var array | |
*/ | |
protected $contentTokens = []; | |
/** | |
* Stores content that needs to be replaced when URLs are parsed out of content. | |
* | |
* @var array | |
*/ | |
protected $contentReplacements = []; | |
/** | |
* Used to rebuild correct URLs when the tokenized URL contains query parameters. | |
* | |
* @var bool | |
*/ | |
protected $usingClickthrough = true; | |
private ?array $contactFieldUrlTokens = null; | |
public function __construct( | |
protected RedirectModel $redirectModel, | |
private LeadFieldRepository $leadFieldRepository, | |
EntityManagerInterface $em, | |
CorePermissions $security, | |
EventDispatcherInterface $dispatcher, | |
UrlGeneratorInterface $router, | |
Translator $translator, | |
UserHelper $userHelper, | |
LoggerInterface $mauticLogger, | |
CoreParametersHelper $coreParametersHelper | |
) { | |
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper); | |
} | |
/** | |
* @return \Mautic\PageBundle\Entity\TrackableRepository | |
*/ | |
public function getRepository() | |
{ | |
return $this->em->getRepository(Trackable::class); | |
} | |
/** | |
* @return RedirectModel | |
*/ | |
protected function getRedirectModel() | |
{ | |
return $this->redirectModel; | |
} | |
/** | |
* @param array $clickthrough | |
* @param bool|false $shortenUrl If true, use the configured shortener service to shorten the URLs | |
* @param array $utmTags | |
* | |
* @return string | |
*/ | |
public function generateTrackableUrl(Trackable $trackable, $clickthrough = [], $shortenUrl = false, $utmTags = []) | |
{ | |
if (!isset($clickthrough['channel'])) { | |
$clickthrough['channel'] = [$trackable->getChannel() => $trackable->getChannelId()]; | |
} | |
$redirect = $trackable->getRedirect(); | |
return $this->getRedirectModel()->generateRedirectUrl($redirect, $clickthrough, $shortenUrl, $utmTags); | |
} | |
/** | |
* Return a channel Trackable entity by URL. | |
* | |
* @return Trackable|null | |
*/ | |
public function getTrackableByUrl($url, $channel, $channelId) | |
{ | |
if (empty($url)) { | |
return null; | |
} | |
// Ensure the URL saved to the database does not have encoded ampersands | |
$url = UrlHelper::decodeAmpersands($url); | |
$trackable = $this->getRepository()->findByUrl($url, $channel, $channelId); | |
if (null == $trackable) { | |
$trackable = $this->createTrackableEntity($url, $channel, $channelId); | |
$this->getRepository()->saveEntity($trackable->getRedirect()); | |
$this->getRepository()->saveEntity($trackable); | |
} | |
return $trackable; | |
} | |
/** | |
* Get Trackable entities by an array of URLs. | |
* | |
* @return array<Trackable> | |
*/ | |
public function getTrackablesByUrls($urls, $channel, $channelId) | |
{ | |
$uniqueUrls = array_unique( | |
array_values($urls) | |
); | |
$trackables = $this->getRepository()->findByUrls( | |
$uniqueUrls, | |
$channel, | |
$channelId | |
); | |
$newRedirects = []; | |
$newTrackables = []; | |
/** @var array<Trackable> $return */ | |
$return = []; | |
/** @var array<string, Trackable> $byUrl */ | |
$byUrl = []; | |
/** @var Trackable $trackable */ | |
foreach ($trackables as $trackable) { | |
$url = $trackable->getRedirect()->getUrl(); | |
$byUrl[$url] = $trackable; | |
} | |
foreach ($urls as $key => $url) { | |
if (empty($url)) { | |
continue; | |
} | |
if (isset($byUrl[$url])) { | |
$return[$key] = $byUrl[$url]; | |
} else { | |
$trackable = $this->createTrackableEntity($url, $channel, $channelId); | |
// Redirect has to be saved first to have ID available | |
$newRedirects[] = $trackable->getRedirect(); | |
$newTrackables[] = $trackable; | |
$return[$key] = $trackable; | |
// Keep track so it can be re-used if applicable | |
$byUrl[$url] = $trackable; | |
} | |
} | |
// Save new entities | |
if (count($newRedirects)) { | |
$this->getRepository()->saveEntities($newRedirects); | |
} | |
if (count($newTrackables)) { | |
$this->getRepository()->saveEntities($newTrackables); | |
} | |
unset($trackables, $newRedirects, $newTrackables, $byUrl); | |
return $return; | |
} | |
/** | |
* Get a list of URLs that are tracked by a specific channel. | |
* | |
* @return mixed[] | |
*/ | |
public function getTrackableList($channel, $channelId): array | |
{ | |
return $this->getRepository()->findByChannel($channel, $channelId); | |
} | |
/** | |
* Returns a list of tokens and/or URLs that should not be converted to trackables. | |
* | |
* @param string|string[]|null $content | |
*/ | |
public function getDoNotTrackList($content): array | |
{ | |
/** @var UntrackableUrlsEvent $event */ | |
$event = $this->dispatcher->dispatch( | |
new UntrackableUrlsEvent($content), | |
PageEvents::REDIRECT_DO_NOT_TRACK | |
); | |
return $event->getDoNotTrackList(); | |
} | |
/** | |
* Extract URLs from content and return as trackables. | |
* | |
* @param string|string[] $content | |
* @param string[] $contentTokens | |
* @param ?string $channel | |
* @param ?int $channelId | |
* @param bool $usingClickthrough Set to false if not using a clickthrough parameter. | |
* This is to ensure that URLs are built correctly with ? or & for | |
* URLs tracked that include query parameters | |
* | |
* @return array{string|string[],Redirect[]|Trackable[]} | |
*/ | |
public function parseContentForTrackables($content, array $contentTokens = [], $channel = null, $channelId = null, $usingClickthrough = true): array | |
{ | |
$this->usingClickthrough = $usingClickthrough; | |
// Set do not track list for validateUrlIsTrackable() | |
$this->doNotTrack = $this->getDoNotTrackList($content); | |
// Set content tokens used by validateUrlIsTrackable() | |
$this->contentTokens = $contentTokens; | |
$contentWasString = false; | |
if (!is_array($content)) { | |
$contentWasString = true; | |
$content = [$content]; | |
} | |
$trackableTokens = []; | |
foreach ($content as $key => $text) { | |
$content[$key] = $this->parseContent($text, $channel, $channelId, $trackableTokens); | |
} | |
return [ | |
$contentWasString ? $content[0] : $content, | |
$trackableTokens, | |
]; | |
} | |
/** | |
* Converts array of Trackable or Redirect entities into {trackable} tokens. | |
* | |
* @param array<string, Trackable|Redirect> $entities | |
* | |
* @return array<string, Redirect|Trackable> | |
*/ | |
protected function createTrackingTokens(array $entities): array | |
{ | |
$tokens = []; | |
foreach ($entities as $trackable) { | |
$redirect = ($trackable instanceof Trackable) ? $trackable->getRedirect() : $trackable; | |
$token = '{trackable='.$redirect->getRedirectId().'}'; | |
$tokens[$token] = $trackable; | |
// Store the URL to be replaced by a token | |
$this->contentReplacements['second_pass'][$redirect->getUrl()] = $token; | |
} | |
return $tokens; | |
} | |
/** | |
* Prepares content for tokenized trackable URLs by replacing them with {trackable=ID} tokens. | |
* | |
* @param string $content | |
* @param string $type html|text | |
* | |
* @return string | |
*/ | |
protected function prepareContentWithTrackableTokens($content, $type) | |
{ | |
if (empty($content)) { | |
return ''; | |
} | |
// Simple search and replace to remove attributes, schema for tokens, and updating URL parameter order | |
$firstPassSearch = array_keys($this->contentReplacements['first_pass']); | |
$firstPassReplace = $this->contentReplacements['first_pass']; | |
$content = str_ireplace($firstPassSearch, $firstPassReplace, $content); | |
// Sort longer to shorter strings to ensure that URLs that share the same base are appropriately replaced | |
uksort($this->contentReplacements['second_pass'], fn ($a, $b): int => strlen($b) - strlen($a)); | |
if ('html' == $type) { | |
// For HTML, replace only the links; leaving the link text (if a URL) intact | |
foreach ($this->contentReplacements['second_pass'] as $search => $replace) { | |
// Make the search regular expression match both "&" and "&". | |
$search = preg_quote($search, '/'); | |
$search = str_replace('&', '&', $search); | |
$search = str_replace('&', '(?:&|&)', $search); | |
$content = preg_replace( | |
'/<(.*?) href=(["\'])'.$search.'(.*?)\\2(.*?)>/i', | |
'<$1 href=$2'.$replace.'$3$2$4>', | |
$content | |
); | |
} | |
} else { | |
// For text, just do a simple search/replace | |
$secondPassSearch = array_keys($this->contentReplacements['second_pass']); | |
$secondPassReplace = $this->contentReplacements['second_pass']; | |
$content = str_ireplace($secondPassSearch, $secondPassReplace, $content); | |
} | |
return $content; | |
} | |
/** | |
* @return array | |
*/ | |
protected function extractTrackablesFromContent($content) | |
{ | |
if (0 !== preg_match('/<[^<]+>/', $content)) { | |
// Parse as HTML | |
$trackableUrls = $this->extractTrackablesFromHtml($content); | |
} else { | |
// Parse as plain text | |
$trackableUrls = $this->extractTrackablesFromText($content); | |
} | |
return $trackableUrls; | |
} | |
/** | |
* Find URLs in HTML and parse into trackables. | |
* | |
* @param string $html HTML content | |
*/ | |
protected function extractTrackablesFromHtml($html): array | |
{ | |
// Find links using DOM to only find <a> tags | |
$libxmlPreviousState = libxml_use_internal_errors(true); | |
libxml_use_internal_errors(true); | |
$dom = new \DOMDocument(); | |
$dom->loadHTML('<?xml encoding="UTF-8">'.$html); | |
libxml_clear_errors(); | |
libxml_use_internal_errors($libxmlPreviousState); | |
$links = $dom->getElementsByTagName('a'); | |
$xpath = new \DOMXPath($dom); | |
$maps = $xpath->query('//map/area'); | |
return array_merge($this->extractTrackables($links), $this->extractTrackables($maps)); | |
} | |
/** | |
* Find URLs in plain text and parse into trackables. | |
* | |
* @param string $text Plain text content | |
*/ | |
protected function extractTrackablesFromText($text): array | |
{ | |
// Remove any HTML tags (such as img) that could contain href or src attributes prior to parsing for links | |
$text = strip_tags($text); | |
// Get a list of URL type contact fields | |
$allUrls = UrlHelper::getUrlsFromPlaintext($text, $this->getContactFieldUrlTokens()); | |
$trackableUrls = []; | |
foreach ($allUrls as $url) { | |
if ($preparedUrl = $this->prepareUrlForTracking($url)) { | |
[$urlKey, $urlValue] = $preparedUrl; | |
$trackableUrls[$urlKey] = $urlValue; | |
} | |
} | |
return $trackableUrls; | |
} | |
/** | |
* Create a Trackable entity. | |
*/ | |
protected function createTrackableEntity($url, $channel, $channelId): Trackable | |
{ | |
$redirect = $this->getRedirectModel()->createRedirectEntity($url); | |
$trackable = new Trackable(); | |
$trackable->setChannel($channel) | |
->setChannelId($channelId) | |
->setRedirect($redirect); | |
return $trackable; | |
} | |
/** | |
* Validate and parse link for tracking. | |
* | |
* @return bool|non-empty-array<mixed, mixed> | |
*/ | |
protected function prepareUrlForTracking(string $url) | |
{ | |
// Ensure it's clean | |
$url = trim($url); | |
// Ensure ampersands are & for the sake of parsing | |
$url = UrlHelper::decodeAmpersands($url); | |
// If this is just a token, validate it's supported before going further | |
if (preg_match('/^{.*?}$/i', $url) && !$this->validateTokenIsTrackable($url)) { | |
return false; | |
} | |
// Default key and final URL to the given $url | |
$trackableKey = $trackableUrl = $url; | |
// Convert URL | |
$urlParts = parse_url($url); | |
// We need to ignore not parsable and invalid urls | |
if (false === $urlParts || !$this->isValidUrl($urlParts, false)) { | |
return false; | |
} | |
// Check if URL is trackable | |
$tokenizedHost = (!isset($urlParts['host']) && isset($urlParts['path'])) ? $urlParts['path'] : $urlParts['host']; | |
if (preg_match('/^(\{\S+?\})/', $tokenizedHost, $match)) { | |
$token = $match[1]; | |
// Tokenized hosts that are standalone tokens shouldn't use a scheme since the token value should contain it | |
if ($token === $tokenizedHost && $scheme = (!empty($urlParts['scheme'])) ? $urlParts['scheme'] : false) { | |
// Token has a schema so let's get rid of it before replacing tokens | |
$this->contentReplacements['first_pass'][$scheme.'://'.$tokenizedHost] = $tokenizedHost; | |
unset($urlParts['scheme']); | |
} | |
// Validate that the token is something that can be trackable | |
if (!$this->validateTokenIsTrackable($token, $tokenizedHost)) { | |
return false; | |
} | |
// Do not convert contact tokens | |
if (!$this->isContactFieldToken($token)) { | |
$trackableUrl = (!empty($urlParts['query'])) ? $this->contentTokens[$token].'?'.$urlParts['query'] : $this->contentTokens[$token]; | |
$trackableKey = $trackableUrl; | |
// Replace the URL token with the actual URL | |
$this->contentReplacements['first_pass'][$url] = $trackableUrl; | |
} | |
} else { | |
// Regular URL without a tokenized host | |
$trackableUrl = $this->httpBuildUrl($urlParts); | |
if ($this->isInDoNotTrack($trackableUrl)) { | |
return false; | |
} | |
} | |
if ($this->isInDoNotTrack($trackableUrl)) { | |
return false; | |
} | |
return [$trackableKey, $trackableUrl]; | |
} | |
/** | |
* Determines if a URL/token is in the do not track list. | |
*/ | |
protected function isInDoNotTrack($url): bool | |
{ | |
// Ensure it's not in the do not track list | |
foreach ($this->doNotTrack as $notTrackable) { | |
if (preg_match('~'.$notTrackable.'~', $url)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Validates that a token is trackable as a URL. | |
*/ | |
protected function validateTokenIsTrackable($token, $tokenizedHost = null): bool | |
{ | |
// Validate if this token is listed as not to be tracked | |
if ($this->isInDoNotTrack($token)) { | |
return false; | |
} | |
if ($this->isContactFieldToken($token)) { | |
// Assume it's true as the redirect methods should handle this dynamically | |
return true; | |
} | |
$tokenValue = TokenHelper::getValueFromTokens($this->contentTokens, $token); | |
// Validate that the token is available | |
if (!$tokenValue) { | |
return false; | |
} | |
if ($tokenizedHost) { | |
$url = str_ireplace($token, $tokenValue, $tokenizedHost); | |
return $this->isValidUrl($url, false); | |
} | |
if (!$this->isValidUrl($tokenValue)) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* @param bool $forceScheme | |
*/ | |
protected function isValidUrl($url, $forceScheme = true): bool | |
{ | |
$urlParts = (!is_array($url)) ? parse_url($url) : $url; | |
// Ensure a applicable URL (rule out URLs as just #) | |
if (!isset($urlParts['host']) && !isset($urlParts['path'])) { | |
return false; | |
} | |
// Ensure a valid scheme | |
if (($forceScheme && !isset($urlParts['scheme'])) | |
|| (isset($urlParts['scheme']) | |
&& !in_array( | |
$urlParts['scheme'], | |
['http', 'https', 'ftp', 'ftps', 'mailto'] | |
))) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Find and extract tokens from the URL as this have to be processed outside of tracking tokens. | |
* | |
* @param $urlParts Array from parse_url | |
* | |
* @return array|false | |
*/ | |
protected function extractTokensFromQuery(&$urlParts) | |
{ | |
$tokenizedParams = false; | |
// Check for a token with a query appended such as {pagelink=1}&key=value | |
if (isset($urlParts['path']) && preg_match('/([https?|ftps?]?\{.*?\})&(.*?)$/', $urlParts['path'], $match)) { | |
$urlParts['path'] = $match[1]; | |
if (isset($urlParts['query'])) { | |
// Likely won't happen but append if this exists | |
$urlParts['query'] .= '&'.$match[2]; | |
} else { | |
$urlParts['query'] = $match[2]; | |
} | |
} | |
// Check for tokens in the query | |
if (!empty($urlParts['query'])) { | |
[$tokenizedParams, $untokenizedParams] = $this->parseTokenizedQuery($urlParts['query']); | |
if ($tokenizedParams) { | |
// Rebuild the query without the tokenized query params for now | |
$urlParts['query'] = $this->httpBuildQuery($untokenizedParams); | |
} | |
} | |
return $tokenizedParams; | |
} | |
/** | |
* Group query parameters into those that have tokens and those that do not. | |
* | |
* @return array<array<string, mixed>> [$tokenizedParams[], $untokenizedParams[]] | |
*/ | |
protected function parseTokenizedQuery($query): array | |
{ | |
$tokenizedParams = | |
$untokenizedParams = []; | |
// Test to see if there are tokens in the query and if so, extract and append them to the end of the tracked link | |
if (preg_match('/(\{\S+?\})/', $query)) { | |
// Equal signs in tokens will confuse parse_str so they need to be encoded | |
$query = preg_replace('/\{(\S+?)=(\S+?)\}/', '{$1%3D$2}', $query); | |
parse_str($query, $queryParts); | |
if (is_array($queryParts)) { | |
foreach ($queryParts as $key => $value) { | |
if (preg_match('/(\{\S+?\})/', $key) || preg_match('/(\{\S+?\})/', $value)) { | |
$tokenizedParams[$key] = $value; | |
} else { | |
$untokenizedParams[$key] = $value; | |
} | |
} | |
} | |
} | |
return [$tokenizedParams, $untokenizedParams]; | |
} | |
/** | |
* @return array<string, Trackable|Redirect> | |
*/ | |
protected function getEntitiesFromUrls($trackableUrls, $channel, $channelId) | |
{ | |
if (!empty($channel) && !empty($channelId)) { | |
// Track as channel aware | |
return $this->getTrackablesByUrls($trackableUrls, $channel, $channelId); | |
} | |
// Simple redirects | |
return $this->getRedirectModel()->getRedirectsByUrls($trackableUrls); | |
} | |
/** | |
* @return string | |
*/ | |
protected function httpBuildUrl($parts) | |
{ | |
if (function_exists('http_build_url')) { | |
return http_build_url($parts); | |
} else { | |
/* | |
* Used if extension is not installed | |
* | |
* http_build_url | |
* Stand alone version of http_build_url (http://php.net/manual/en/function.http-build-url.php) | |
* Based on buggy and inefficient version I found at http://www.mediafire.com/?zjry3tynkg5 by tycoonmaster[at]gmail[dot]com | |
* | |
* @author Chris Nasr (chris[at]fuelforthefire[dot]ca) | |
* @copyright Fuel for the Fire | |
* @package http | |
* @version 0.1 | |
* @created 2012-07-26 | |
*/ | |
if (!defined('HTTP_URL_REPLACE')) { | |
// Define constants | |
define('HTTP_URL_REPLACE', 0x0001); // Replace every part of the first URL when there's one of the second URL | |
define('HTTP_URL_JOIN_PATH', 0x0002); // Join relative paths | |
define('HTTP_URL_JOIN_QUERY', 0x0004); // Join query strings | |
define('HTTP_URL_STRIP_USER', 0x0008); // Strip any user authentication information | |
define('HTTP_URL_STRIP_PASS', 0x0010); // Strip any password authentication information | |
define('HTTP_URL_STRIP_PORT', 0x0020); // Strip explicit port numbers | |
define('HTTP_URL_STRIP_PATH', 0x0040); // Strip complete path | |
define('HTTP_URL_STRIP_QUERY', 0x0080); // Strip query string | |
define('HTTP_URL_STRIP_FRAGMENT', 0x0100); // Strip any fragments (#identifier) | |
// Combination constants | |
define('HTTP_URL_STRIP_AUTH', HTTP_URL_STRIP_USER | HTTP_URL_STRIP_PASS); | |
define('HTTP_URL_STRIP_ALL', HTTP_URL_STRIP_AUTH | HTTP_URL_STRIP_PORT | HTTP_URL_STRIP_QUERY | HTTP_URL_STRIP_FRAGMENT); | |
} | |
$flags = HTTP_URL_REPLACE; | |
$url = []; | |
// Scheme and Host are always replaced | |
if (isset($parts['scheme'])) { | |
$url['scheme'] = $parts['scheme']; | |
} | |
if (isset($parts['host'])) { | |
$url['host'] = $parts['host']; | |
} | |
// (If applicable) Replace the original URL with it's new parts | |
if (HTTP_URL_REPLACE & $flags) { | |
// Go through each possible key | |
foreach (['user', 'pass', 'port', 'path', 'query', 'fragment'] as $key) { | |
// If it's set in $parts, replace it in $url | |
if (isset($parts[$key])) { | |
$url[$key] = $parts[$key]; | |
} | |
} | |
} else { | |
// Join the original URL path with the new path | |
if (isset($parts['path']) && (HTTP_URL_JOIN_PATH & $flags)) { | |
if (isset($url['path']) && '' != $url['path']) { | |
// If the URL doesn't start with a slash, we need to merge | |
if ('/' != $url['path'][0]) { | |
// If the path ends with a slash, store as is | |
if ('/' == $parts['path'][strlen($parts['path']) - 1]) { | |
$sBasePath = $parts['path']; | |
} // Else trim off the file | |
else { | |
// Get just the base directory | |
$sBasePath = dirname($parts['path']); | |
} | |
// If it's empty | |
if ('' == $sBasePath) { | |
$sBasePath = '/'; | |
} | |
// Add the two together | |
$url['path'] = $sBasePath.$url['path']; | |
// Free memory | |
unset($sBasePath); | |
} | |
if (str_contains($url['path'], './')) { | |
// Remove any '../' and their directories | |
while (preg_match('/\w+\/\.\.\//', $url['path'])) { | |
$url['path'] = preg_replace('/\w+\/\.\.\//', '', $url['path']); | |
} | |
// Remove any './' | |
$url['path'] = str_replace('./', '', $url['path']); | |
} | |
} else { | |
$url['path'] = $parts['path']; | |
} | |
} | |
// Join the original query string with the new query string | |
if (isset($parts['query']) && (HTTP_URL_JOIN_QUERY & $flags)) { | |
if (isset($url['query'])) { | |
$url['query'] .= '&'.$parts['query']; | |
} else { | |
$url['query'] = $parts['query']; | |
} | |
} | |
} | |
// Strips all the applicable sections of the URL | |
if (HTTP_URL_STRIP_USER & $flags) { | |
unset($url['user']); | |
} | |
if (HTTP_URL_STRIP_PASS & $flags) { | |
unset($url['pass']); | |
} | |
if (HTTP_URL_STRIP_PORT & $flags) { | |
unset($url['port']); | |
} | |
if (HTTP_URL_STRIP_PATH & $flags) { | |
unset($url['path']); | |
} | |
if (HTTP_URL_STRIP_QUERY & $flags) { | |
unset($url['query']); | |
} | |
if (HTTP_URL_STRIP_FRAGMENT & $flags) { | |
unset($url['fragment']); | |
} | |
// Combine the new elements into a string and return it | |
return | |
((isset($url['scheme'])) ? 'mailto' == $url['scheme'] ? $url['scheme'].':' : $url['scheme'].'://' : '') | |
.((isset($url['user'])) ? $url['user'].((isset($url['pass'])) ? ':'.$url['pass'] : '').'@' : '') | |
.($url['host'] ?? '') | |
.((isset($url['port'])) ? ':'.$url['port'] : '') | |
.($url['path'] ?? '') | |
.((!empty($url['query'])) ? '?'.$url['query'] : '') | |
.((!empty($url['fragment'])) ? '#'.$url['fragment'] : ''); | |
} | |
} | |
/** | |
* Build query string while accounting for tokens that include an equal sign. | |
* | |
* @return mixed|string | |
*/ | |
protected function httpBuildQuery(array $queryParts) | |
{ | |
$query = http_build_query($queryParts); | |
// http_build_query likely encoded tokens so that has to be fixed so they get replaced | |
$query = preg_replace_callback( | |
'/%7B(\S+?)%7D/i', | |
fn ($matches): string => urldecode($matches[0]), | |
$query | |
); | |
return $query; | |
} | |
private function isContactFieldToken($token): bool | |
{ | |
return str_contains($token, '{contactfield') || str_contains($token, '{leadfield'); | |
} | |
/** | |
* @param array<int, Redirect|Trackable> $trackableTokens | |
* | |
* @return string | |
*/ | |
private function parseContent($content, $channel, $channelId, array &$trackableTokens) | |
{ | |
// Reset content replacement arrays | |
$this->contentReplacements = [ | |
// PHPSTAN reported duplicate keys in this array. I can't determine which is the right one. | |
// I'm leaving the second one to keep current behaviour but leaving the first one commented | |
// out as it may be the one we want. | |
// 'first_pass' => [ | |
// // Remove internal attributes | |
// // Editor may convert to HTML4 | |
// 'mautic:disable-tracking=""' => '', | |
// // HTML5 | |
// 'mautic:disable-tracking' => '', | |
// ], | |
'first_pass' => [], | |
'second_pass' => [], | |
]; | |
$trackableUrls = $this->extractTrackablesFromContent($content); | |
$contentType = (preg_match('/<(.*?) href/i', $content)) ? 'html' : 'text'; | |
if (count($trackableUrls)) { | |
// Create Trackable/Redirect entities for the URLs | |
$entities = $this->getEntitiesFromUrls($trackableUrls, $channel, $channelId); | |
unset($trackableUrls); | |
// Get a list of url => token to return to calling method and also to be used to | |
// replace the urls in the content with tokens | |
$trackableTokens = array_merge( | |
$trackableTokens, | |
$this->createTrackingTokens($entities) | |
); | |
unset($entities); | |
// Replace URLs in content with tokens | |
$content = $this->prepareContentWithTrackableTokens($content, $contentType); | |
} elseif (!empty($this->contentReplacements['first_pass'])) { | |
// Replace URLs in content with tokens | |
$content = $this->prepareContentWithTrackableTokens($content, $contentType); | |
} | |
return $content; | |
} | |
/** | |
* @return array | |
*/ | |
protected function getContactFieldUrlTokens() | |
{ | |
if (null !== $this->contactFieldUrlTokens) { | |
return $this->contactFieldUrlTokens; | |
} | |
$this->contactFieldUrlTokens = []; | |
$fieldEntities = $this->leadFieldRepository->getFieldsByType('url'); | |
foreach ($fieldEntities as $field) { | |
$this->contactFieldUrlTokens[] = $field->getAlias(); | |
} | |
$this->leadFieldRepository->detachEntities($fieldEntities); | |
return $this->contactFieldUrlTokens; | |
} | |
/** | |
* @param \DOMNodeList<\DOMNode> $links | |
* | |
* @return array<string, string> | |
*/ | |
private function extractTrackables(\DOMNodeList $links): array | |
{ | |
$trackableUrls = []; | |
/** @var \DOMElement $link */ | |
foreach ($links as $link) { | |
$url = $link->getAttribute('href'); | |
if ('' === $url) { | |
continue; | |
} | |
// Check for a do not track | |
if ($link->hasAttribute('mautic:disable-tracking')) { | |
$this->doNotTrack[$url] = $url; | |
continue; | |
} | |
if ($preparedUrl = $this->prepareUrlForTracking($url)) { | |
[$urlKey, $urlValue] = $preparedUrl; | |
$trackableUrls[$urlKey] = $urlValue; | |
} | |
} | |
return $trackableUrls; | |
} | |
} | |