=');
}
/**
* Parse mixed input into an array.
*
* Accepts either a native array or a JSON string. For string inputs,
* it attempts decoding the raw payload first, then retries with
* `wp_unslash()` only when the string changes. Returns `$default` when
* decoding fails or when the decoded JSON is not an array.
*
* @param mixed $value Input value from request/body.
* @param array $default Fallback value when parsing fails.
* @return array
*/
public static function parseArrayOrJson($value, $default = [])
{
if (!is_string($value)) {
return is_array($value) ? $value : $default;
}
$payloads = [$value];
$unslashed = wp_unslash($value);
if ($unslashed !== $value) {
$payloads[] = $unslashed;
}
foreach ($payloads as $payload) {
$decoded = json_decode($payload, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
return $decoded;
}
}
return $default;
}
public static function getLinksFromString($string)
{
preg_match_all('/]+(href\=["|\'](http.*?)["|\'])/m', $string, $urls);
if (!empty($urls[2])) {
return $urls[2];
}
return [];
}
public static function urlReplaces($string)
{
preg_match_all('/]+(href=["\'](http[^"\']*)["\'])/m', $string, $urls);
$replaces = $urls[1];
$urls = $urls[2];
// Replace '|' with '%7C' in the URLs
$urls = array_map(function ($url) {
return str_replace('|', '%7C', $url);
}, $urls);
$formatted = [];
$baseUrl = self::getSiteUrl();
foreach ($urls as $index => $url) {
$urlSlug = UrlStores::getUrlSlug($url);
if (!$urlSlug) {
continue;
}
$formatted[$replaces[$index]] = add_query_arg([
'ns_url' => $urlSlug
], $baseUrl);
}
return $formatted;
}
public static function attachUrls($html, $campaignUrls, $insertId, $hash = false)
{
$hasSmartUrl = strpos($html, 'smart_url') !== false;
foreach ($campaignUrls as $src => $url) {
$url .= '&mid=' . $insertId;
if ($hash) {
$url .= '&fch=' . substr($hash, 0, 8);
}
if ($hasSmartUrl && strpos($src, 'smart_url') !== false) {
$url .= '&signed_hash=' . rawurlencode(self::signSmartUrlHash($hash));
}
$campaignUrls[$src] = 'href="' . $url . '"';
}
return str_replace(array_keys($campaignUrls), array_values($campaignUrls), $html);
}
public static function attachAnonymousUrls($html, $campaignUrls, $insertId, $hash = false)
{
$hasSmartUrl = strpos($html, 'smart_url') !== false;
foreach ($campaignUrls as $src => $url) {
$url .= '&mid=' . $insertId . '&ano=1';
if ($hash) {
$url .= '&fch=' . substr($hash, 0, 8);
}
if ($hasSmartUrl && strpos($src, 'smart_url') !== false) {
$url .= '&signed_hash=' . rawurlencode(self::signSmartUrlHash($hash));
}
$campaignUrls[$src] = 'href="' . $url . '"';
}
return str_replace(array_keys($campaignUrls), array_values($campaignUrls), $html);
}
/**
* Generate an HMAC signature for smart URL verification.
*
* Uses a dedicated persistent key (not wp_salt) so that WordPress
* salt rotation does not invalidate previously sent email links.
*
* @param string $hash The email hash to sign.
* @return string
*/
public static function signSmartUrlHash($hash)
{
return hash_hmac('sha256', $hash, wp_salt('auth'));
}
/**
* Verify a smart URL signed hash.
*
* Supports both the new HMAC signatures and legacy bcrypt hashes
* for backward compatibility with emails sent before the migration.
*
* @param string $emailHash The campaign email hash.
* @param string $signedHash The signed hash from the URL.
* @return bool
*/
public static function verifySmartUrlHash($emailHash, $signedHash)
{
// New HMAC verification (fast, constant-time)
$expected = self::signSmartUrlHash($emailHash);
if (hash_equals($expected, $signedHash)) {
return true;
}
// Backward compatibility: verify legacy bcrypt hashes
// for emails sent before the HMAC migration
return wp_check_password($emailHash, $signedHash);
}
public static function generateEmailHash($insertId = null)
{
return wp_generate_uuid4();
}
public static function injectTrackerPixel($emailBody, $hash, $emailId = null)
{
if (!$hash) {
return $emailBody;
}
$trackingType = fluentcrmTrackEmailOpen();
if (!$trackingType) {
return $emailBody;
}
$args = [
'fluentcrm' => 1,
'route' => 'open',
'_e_hash' => $hash,
'_e_id' => $emailId
];
if ($trackingType === 'anonymous') {
$args['ano'] = 1;
}
$trackImageUrl = add_query_arg($args, self::getSiteUrl());
$trackPixelHtml = '
';
if (strpos($emailBody, '{fluent_track_pixel}') !== false) {
$emailBody = str_replace('{fluent_track_pixel}', $trackPixelHtml, $emailBody);
} elseif (stripos($emailBody, '