vendor/contao/core-bundle/src/Resources/contao/library/Contao/StringUtil.php line 569

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Contao.
  4.  *
  5.  * (c) Leo Feyer
  6.  *
  7.  * @license LGPL-3.0-or-later
  8.  */
  9. namespace Contao;
  10. use Symfony\Component\Filesystem\Path;
  11. /**
  12.  * Provides string manipulation methods
  13.  *
  14.  * Usage:
  15.  *
  16.  *     $short = StringUtil::substr($str, 32);
  17.  *     $html  = StringUtil::substrHtml($str, 32);
  18.  *     $decoded = StringUtil::decodeEntities($str);
  19.  */
  20. class StringUtil
  21. {
  22.     /**
  23.      * Shorten a string to a given number of characters
  24.      *
  25.      * The function preserves words, so the result might be a bit shorter or
  26.      * longer than the number of characters given. It strips all tags.
  27.      *
  28.      * @param string  $strString        The string to shorten
  29.      * @param integer $intNumberOfChars The target number of characters
  30.      * @param string  $strEllipsis      An optional ellipsis to append to the shortened string
  31.      *
  32.      * @return string The shortened string
  33.      */
  34.     public static function substr($strString$intNumberOfChars$strEllipsis=' …')
  35.     {
  36.         $strString preg_replace('/[\t\n\r]+/'' '$strString);
  37.         $strString strip_tags($strString);
  38.         if (mb_strlen($strString) <= $intNumberOfChars)
  39.         {
  40.             return $strString;
  41.         }
  42.         $intCharCount 0;
  43.         $arrWords = array();
  44.         $arrChunks preg_split('/\s+/'$strString);
  45.         foreach ($arrChunks as $strChunk)
  46.         {
  47.             $intCharCount += mb_strlen(static::decodeEntities($strChunk));
  48.             if ($intCharCount++ <= $intNumberOfChars)
  49.             {
  50.                 $arrWords[] = $strChunk;
  51.                 continue;
  52.             }
  53.             // If the first word is longer than $intNumberOfChars already, shorten it
  54.             // with mb_substr() so the method does not return an empty string.
  55.             if (empty($arrWords))
  56.             {
  57.                 $arrWords[] = mb_substr($strChunk0$intNumberOfChars);
  58.             }
  59.             break;
  60.         }
  61.         if ($strEllipsis === false)
  62.         {
  63.             trigger_deprecation('contao/core-bundle''4.0''Passing "false" as third argument to "Contao\StringUtil::substr()" has been deprecated and will no longer work in Contao 5.0. Pass an empty string instead.');
  64.             $strEllipsis '';
  65.         }
  66.         // Deprecated since Contao 4.0, to be removed in Contao 5.0
  67.         if ($strEllipsis === true)
  68.         {
  69.             trigger_deprecation('contao/core-bundle''4.0''Passing "true" as third argument to "Contao\StringUtil::substr()" has been deprecated and will no longer work in Contao 5.0. Pass the ellipsis string instead.');
  70.             $strEllipsis ' …';
  71.         }
  72.         return implode(' '$arrWords) . $strEllipsis;
  73.     }
  74.     /**
  75.      * Shorten an HTML string to a given number of characters
  76.      *
  77.      * The function preserves words, so the result might be a bit shorter or
  78.      * longer than the number of characters given. It preserves allowed tags.
  79.      *
  80.      * @param string  $strString        The string to shorten
  81.      * @param integer $intNumberOfChars The target number of characters
  82.      *
  83.      * @return string The shortened HTML string
  84.      */
  85.     public static function substrHtml($strString$intNumberOfChars)
  86.     {
  87.         $strReturn '';
  88.         $intCharCount 0;
  89.         $arrOpenTags = array();
  90.         $arrTagBuffer = array();
  91.         $arrEmptyTags = array('area''base''br''col''embed''hr''img''input''link''meta''param''source''track''wbr');
  92.         $strString preg_replace('/[\t\n\r]+/'' '$strString);
  93.         $strString strip_tags($strStringConfig::get('allowedTags'));
  94.         $strString preg_replace('/ +/'' '$strString);
  95.         // Separate tags and text
  96.         $arrChunks preg_split('/(<[^>]+>)/'$strString, -1PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  97.         for ($i=0$c=\count($arrChunks); $i<$c$i++)
  98.         {
  99.             // Buffer tags to include them later
  100.             if (preg_match('/<([^>]+)>/'$arrChunks[$i]))
  101.             {
  102.                 $arrTagBuffer[] = $arrChunks[$i];
  103.                 continue;
  104.             }
  105.             $buffer $arrChunks[$i];
  106.             // Get the substring of the current text
  107.             if (!$arrChunks[$i] = static::substr($arrChunks[$i], ($intNumberOfChars $intCharCount), false))
  108.             {
  109.                 break;
  110.             }
  111.             $blnModified = ($buffer !== $arrChunks[$i]);
  112.             $intCharCount += mb_strlen(static::decodeEntities($arrChunks[$i]));
  113.             if ($intCharCount <= $intNumberOfChars)
  114.             {
  115.                 foreach ($arrTagBuffer as $strTag)
  116.                 {
  117.                     $strTagName strtolower(trim($strTag));
  118.                     // Extract the tag name (see #5669)
  119.                     if (($pos strpos($strTagName' ')) !== false)
  120.                     {
  121.                         $strTagName substr($strTagName1$pos 1);
  122.                     }
  123.                     else
  124.                     {
  125.                         $strTagName substr($strTagName1, -1);
  126.                     }
  127.                     // Skip empty tags
  128.                     if (\in_array($strTagName$arrEmptyTags))
  129.                     {
  130.                         continue;
  131.                     }
  132.                     // Store opening tags in the open_tags array
  133.                     if (strncmp($strTagName'/'1) !== 0)
  134.                     {
  135.                         if ($i<$c || !empty($arrChunks[$i]))
  136.                         {
  137.                             $arrOpenTags[] = $strTagName;
  138.                         }
  139.                         continue;
  140.                     }
  141.                     // Closing tags will be removed from the "open tags" array
  142.                     if ($i<$c || !empty($arrChunks[$i]))
  143.                     {
  144.                         $arrOpenTags array_values($arrOpenTags);
  145.                         for ($j=\count($arrOpenTags)-1$j>=0$j--)
  146.                         {
  147.                             if ($strTagName == '/' $arrOpenTags[$j])
  148.                             {
  149.                                 unset($arrOpenTags[$j]);
  150.                                 break;
  151.                             }
  152.                         }
  153.                     }
  154.                 }
  155.                 // If the current chunk contains text, add tags and text to the return string
  156.                 if ($i<$c || \strlen($arrChunks[$i]))
  157.                 {
  158.                     $strReturn .= implode(''$arrTagBuffer) . $arrChunks[$i];
  159.                 }
  160.                 // Stop after the first shortened chunk (see #7311)
  161.                 if ($blnModified)
  162.                 {
  163.                     break;
  164.                 }
  165.                 $arrTagBuffer = array();
  166.                 continue;
  167.             }
  168.             break;
  169.         }
  170.         // Close all remaining open tags
  171.         krsort($arrOpenTags);
  172.         foreach ($arrOpenTags as $strTag)
  173.         {
  174.             $strReturn .= '</' $strTag '>';
  175.         }
  176.         return trim($strReturn);
  177.     }
  178.     /**
  179.      * Decode all entities
  180.      *
  181.      * @param mixed   $strString     The string to decode
  182.      * @param integer $strQuoteStyle The quote style (defaults to ENT_QUOTES)
  183.      * @param string  $strCharset    An optional charset
  184.      *
  185.      * @return string The decoded string
  186.      */
  187.     public static function decodeEntities($strString$strQuoteStyle=ENT_QUOTES$strCharset=null)
  188.     {
  189.         if ((string) $strString === '')
  190.         {
  191.             return '';
  192.         }
  193.         if ($strCharset === null)
  194.         {
  195.             $strCharset 'UTF-8';
  196.         }
  197.         else
  198.         {
  199.             trigger_deprecation('contao/core-bundle''4.13''Passing a charset to StringUtil::decodeEntities() has been deprecated and will no longer work in Contao 5.0. Always use UTF-8 instead.');
  200.         }
  201.         $strString preg_replace('/(&#*\w+)[\x00-\x20]+;/i''$1;'$strString);
  202.         $strString preg_replace('/(&#x*)([0-9a-f]+);/i''$1$2;'$strString);
  203.         return html_entity_decode($strString$strQuoteStyle$strCharset);
  204.     }
  205.     /**
  206.      * Restore basic entities
  207.      *
  208.      * @param string|array $strBuffer The string with the tags to be replaced
  209.      *
  210.      * @return string|array The string with the original entities
  211.      */
  212.     public static function restoreBasicEntities($strBuffer)
  213.     {
  214.         return str_replace(array('[&]''[&amp;]''[lt]''[gt]''[nbsp]''[-]'), array('&amp;''&amp;''&lt;''&gt;''&nbsp;''&shy;'), $strBuffer);
  215.     }
  216.     /**
  217.      * Generate an alias from a string
  218.      *
  219.      * @param string $strString The string
  220.      *
  221.      * @return string The alias
  222.      */
  223.     public static function generateAlias($strString)
  224.     {
  225.         $strString = static::decodeEntities($strString);
  226.         $strString = static::restoreBasicEntities($strString);
  227.         $strString = static::standardize(strip_tags($strString));
  228.         // Remove the prefix if the alias is not numeric (see #707)
  229.         if (strncmp($strString'id-'3) === && !is_numeric($strSubstr substr($strString3)))
  230.         {
  231.             $strString $strSubstr;
  232.         }
  233.         return $strString;
  234.     }
  235.     /**
  236.      * Prepare a slug
  237.      *
  238.      * @param string $strSlug The slug
  239.      *
  240.      * @return string
  241.      */
  242.     public static function prepareSlug($strSlug)
  243.     {
  244.         $strSlug = static::stripInsertTags($strSlug);
  245.         $strSlug = static::restoreBasicEntities($strSlug);
  246.         $strSlug = static::decodeEntities($strSlug);
  247.         return $strSlug;
  248.     }
  249.     /**
  250.      * Censor a single word or an array of words within a string
  251.      *
  252.      * @param string $strString  The string to censor
  253.      * @param mixed  $varWords   A string or array or words to replace
  254.      * @param string $strReplace An optional replacement string
  255.      *
  256.      * @return string The cleaned string
  257.      */
  258.     public static function censor($strString$varWords$strReplace='')
  259.     {
  260.         foreach ((array) $varWords as $strWord)
  261.         {
  262.             $strString preg_replace('/\b(' str_replace('\*''\w*?'preg_quote($strWord'/')) . ')\b/i'$strReplace$strString);
  263.         }
  264.         return $strString;
  265.     }
  266.     /**
  267.      * Encode all e-mail addresses within a string
  268.      *
  269.      * @param string $strString The string to encode
  270.      *
  271.      * @return string The encoded string
  272.      */
  273.     public static function encodeEmail($strString)
  274.     {
  275.         if (strpos($strString'@') === false)
  276.         {
  277.             return $strString;
  278.         }
  279.         $arrEmails = static::extractEmail($strStringConfig::get('allowedTags'));
  280.         foreach ($arrEmails as $strEmail)
  281.         {
  282.             $strEncoded '';
  283.             $arrCharacters mb_str_split($strEmail);
  284.             foreach ($arrCharacters as $index => $strCharacter)
  285.             {
  286.                 $strEncoded .= sprintf(($index 2) ? '&#x%X;' '&#%s;'mb_ord($strCharacter));
  287.             }
  288.             $strString str_replace($strEmail$strEncoded$strString);
  289.         }
  290.         return str_replace('mailto:''&#109;&#97;&#105;&#108;&#116;&#111;&#58;'$strString);
  291.     }
  292.     /**
  293.      * Extract all e-mail addresses from a string
  294.      *
  295.      * @param string $strString      The string
  296.      * @param string $strAllowedTags A list of allowed HTML tags
  297.      *
  298.      * @return array The e-mail addresses
  299.      */
  300.     public static function extractEmail($strString$strAllowedTags='')
  301.     {
  302.         $arrEmails = array();
  303.         if (strpos($strString'@') === false)
  304.         {
  305.             return $arrEmails;
  306.         }
  307.         // Find all mailto: addresses
  308.         preg_match_all('/mailto:(?:[^\x00-\x20\x22\x40\x7F]{1,64}+|\x22[^\x00-\x1F\x7F]{1,64}?\x22)@(?:\[(?:IPv)?[a-f0-9.:]{1,47}]|[\w.-]{1,252}\.[a-z]{2,63}\b)/u'$strString$matches);
  309.         foreach ($matches[0] as &$strEmail)
  310.         {
  311.             $strEmail str_replace('mailto:'''$strEmail);
  312.             if (Validator::isEmail($strEmail))
  313.             {
  314.                 $arrEmails[] = $strEmail;
  315.             }
  316.         }
  317.         unset($strEmail);
  318.         // Encode opening arrow brackets (see #3998)
  319.         $strString preg_replace_callback('@</?([^\s<>/]*)@', static function ($matches) use ($strAllowedTags)
  320.         {
  321.             if (!$matches[1] || stripos($strAllowedTags'<' strtolower($matches[1]) . '>') === false)
  322.             {
  323.                 $matches[0] = str_replace('<''&lt;'$matches[0]);
  324.             }
  325.             return $matches[0];
  326.         }, $strString);
  327.         // Find all addresses in the plain text
  328.         preg_match_all('/(?:[^\x00-\x20\x22\x40\x7F]{1,64}|\x22[^\x00-\x1F\x7F]{1,64}?\x22)@(?:\[(?:IPv)?[a-f0-9.:]{1,47}]|[\w.-]{1,252}\.[a-z]{2,63}\b)/u'strip_tags($strString), $matches);
  329.         foreach ($matches[0] as &$strEmail)
  330.         {
  331.             $strEmail str_replace('&lt;''<'$strEmail);
  332.             if (Validator::isEmail($strEmail))
  333.             {
  334.                 $arrEmails[] = $strEmail;
  335.             }
  336.         }
  337.         return array_unique($arrEmails);
  338.     }
  339.     /**
  340.      * Split a friendly-name e-mail address and return name and e-mail as array
  341.      *
  342.      * @param string $strEmail A friendly-name e-mail address
  343.      *
  344.      * @return array An array with name and e-mail address
  345.      */
  346.     public static function splitFriendlyEmail($strEmail)
  347.     {
  348.         if (strpos($strEmail'<') !== false)
  349.         {
  350.             return array_map('trim'explode(' <'str_replace('>'''$strEmail)));
  351.         }
  352.         if (strpos($strEmail'[') !== false)
  353.         {
  354.             return array_map('trim'explode(' ['str_replace(']'''$strEmail)));
  355.         }
  356.         return array(''$strEmail);
  357.     }
  358.     /**
  359.      * Wrap words after a particular number of characers
  360.      *
  361.      * @param string  $strString The string to wrap
  362.      * @param integer $strLength The number of characters to wrap after
  363.      * @param string  $strBreak  An optional break character
  364.      *
  365.      * @return string The wrapped string
  366.      */
  367.     public static function wordWrap($strString$strLength=75$strBreak="\n")
  368.     {
  369.         return wordwrap($strString$strLength$strBreak);
  370.     }
  371.     /**
  372.      * Highlight a phrase within a string
  373.      *
  374.      * @param string $strString     The string
  375.      * @param string $strPhrase     The phrase to highlight
  376.      * @param string $strOpeningTag The opening tag (defaults to <strong>)
  377.      * @param string $strClosingTag The closing tag (defaults to </strong>)
  378.      *
  379.      * @return string The highlighted string
  380.      */
  381.     public static function highlight($strString$strPhrase$strOpeningTag='<strong>'$strClosingTag='</strong>')
  382.     {
  383.         if (!$strString || !$strPhrase)
  384.         {
  385.             return $strString;
  386.         }
  387.         return preg_replace('/(' preg_quote($strPhrase'/') . ')/i'$strOpeningTag '\\1' $strClosingTag$strString);
  388.     }
  389.     /**
  390.      * Split a string of comma separated values
  391.      *
  392.      * @param string $strString    The string to split
  393.      * @param string $strDelimiter An optional delimiter
  394.      *
  395.      * @return array The string chunks
  396.      */
  397.     public static function splitCsv($strString$strDelimiter=',')
  398.     {
  399.         $arrValues preg_split('/' $strDelimiter '(?=(?:[^"]*"[^"]*")*(?![^"]*"))/'$strString);
  400.         foreach ($arrValues as $k=>$v)
  401.         {
  402.             $arrValues[$k] = trim($v' "');
  403.         }
  404.         return $arrValues;
  405.     }
  406.     /**
  407.      * Convert a string to XHTML
  408.      *
  409.      * @param string $strString The HTML5 string
  410.      *
  411.      * @return string The XHTML string
  412.      *
  413.      * @deprecated Deprecated since Contao 4.9, to be removed in Contao 5.0
  414.      */
  415.     public static function toXhtml($strString)
  416.     {
  417.         trigger_deprecation('contao/core-bundle''4.9''The "StringUtil::toXhtml()" method has been deprecated and will no longer work in Contao 5.0.');
  418.         $arrPregReplace = array
  419.         (
  420.             '/<(br|hr|img)([^>]*)>/i' => '<$1$2 />'// Close stand-alone tags
  421.             '/ border="[^"]*"/'       => ''          // Remove deprecated attributes
  422.         );
  423.         $arrStrReplace = array
  424.         (
  425.             '/ />'             => ' />',        // Fix incorrectly closed tags
  426.             '<b>'              => '<strong>',   // Replace <b> with <strong>
  427.             '</b>'             => '</strong>',
  428.             '<i>'              => '<em>',       // Replace <i> with <em>
  429.             '</i>'             => '</em>',
  430.             '<u>'              => '<span style="text-decoration:underline">',
  431.             '</u>'             => '</span>',
  432.             ' target="_self"'  => '',
  433.             ' target="_blank"' => ' onclick="return !window.open(this.href)"'
  434.         );
  435.         $strString preg_replace(array_keys($arrPregReplace), $arrPregReplace$strString);
  436.         $strString str_ireplace(array_keys($arrStrReplace), $arrStrReplace$strString);
  437.         return $strString;
  438.     }
  439.     /**
  440.      * Convert a string to HTML5
  441.      *
  442.      * @param string $strString The XHTML string
  443.      *
  444.      * @return string The HTML5 string
  445.      *
  446.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5.0
  447.      */
  448.     public static function toHtml5($strString)
  449.     {
  450.         trigger_deprecation('contao/core-bundle''4.13''The "StringUtil::toHtml5()" method has been deprecated and will no longer work in Contao 5.0.');
  451.         $arrPregReplace = array
  452.         (
  453.             '/<(br|hr|img)([^>]*) \/>/i'                  => '<$1$2>',             // Close stand-alone tags
  454.             '/ (cellpadding|cellspacing|border)="[^"]*"/' => '',                   // Remove deprecated attributes
  455.             '/ rel="lightbox(\[([^\]]+)\])?"/'            => ' data-lightbox="$2"' // see #4073
  456.         );
  457.         $arrStrReplace = array
  458.         (
  459.             '<u>'                                              => '<span style="text-decoration:underline">',
  460.             '</u>'                                             => '</span>',
  461.             ' target="_self"'                                  => '',
  462.             ' onclick="window.open(this.href); return false"'  => ' target="_blank"',
  463.             ' onclick="window.open(this.href);return false"'   => ' target="_blank"',
  464.             ' onclick="window.open(this.href); return false;"' => ' target="_blank"'
  465.         );
  466.         $strString preg_replace(array_keys($arrPregReplace), $arrPregReplace$strString);
  467.         $strString str_ireplace(array_keys($arrStrReplace), $arrStrReplace$strString);
  468.         return $strString;
  469.     }
  470.     /**
  471.      * Parse simple tokens
  472.      *
  473.      * @param string $strString    The string to be parsed
  474.      * @param array  $arrData      The replacement data
  475.      * @param array  $blnAllowHtml Whether HTML should be decoded inside conditions
  476.      *
  477.      * @return string The converted string
  478.      *
  479.      * @throws \RuntimeException         If $strString cannot be parsed
  480.      * @throws \InvalidArgumentException If there are incorrectly formatted if-tags
  481.      *
  482.      * @deprecated Deprecated since Contao 4.10, to be removed in Contao 5.
  483.      *             Use the contao.string.simple_token_parser service instead.
  484.      */
  485.     public static function parseSimpleTokens($strString$arrData$blnAllowHtml true)
  486.     {
  487.         trigger_deprecation('contao/core-bundle''4.10''Using "Contao\StringUtil::parseSimpleTokens()" has been deprecated and will no longer work in Contao 5.0. Use the "contao.string.simple_token_parser" service instead.');
  488.         return System::getContainer()->get('contao.string.simple_token_parser')->parse($strString$arrData$blnAllowHtml);
  489.     }
  490.     /**
  491.      * Convert a UUID string to binary data
  492.      *
  493.      * @param string $uuid The UUID string
  494.      *
  495.      * @return string The binary data
  496.      */
  497.     public static function uuidToBin($uuid)
  498.     {
  499.         return hex2bin(str_replace('-'''$uuid));
  500.     }
  501.     /**
  502.      * Get a UUID string from binary data
  503.      *
  504.      * @param string $data The binary data
  505.      *
  506.      * @return string The UUID string
  507.      */
  508.     public static function binToUuid($data)
  509.     {
  510.         return implode('-'unpack('H8time_low/H4time_mid/H4time_high/H4clock_seq/H12node'$data));
  511.     }
  512.     /**
  513.      * Encode a string with Crockford’s Base32 (0123456789ABCDEFGHJKMNPQRSTVWXYZ)
  514.      *
  515.      * @see StringUtil::decodeBase32()
  516.      */
  517.     public static function encodeBase32(string $bytes): string
  518.     {
  519.         $result = array();
  520.         foreach (str_split($bytes5) as $chunk)
  521.         {
  522.             $result[] = substr(
  523.                 str_pad(
  524.                     strtr(
  525.                         base_convert(bin2hex(str_pad($chunk5"\0")), 1632),
  526.                         'ijklmnopqrstuv',
  527.                         'jkmnpqrstvwxyz'// Crockford's Base32
  528.                     ),
  529.                     8,
  530.                     '0',
  531.                     STR_PAD_LEFT,
  532.                 ),
  533.                 0,
  534.                 (int) ceil(\strlen($chunk) * 5),
  535.             );
  536.         }
  537.         return strtoupper(implode(''$result));
  538.     }
  539.     /**
  540.      * Decode a Crockford’s Base32 encoded string
  541.      *
  542.      * Uppercase and lowercase letters are supported. Letters O and o are
  543.      * interpreted as 0. Letters I, i, L and l are interpreted as 1.
  544.      *
  545.      * @see StringUtil::encodeBase32()
  546.      */
  547.     public static function decodeBase32(string $base32String): string
  548.     {
  549.         if (!== preg_match('/^[0-9a-tv-z]*$/i'$base32String))
  550.         {
  551.             throw new \InvalidArgumentException('Base32 string must only consist of digits and letters except "U"');
  552.         }
  553.         $result = array();
  554.         foreach (str_split($base32String8) as $chunk)
  555.         {
  556.             $result[] = substr(
  557.                 hex2bin(
  558.                     str_pad(
  559.                         base_convert(
  560.                             strtr(
  561.                                 str_pad(strtolower($chunk), 8'0'),
  562.                                 'oiljkmnpqrstvwxyz'// Crockford's Base32
  563.                                 '011ijklmnopqrstuv',
  564.                             ),
  565.                             32,
  566.                             16,
  567.                         ),
  568.                         10,
  569.                         '0',
  570.                         STR_PAD_LEFT,
  571.                     ),
  572.                 ),
  573.                 0,
  574.                 (int) floor(\strlen($chunk) * 8),
  575.             );
  576.         }
  577.         return implode(''$result);
  578.     }
  579.     /**
  580.      * Convert file paths inside "src" attributes to insert tags
  581.      *
  582.      * @param string $data The markup string
  583.      *
  584.      * @return string The markup with file paths converted to insert tags
  585.      */
  586.     public static function srcToInsertTag($data)
  587.     {
  588.         $return '';
  589.         $paths preg_split('/((src|href)="([^"]+)")/i'$data, -1PREG_SPLIT_DELIM_CAPTURE);
  590.         for ($i=0$c=\count($paths); $i<$c$i+=4)
  591.         {
  592.             $return .= $paths[$i];
  593.             if (!isset($paths[$i+1]))
  594.             {
  595.                 continue;
  596.             }
  597.             $file FilesModel::findByPath($paths[$i+3]);
  598.             if ($file !== null)
  599.             {
  600.                 $return .= $paths[$i+2] . '="{{file::' . static::binToUuid($file->uuid) . '|urlattr}}"';
  601.             }
  602.             else
  603.             {
  604.                 $return .= $paths[$i+2] . '="' $paths[$i+3] . '"';
  605.             }
  606.         }
  607.         return $return;
  608.     }
  609.     /**
  610.      * Convert insert tags inside "src" attributes to file paths
  611.      *
  612.      * @param string $data The markup string
  613.      *
  614.      * @return string The markup with insert tags converted to file paths
  615.      */
  616.     public static function insertTagToSrc($data)
  617.     {
  618.         $return '';
  619.         $paths preg_split('/((src|href)="([^"]*){{file::([^"}|]+)[^"}]*}}")/i'$data, -1PREG_SPLIT_DELIM_CAPTURE);
  620.         for ($i=0$c=\count($paths); $i<$c$i+=5)
  621.         {
  622.             $return .= $paths[$i];
  623.             if (!isset($paths[$i+1]))
  624.             {
  625.                 continue;
  626.             }
  627.             $file FilesModel::findByUuid($paths[$i+4]);
  628.             if ($file !== null)
  629.             {
  630.                 $return .= $paths[$i+2] . '="' $paths[$i+3] . $file->path '"';
  631.             }
  632.             else
  633.             {
  634.                 $return .= $paths[$i+1];
  635.             }
  636.         }
  637.         return $return;
  638.     }
  639.     /**
  640.      * Sanitize a file name
  641.      *
  642.      * @param string $strName The file name
  643.      *
  644.      * @return string The sanitized file name
  645.      */
  646.     public static function sanitizeFileName($strName)
  647.     {
  648.         // Remove invisible control characters and unused code points
  649.         $strName preg_replace('/[\pC]/u'''$strName);
  650.         if ($strName === null)
  651.         {
  652.             throw new \InvalidArgumentException('The file name could not be sanitzied');
  653.         }
  654.         // Remove special characters not supported on e.g. Windows
  655.         return str_replace(array('\\''/'':''*''?''"''<''>''|'), '-'$strName);
  656.     }
  657.     /**
  658.      * Resolve a flagged URL such as assets/js/core.js|static|10184084
  659.      *
  660.      * @param string $url The URL
  661.      *
  662.      * @return \stdClass The options object
  663.      */
  664.     public static function resolveFlaggedUrl(&$url)
  665.     {
  666.         $options = new \stdClass();
  667.         // Defaults
  668.         $options->static false;
  669.         $options->media  null;
  670.         $options->mtime  null;
  671.         $options->async  false;
  672.         $chunks explode('|'$url);
  673.         // Remove the flags from the URL
  674.         $url $chunks[0];
  675.         for ($i=1$c=\count($chunks); $i<$c$i++)
  676.         {
  677.             if (empty($chunks[$i]))
  678.             {
  679.                 continue;
  680.             }
  681.             switch ($chunks[$i])
  682.             {
  683.                 case 'static':
  684.                     $options->static true;
  685.                     break;
  686.                 case 'async':
  687.                     $options->async true;
  688.                     break;
  689.                 case is_numeric($chunks[$i]):
  690.                     $options->mtime $chunks[$i];
  691.                     break;
  692.                 default:
  693.                     $options->media $chunks[$i];
  694.                     break;
  695.             }
  696.         }
  697.         return $options;
  698.     }
  699.     /**
  700.      * Convert the character encoding
  701.      *
  702.      * @param string $str  The input string
  703.      * @param string $to   The target character set
  704.      * @param string $from An optional source character set
  705.      *
  706.      * @return string The converted string
  707.      */
  708.     public static function convertEncoding($str$to$from=null)
  709.     {
  710.         if ($str !== null && !\is_scalar($str) && !(\is_object($str) && method_exists($str'__toString')))
  711.         {
  712.             trigger_deprecation('contao/core-bundle''4.9''Passing a non-stringable argument to StringUtil::convertEncoding() has been deprecated an will no longer work in Contao 5.0.');
  713.             return '';
  714.         }
  715.         $str = (string) $str;
  716.         if ('' === $str)
  717.         {
  718.             return $str;
  719.         }
  720.         if (!$from)
  721.         {
  722.             $from mb_detect_encoding($str'ASCII,ISO-2022-JP,UTF-8,EUC-JP,ISO-8859-1');
  723.         }
  724.         if ($from == $to)
  725.         {
  726.             return $str;
  727.         }
  728.         return mb_convert_encoding($str$to$from);
  729.     }
  730.     /**
  731.      * Convert special characters to HTML entities preventing double conversions
  732.      *
  733.      * @param string  $strString          The input string
  734.      * @param boolean $blnStripInsertTags True to strip insert tags
  735.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  736.      *
  737.      * @return string The converted string
  738.      */
  739.     public static function specialchars($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  740.     {
  741.         if ($blnStripInsertTags)
  742.         {
  743.             $strString = static::stripInsertTags($strString);
  744.         }
  745.         return htmlspecialchars((string) $strStringENT_QUOTES$GLOBALS['TL_CONFIG']['characterSet'] ?? 'UTF-8'$blnDoubleEncode);
  746.     }
  747.     /**
  748.      * Encodes specialchars and nested insert tags for attributes
  749.      *
  750.      * @param string  $strString          The input string
  751.      * @param boolean $blnStripInsertTags True to strip insert tags
  752.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  753.      *
  754.      * @return string The converted string
  755.      */
  756.     public static function specialcharsAttribute($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  757.     {
  758.         $strString self::specialchars($strString$blnStripInsertTags$blnDoubleEncode);
  759.         // Improve compatibility with JSON in attributes if no insert tags are present
  760.         if ($strString === self::stripInsertTags($strString))
  761.         {
  762.             $strString str_replace('}}''&#125;&#125;'$strString);
  763.         }
  764.         // Encode insert tags too
  765.         $strString preg_replace('/(?:\|attr)?}}/''|attr}}'$strString);
  766.         $strString str_replace('|urlattr|attr}}''|urlattr}}'$strString);
  767.         // Encode all remaining single closing curly braces
  768.         return preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === $match[0] : '&#125;'$strString);
  769.     }
  770.     /**
  771.      * Encodes disallowed protocols and specialchars for URL attributes
  772.      *
  773.      * @param string  $strString          The input string
  774.      * @param boolean $blnStripInsertTags True to strip insert tags
  775.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  776.      *
  777.      * @return string The converted string
  778.      */
  779.     public static function specialcharsUrl($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  780.     {
  781.         $strString self::specialchars($strString$blnStripInsertTags$blnDoubleEncode);
  782.         // Encode insert tags too
  783.         $strString preg_replace('/(?:\|urlattr|\|attr)?}}/''|urlattr}}'$strString);
  784.         // Encode all remaining single closing curly braces
  785.         $strString preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === $match[0] : '&#125;'$strString);
  786.         $colonRegEx '('
  787.             ':'                 // Plain text colon
  788.             '|'                 // OR
  789.             '&colon;'           // Named entity
  790.             '|'                 // OR
  791.             '&#(?:'             // Start of entity
  792.                 'x0*+3a'        // Hex number 3A
  793.                 '(?![0-9a-f])'  // Must not be followed by another hex digit
  794.                 '|'             // OR
  795.                 '0*+58'         // Decimal number 58
  796.                 '(?![0-9])'     // Must not be followed by another digit
  797.             ');?'               // Optional semicolon
  798.         ')i';
  799.         $arrAllowedUrlProtocols System::getContainer()->getParameter('contao.sanitizer.allowed_url_protocols');
  800.         // URL-encode colon to prevent disallowed protocols
  801.         if (
  802.             !preg_match('(^(?:' implode('|'array_map('preg_quote'$arrAllowedUrlProtocols)) . '):)i'self::decodeEntities($strString))
  803.             && preg_match($colonRegExself::stripInsertTags($strString))
  804.         ) {
  805.             $strString preg_replace($colonRegEx'%3A'$strString);
  806.         }
  807.         return $strString;
  808.     }
  809.     /**
  810.      * Remove Contao insert tags from a string
  811.      *
  812.      * @param string $strString The input string
  813.      *
  814.      * @return string The converted string
  815.      */
  816.     public static function stripInsertTags($strString)
  817.     {
  818.         $count 0;
  819.         do
  820.         {
  821.             $strString preg_replace('/{{[^{}]*}}/'''$strString, -1$count);
  822.         }
  823.         while ($count 0);
  824.         return $strString;
  825.     }
  826.     /**
  827.      * Standardize a parameter (strip special characters and convert spaces)
  828.      *
  829.      * @param string  $strString            The input string
  830.      * @param boolean $blnPreserveUppercase True to preserver uppercase characters
  831.      *
  832.      * @return string The converted string
  833.      */
  834.     public static function standardize($strString$blnPreserveUppercase=false)
  835.     {
  836.         $arrSearch = array('/[^\pN\pL \.\&\/_-]+/u''/[ \.\&\/-]+/');
  837.         $arrReplace = array('''-');
  838.         $strString html_entity_decode($strStringENT_QUOTES$GLOBALS['TL_CONFIG']['characterSet'] ?? 'UTF-8');
  839.         $strString = static::stripInsertTags($strString);
  840.         $strString preg_replace($arrSearch$arrReplace$strString);
  841.         if (is_numeric(substr($strString01)))
  842.         {
  843.             $strString 'id-' $strString;
  844.         }
  845.         if (!$blnPreserveUppercase)
  846.         {
  847.             $strString mb_strtolower($strString);
  848.         }
  849.         return trim($strString'-');
  850.     }
  851.     /**
  852.      * Return an unserialized array or the argument
  853.      *
  854.      * @param mixed   $varValue      The serialized string
  855.      * @param boolean $blnForceArray True to always return an array
  856.      *
  857.      * @return mixed The unserialized array or the unprocessed input value
  858.      */
  859.     public static function deserialize($varValue$blnForceArray=false)
  860.     {
  861.         // Already an array
  862.         if (\is_array($varValue))
  863.         {
  864.             return $varValue;
  865.         }
  866.         // Null
  867.         if ($varValue === null)
  868.         {
  869.             return $blnForceArray ? array() : null;
  870.         }
  871.         // Not a string
  872.         if (!\is_string($varValue))
  873.         {
  874.             return $blnForceArray ? array($varValue) : $varValue;
  875.         }
  876.         // Empty string
  877.         if (trim($varValue) === '')
  878.         {
  879.             return $blnForceArray ? array() : '';
  880.         }
  881.         // Not a serialized array (see #1486)
  882.         if (strncmp($varValue'a:'2) !== 0)
  883.         {
  884.             return $blnForceArray ? array($varValue) : $varValue;
  885.         }
  886.         // Potentially including an object (see #6724)
  887.         if (preg_match('/[OoC]:\+?[0-9]+:"/'$varValue))
  888.         {
  889.             trigger_error('StringUtil::deserialize() does not allow serialized objects'E_USER_WARNING);
  890.             return $blnForceArray ? array($varValue) : $varValue;
  891.         }
  892.         $varUnserialized = @unserialize($varValue, array('allowed_classes' => false));
  893.         if (\is_array($varUnserialized))
  894.         {
  895.             $varValue $varUnserialized;
  896.         }
  897.         elseif ($blnForceArray)
  898.         {
  899.             $varValue = array($varValue);
  900.         }
  901.         return $varValue;
  902.     }
  903.     /**
  904.      * Split a string into fragments, remove whitespace and return fragments as array
  905.      *
  906.      * @param string $strPattern The split pattern
  907.      * @param string $strString  The input string
  908.      *
  909.      * @return array The fragments array
  910.      */
  911.     public static function trimsplit($strPattern$strString)
  912.     {
  913.         // Split
  914.         if (\strlen($strPattern) == 1)
  915.         {
  916.             $arrFragments array_map('trim'explode($strPattern$strString));
  917.         }
  918.         else
  919.         {
  920.             $arrFragments array_map('trim'preg_split('/' $strPattern '/ui'$strString));
  921.         }
  922.         // Empty array
  923.         if (\count($arrFragments) < && !\strlen($arrFragments[0]))
  924.         {
  925.             $arrFragments = array();
  926.         }
  927.         return $arrFragments;
  928.     }
  929.     /**
  930.      * Strip the Contao root dir from the given absolute path
  931.      *
  932.      * @param string $path
  933.      *
  934.      * @return string
  935.      *
  936.      * @throws \InvalidArgumentException
  937.      */
  938.     public static function stripRootDir($path)
  939.     {
  940.         // Compare normalized version of the paths
  941.         $projectDir Path::normalize(System::getContainer()->getParameter('kernel.project_dir'));
  942.         $normalizedPath Path::normalize($path);
  943.         $length = \strlen($projectDir);
  944.         if (strncmp($normalizedPath$projectDir$length) !== || \strlen($normalizedPath) <= $length || $normalizedPath[$length] !== '/')
  945.         {
  946.             throw new \InvalidArgumentException(sprintf('Path "%s" is not inside the Contao root dir "%s"'$path$projectDir));
  947.         }
  948.         return (string) substr($path$length 1);
  949.     }
  950.     /**
  951.      * Convert all ampersands into their HTML entity (default) or unencoded value
  952.      *
  953.      * @param string  $strString
  954.      * @param boolean $blnEncode
  955.      *
  956.      * @return string
  957.      */
  958.     public static function ampersand($strString$blnEncode=true): string
  959.     {
  960.         return preg_replace('/&(amp;)?/i', ($blnEncode '&amp;' '&'), $strString);
  961.     }
  962.     /**
  963.      * Convert an input-encoded string back to the raw UTF-8 value it originated from
  964.      *
  965.      * It handles all Contao input encoding specifics like basic entities and encoded entities.
  966.      */
  967.     public static function revertInputEncoding(string $strValue): string
  968.     {
  969.         $strValue = static::restoreBasicEntities($strValue);
  970.         $strValue = static::decodeEntities($strValue);
  971.         // Ensure valid UTF-8
  972.         if (preg_match('//u'$strValue) !== 1)
  973.         {
  974.             $substituteCharacter mb_substitute_character();
  975.             mb_substitute_character(0xFFFD);
  976.             $strValue mb_convert_encoding($strValue'UTF-8''UTF-8');
  977.             mb_substitute_character($substituteCharacter);
  978.         }
  979.         return $strValue;
  980.     }
  981.     /**
  982.      * Convert an input-encoded string to plain text UTF-8
  983.      *
  984.      * Strips or replaces insert tags, strips HTML tags, decodes entities, escapes insert tag braces.
  985.      *
  986.      * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them
  987.      *
  988.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5;
  989.      *             use the Contao\CoreBundle\String\HtmlDecoder service instead
  990.      */
  991.     public static function inputEncodedToPlainText(string $strValuebool $blnRemoveInsertTags false): string
  992.     {
  993.         trigger_deprecation('contao/core-bundle''4.13''Using "StringUtil::inputEncodedToPlainText()" has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\HtmlDecoder" service instead.');
  994.         return System::getContainer()->get('contao.string.html_decoder')->inputEncodedToPlainText($strValue$blnRemoveInsertTags);
  995.     }
  996.     /**
  997.      * Convert an HTML string to plain text with normalized white space
  998.      *
  999.      * It handles all Contao input encoding specifics like insert tags, basic
  1000.      * entities and encoded entities and is meant to be used with content from
  1001.      * fields that have the allowHtml flag enabled.
  1002.      *
  1003.      * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them
  1004.      *
  1005.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5;
  1006.      *             use the Contao\CoreBundle\String\HtmlDecoder service instead
  1007.      */
  1008.     public static function htmlToPlainText(string $strValuebool $blnRemoveInsertTags false): string
  1009.     {
  1010.         trigger_deprecation('contao/core-bundle''4.13''Using "StringUtil::htmlToPlainText()" has been deprecated and will no longer work in Contao 5.0. Use the "Contao\CoreBundle\String\HtmlDecoder" service instead.');
  1011.         return System::getContainer()->get('contao.string.html_decoder')->htmlToPlainText($strValue$blnRemoveInsertTags);
  1012.     }
  1013.     /**
  1014.      * @param float|int $number
  1015.      */
  1016.     public static function numberToString($numberint $precision null): string
  1017.     {
  1018.         if (\is_int($number))
  1019.         {
  1020.             if (null === $precision)
  1021.             {
  1022.                 return (string) $number;
  1023.             }
  1024.             $number = (float) $number;
  1025.         }
  1026.         if (!\is_float($number))
  1027.         {
  1028.             throw new \TypeError(sprintf('Argument 1 passed to %s() must be of the type int|float, %s given'__METHOD__get_debug_type($number)));
  1029.         }
  1030.         if ($precision === null)
  1031.         {
  1032.             $precision = (int) \ini_get('precision');
  1033.         }
  1034.         // Special value from PHP ini
  1035.         if ($precision === -1)
  1036.         {
  1037.             $precision 14;
  1038.         }
  1039.         if ($precision <= 1)
  1040.         {
  1041.             throw new \InvalidArgumentException(sprintf('Precision must be greater than 1, "%s" given.'$precision));
  1042.         }
  1043.         if (!preg_match('/^(-?)(\d)\.(\d+)e([+-]\d+)$/'sprintf('%.' . ($precision 1) . 'e'$number), $match))
  1044.         {
  1045.             throw new \InvalidArgumentException(sprintf('Unable to convert "%s" into a string representation.'$number));
  1046.         }
  1047.         $significantDigits rtrim($match[2] . $match[3], '0');
  1048.         $shiftBy = (int) $match[4] + 1;
  1049.         $signPart $match[1];
  1050.         $wholePart substr(str_pad($significantDigits$shiftBy'0'), 0max(0$shiftBy)) ?: '0';
  1051.         $decimalPart str_repeat('0'max(0, -$shiftBy)) . substr($significantDigitsmax(0$shiftBy));
  1052.         return rtrim("$signPart$wholePart.$decimalPart"'.');
  1053.     }
  1054. }
  1055. class_alias(StringUtil::class, 'StringUtil');