HEX
Server: LiteSpeed
System: Linux s1049.use1.mysecurecloudhost.com 4.18.0-477.27.2.lve.el8.x86_64 #1 SMP Wed Oct 11 12:32:56 UTC 2023 x86_64
User: xedaptot (3356)
PHP: 8.3.31
Disabled: NONE
Upload Files
File: /home/xedaptot/ai.naniguide.com/app/Library/StringHelper.php
<?php

/**
 * StringHelper class.
 *
 * Provide string helper methods
 *
 * LICENSE: This product includes software developed at
 * the Acelle Co., Ltd. (http://acellemail.com/).
 *
 * @category   Acelle Library
 *
 * @copyright  Acelle Co., Ltd
 * @license    Acelle Co., Ltd
 *
 * @version    1.0
 *
 * @link       http://acellemail.com
 */

namespace App\Library;

use DOMDocument;
use DomXpath;
use Closure;
use Exception;

use function App\Helpers\handlehtmlentities;
use function App\Helpers\is_non_web_link;

class StringHelper
{
    /**
     * Custom base64 encoding. Replace unsafe url chars.
     *
     * @param string $val
     *
     * @return string
     */
    public static function base64UrlEncode($string)
    {
        if (is_null($string)) {
            return null;
        }

        return str_replace(['+','/','='], ['-','_',''], base64_encode($string));
    }

    /**
     * Custom base64 decode. Replace custom url safe values with normal
     * base64 characters before decoding.
     *
     * @param string $val
     *
     * @return string
     */
    public static function base64UrlDecode($string)
    {
        if (is_null($string)) {
            return null;
        }

        return base64_decode(str_replace(['-','_'], ['+','/'], $string));
    }

    /**
     * Custom base64 decode. Replace custom url safe values with normal
     * base64 characters before decoding.
     *
     * @param string $val
     *
     * @return string
     */
    public static function cleanupMessageId($msgId)
    {
        return preg_replace('/[<>\s]*/', '', $msgId);
    }

    /**
     * Custom base64 decode. Replace custom url safe values with normal
     * base64 characters before decoding.
     *
     * @param string $val
     *
     * @return string
     */
    public static function getDomainFromEmail($emailAddr)
    {
        $s = trim($emailAddr);

        // Extract address inside <...> if present
        if (preg_match('/<\s*([^>]+)\s*>/u', $s, $m)) {
            $s = trim($m[1]);
        }

        // Get substring after last '@'
        $at = strrpos($s, '@');
        if ($at === false) {
            return null;
        }

        return trim(substr($s, $at + 1));
    }

    /**
     * Generate MessageId from domain name.
     *
     * @param string $val
     *
     * @return string
     */
    public static function generateMessageId($domain, $customerUid = null)
    {
        if (is_null($customerUid)) {
            $customerUid = '0000000000000'; // 13 digits
        }

        return time().rand(100000, 999999).'.'.uniqid().'.'.$customerUid.'@'.$domain;
    }

    public static function extractCustomerUidFromMessageId(string $messageId)
    {
        preg_match('/\.(?P<CustomerUid>[a-z0-9]+)@/', $messageId, $matched);

        return $matched['CustomerUid'] ?? null;
    }

    /**
     * Custom base64 decode. Replace custom url safe values with normal
     * base64 characters before decoding.
     *
     * @param string $val
     *
     * @return string
     */
    public static function joinUrl()
    {
        $array = array_map(function ($e) {
            return preg_replace('/(^\/+|\/+$)/', '', $e);
        }, func_get_args());

        return implode('/', $array);
    }

    /**
     * Extract SendGrid X-Message-Id from Smtp-Id
     * For example, extract "GuUFV1znQTmkQyPXrPLyxA" from "<[email protected]>".
     *
     * @param string $val
     *
     * @return string
     */
    public static function extractSendGridMessageId($smtpId)
    {
        $cleaned = self::cleanupMessageId($smtpId);

        return substr($cleaned, 0, strpos($cleaned, '@'));
    }

    /**
     * Detect file encoding.
     *
     * @param string file path
     *
     * @return string encoding or false if cannot detect one
     */
    public static function detectEncoding($file, $max = 100)
    {
        // @important: sometimes PHP fails to correctly detect UTF-8 encoding (it returns ISO-8859-1 for a UTF-8 file!)
        // So, for safety, we simply return false here
        return false;

        // legacy code:
        $file = fopen($file, 'r');

        $sample = '';
        $count = 0;
        while (!feof($file) && $count <= $max) {
            $count += 1;
            $sample .= fgets($file);
        }
        fclose($file);

        return mb_detect_encoding($sample, ['UTF-8', 'ISO-8859-1'], true);
    }

    /**
     * Convert from one encoding to the other.
     *
     * @param string file path
     */
    public static function toUTF8($file, $from = 'UTF-8')
    {
        $content = file_get_contents($file);
        $content = mb_convert_encoding($content, 'UTF-8', $from);
        file_put_contents($file, $content);
    }

    /**
     * Check if a (UTF-8 encoded) file contains BOM
     * Fix it (remove BOM chars) if any.
     *
     * @param string file path
     */
    public static function checkAndRemoveUTF8BOM($file)
    {
        $bom = pack('H*', 'EFBBBF');
        $text = file_get_contents($file);
        $matched = preg_match("/^$bom/", $text, $result);

        if (!$matched) {
            return false;
        }

        $text = preg_replace("/^$bom/", '', $text);
        file_put_contents($file, $text);

        return true;
    }

    // Remove from string, use for email addresses
    public static function removeUTF8BOM($text)
    {
        $bom = pack('H*', 'EFBBBF');

        // Standard method
        $text = preg_replace("/^$bom/", '', $text);

        // More destructive method, as the first method may miss the following ones with more than one BOM:
        // [email protected]
        // [email protected]
        $text = str_replace("\xEF\xBB\xBF", '', $text);
        return $text;
    }

    public static function appendHtml($content, $html, $appendTo = 'body')
    {
        return StringHelper::updateHtmlWithoutWrapping($content, function ($document) use ($html, $appendTo) {
            $tmpDoc = new DOMDocument();
            $tmpDoc->encoding = 'utf-8';
            $tmpDoc->loadHTML(handlehtmlentities($html), LIBXML_NOWARNING | LIBXML_NOERROR);
            $whatToAppend = $tmpDoc->getElementsByTagName('body');
            foreach ($whatToAppend->item(0)->childNodes as $node) {
                $node = $document->importNode($node, true);

                $target = $document->getElementsByTagName($appendTo);

                if ($target->length == 0) {
                    throw new \Exception("Cannot find <$appendTo> tag to append to");
                }

                $target->item(0)->appendChild($node);
            }
        });
    }

    public static function replaceBareLineFeed($content)
    {
        return preg_replace("/(?<=[^\r])\n/", "\r\n", $content);
    }

    public static function getRandomUSIpAddresses()
    {
        $ips = [
            '204.113.58.0',
            '121.54.104.0',
            '213.157.236.0',
            '31.142.80.0',
            '212.95.142.0',
            '172.237.145.0',
            '202.3.232.0',
            '123.50.72.0',
            '62.162.72.0',
            '195.76.126.0',
            '104.235.112.0',
            '203.84.183.0',
            '79.107.195.0',
            '197.210.185.0',
            '216.9.154.0',
            '173.25.32.0',
            '156.157.173.0',
            '31.132.224.0',
            '23.18.2.0',
            '58.181.96.0',
            '201.190.47.0',
            '208.84.83.0',
            '213.101.168.0',
            '201.217.11.0',
            '212.235.251.0',
            '153.215.187.0',
            '217.131.28.0',
            '203.128.83.0',
            '37.218.168.0',
            '61.94.163.0',
            '188.140.171.0',
            '188.135.24.0',
            '89.135.185.0',
            '84.237.238.0',
            '118.97.136.0',
            '61.5.40.0',
            '87.110.26.0',
            '87.110.41.0',
            '41.224.103.0',
            '125.139.107.0',
            '59.98.0.0',
            '222.45.112.0',
            '85.71.183.0',
            '1.9.100.0',
            '213.100.103.0',
            '201.252.22.0',
            '201.240.143.0',
            '201.216.155.0',
            '212.52.128.0',
            '213.154.39.0',
            '59.129.59.0',
            '213.14.139.0',
            '182.1.62.0',
            '93.187.176.0',
            '89.142.48.0',
            '36.37.133.0',
            '58.157.102.0',
            '41.74.46.0',
            '205.242.198.0',
            '59.104.0.0',
            '212.156.99.0',
            '83.239.122.0',
            '88.229.68.0',
            '94.66.154.0',
            '120.89.125.0',
            '220.224.146.0',
            '84.112.150.0',
            '31.206.151.0',
            '37.26.98.0',
            '94.67.116.0',
            '202.188.148.0',
            '109.178.2.0',
            '115.133.224.0',
            '74.128.61.0',
            '125.162.76.0',
            '109.76.207.0',
            '31.209.96.0',
            '194.157.4.0',
            '94.25.40.0',
            '94.66.59.0',
            '213.87.251.0',
            '37.233.130.0',
            '31.144.173.0',
            '89.47.78.0',
            '43.229.13.0',
            '217.16.81.0',
            '203.210.235.0',
            '88.231.42.0',
            '213.179.245.0',
            '31.128.41.0',
            '197.231.251.0',
            '89.143.77.0',
            '78.171.254.0',
            '59.131.117.0',
            '90.139.213.0',
            '87.229.46.0',
            '217.73.140.0',
            '188.245.148.0',
            '94.66.206.0',
            '36.252.32.0',
            '212.26.188.0',
            '217.107.126.0',
            '196.200.61.0',
            '31.152.23.0',
            '223.205.207.0',
            '217.117.176.0',
            '36.37.231.0',
            '202.53.146.0',
            '202.8.73.0',
            '180.54.50.0',
            '103.30.138.0',
            '41.63.223.0',
            '41.73.212.0',
            '31.141.120.0',
            '31.128.90.0',
            '212.90.50.0',
            '202.252.244.0',
            '203.104.24.0',
            '80.249.68.0',
            '196.22.6.0',
            '205.160.111.0',
            '116.96.30.0',
            '117.242.81.0'
        ];

        return $ips[array_rand($ips)];
    }

    public static function purifyHtml($html, $tags = [])
    {
        $defaults = ['script'];
        $defaults = array_merge($defaults, $tags);
        $dom = new DOMDocument();
        libxml_use_internal_errors(true); // disable warning for invalid tags
        $dom->loadHTML($html, LIBXML_HTML_NODEFDTD | LIBXML_HTML_NOIMPLIED);
        foreach ($defaults as $tag) {
            $itemsToRemove = $dom->getElementsByTagName($tag);

            $remove = [];
            foreach ($itemsToRemove as $item) {
                $remove[] = $item;
            }

            foreach ($remove as $item) {
                $item->parentNode->removeChild($item);
            }
        }
        return $dom->saveHTML();
    }

    // @Important: use this helper method whenever it comes to update DOM document
    // This method already solves the following:
    // + UTF8 characters are wrongly encoded
    // + Ampersand & is encoded to &amp;, breaking URLs
    // + Others?
    public static function updateHtml(string $content, Closure $callback)
    {
        $document = new DOMDocument();
        $document->encoding = 'utf-8';
        $document->loadHTML(handlehtmlentities($content), LIBXML_NOWARNING | LIBXML_NOERROR);

        $callback($document);
        // IMPORTANT:
        // Do not use urldecode() here, use rawurldecode() instead
        // Otherwise, it will encode plus (+) chars to spaces, breaking IMG tag with Base64 src content:
        // For example:
        //     <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+kAAA..."
        // Would become:
        //     <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA kAAA..."
        return self::saveHTMLWithUTF8AndDoctype($document);
    }

    public static function updateHtmlWithoutWrapping(string $content, Closure $callback)
    {
        $document = new DOMDocument();
        $document->encoding = 'utf-8';
        $document->loadHTML(handlehtmlentities($content), LIBXML_NOWARNING | LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED);

        $callback($document);
        // IMPORTANT:
        // Do not use urldecode() here, use rawurldecode() instead
        // Otherwise, it will encode plus (+) chars to spaces, breaking IMG tag with Base64 src content:
        // For example:
        //     <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+kAAA..."
        // Would become:
        //     <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA kAAA..."
        return rawurldecode($document->saveHTML($document->documentElement));
    }

    public static function saveHTMLWithUTF8AndDoctype($document)
    {
        // Do not use "htmlspecialchars_decode()" here, which transforms "&gt;" to ">"
        // as it will crash with PHP's DOMDocument
        // Use it as an explicit & final step before rendering HTML content
        $output = rawurldecode($document->saveHTML($document->documentElement));
        $output = '<!DOCTYPE html>'.$output; // saveHTML(params) removes DOCTYPE
        return $output;
    }

    public static function sanitizeFilename($filename)
    {
        return preg_replace('/[^a-zA-Z0-9\.\-]+/', '_', trim($filename));
    }

    public static function generateUniqueName($directory, $name)
    {
        $count = 1;
        $path = join_paths($directory, $name);
        $newName = $name;
        while (file_exists($path)) {
            $regxp = '/(?<ext>\.[^\/\.]+$)/';
            preg_match($regxp, $name, $matched);

            if (array_key_exists('ext', $matched)) {
                $fileExt = $matched['ext'];
            } else {
                $fileExt = '';
            }

            $base = preg_replace($regxp, '', $name);
            $newName = $base.'_'.$count.$fileExt;
            $path = join_paths($directory, $newName);
            $count += 1;
        }

        return $newName;
    }

    public static function isTag($string)
    {
        return preg_match('/{[a-zA-Z0-9_]+}/', $string);
    }

    public static function containsUnsubscribeTag(string $content): bool
    {
        // Match canonical Twig forms only: `{{ unsubscribe_url }}` (lowercase)
        // and `{{ UNSUBSCRIBE_URL }}` (uppercase). Mixed casing would pass an
        // /i regex but fail to render — TagTranslationService::duplicateUppercaseKeys
        // only injects the strtoupper() variant, so `{{ Unsubscribe_url }}` resolves
        // to empty string in the send pipeline and recipients see the literal tag.
        return (bool) preg_match('/\{\{\s*(?:unsubscribe_url|UNSUBSCRIBE_URL)\s*\}\}/', $content);
    }

    public static function fromHumanIpAddress($ipAddress)
    {
        $googleIpRanges = config('google');
        foreach ($googleIpRanges as $cidr) {
            if (\Symfony\Component\HttpFoundation\IpUtils::checkIp($ipAddress, $cidr)) {
                return false;
            }
        }

        return true;
    }

    public static function replaceUrl($html, Closure $process, $throwIfInvalidUrl = true, $throwIfAttrMissing = false, $throwIfAttrEmpty = false)
    {
        return static::updateHtmlWithoutWrapping($html, function ($document) use ($process, $throwIfInvalidUrl, $throwIfAttrMissing, $throwIfAttrEmpty) {
            $searchFor = [
                'link' => 'href',
                'a' => 'href',
                'img' => 'src',
                'video' => 'src',
                'audio' => 'src',
                'source' => 'src',
                'track' => 'src',
                'embed' => 'src',
            ];

            foreach ($searchFor as $tag => $attr) {
                $elements = $document->getElementsByTagName($tag);
                foreach ($elements as $element) {
                    if (!$element->hasAttribute($attr)) {
                        if ($throwIfAttrMissing) {
                            throw new \Exception("Cannot find '{$attr}' attribute for a <{$element->tagName}> tag at: ".$document->saveHTML($element), 1);
                        } else {
                            continue;
                        }

                    }

                    $url = trim($element->getAttribute($attr));

                    if (empty($url)) {
                        if ($throwIfAttrEmpty) {
                            throw new \Exception("'{$attr}' value of an <{$element->tagName}> tag is empty, at: ".$document->saveHTML($element), 1);
                        } else {
                            continue;
                        }
                    }

                    // If the given URL is a non-web link, like <a href="tel:"... (mailto:, ftp: etc.)
                    if (is_non_web_link($url)) {
                        continue;
                    }

                    // parse_url($url, PHP_URL_HOST) returns null when the URL is relative (e.g. "/abc/xyz" or "abc/xyz")
                    $host = parse_url($url, PHP_URL_HOST);

                    if ($host === false) {
                        if ($throwIfInvalidUrl) {
                            throw new \Exception("'{$attr}' value of an <{$element->tagName}> tag is an invalid url: {$url}, at".$document->saveHTML($element), 1);
                        } else {
                            continue;
                        }
                    }

                    /*
                    if (is_null($host)) {
                        $urlWithHost = join_url($this->getAppUrl(), $url);
                        $element->setAttribute($attr, $urlWithHost);
                    }
                    */

                    $updatedUrl = $process($element, $url);
                    $element->setAttribute($attr, $updatedUrl);
                }
            }
        });
    }

    /**
     * Recursively find and modify *all* nodes in a nested associative array.
     *
     * - Traverses any structure (no assumption about "children" key)
     * - Applies closure to every node where [$keyToFind] === $valueToFind
     *
     * @param array &$data
     * @param string $keyToFind
     * @param mixed $valueToFind
     * @param Closure $closure  function (&$element) { ... }
     * @return int              Number of modified nodes
     */
    public static function iterate(array &$data, Closure $closure): int
    {
        $count = 1;
        $closure($data);

        foreach ($data as &$value) {
            if (is_array($value)) {
                $count += static::iterate($value, $closure);
            }
        }

        return $count;
    }
}