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

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 ENT_SUBSTITUTE ENT_HTML5$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.         $paths preg_split('/((src|href)="([^"]*){{file::([^"}|]+)[^"}]*}}")/i'$data, -1PREG_SPLIT_DELIM_CAPTURE);
  619.         if (!$paths)
  620.         {
  621.             return $data;
  622.         }
  623.         $return '';
  624.         for ($i=0$c=\count($paths); $i<$c$i+=5)
  625.         {
  626.             $return .= $paths[$i];
  627.             if (!isset($paths[$i+1]))
  628.             {
  629.                 continue;
  630.             }
  631.             $file FilesModel::findByUuid($paths[$i+4]);
  632.             if ($file !== null)
  633.             {
  634.                 $return .= $paths[$i+2] . '="' $paths[$i+3] . $file->path '"';
  635.             }
  636.             else
  637.             {
  638.                 $return .= $paths[$i+1];
  639.             }
  640.         }
  641.         return $return;
  642.     }
  643.     /**
  644.      * Sanitize a file name
  645.      *
  646.      * @param string $strName The file name
  647.      *
  648.      * @return string The sanitized file name
  649.      */
  650.     public static function sanitizeFileName($strName)
  651.     {
  652.         // Remove invisible control characters and unused code points
  653.         $strName preg_replace('/[\pC]/u'''$strName);
  654.         if ($strName === null)
  655.         {
  656.             throw new \InvalidArgumentException('The file name could not be sanitzied');
  657.         }
  658.         // Remove special characters not supported on e.g. Windows
  659.         return str_replace(array('\\''/'':''*''?''"''<''>''|'), '-'$strName);
  660.     }
  661.     /**
  662.      * Resolve a flagged URL such as assets/js/core.js|static|10184084
  663.      *
  664.      * @param string $url The URL
  665.      *
  666.      * @return \stdClass The options object
  667.      */
  668.     public static function resolveFlaggedUrl(&$url)
  669.     {
  670.         $options = new \stdClass();
  671.         // Defaults
  672.         $options->static false;
  673.         $options->media  null;
  674.         $options->mtime  null;
  675.         $options->async  false;
  676.         $chunks explode('|'$url);
  677.         // Remove the flags from the URL
  678.         $url $chunks[0];
  679.         for ($i=1$c=\count($chunks); $i<$c$i++)
  680.         {
  681.             if (empty($chunks[$i]))
  682.             {
  683.                 continue;
  684.             }
  685.             switch ($chunks[$i])
  686.             {
  687.                 case 'static':
  688.                     $options->static true;
  689.                     break;
  690.                 case 'async':
  691.                     $options->async true;
  692.                     break;
  693.                 case is_numeric($chunks[$i]):
  694.                     $options->mtime $chunks[$i];
  695.                     break;
  696.                 default:
  697.                     $options->media $chunks[$i];
  698.                     break;
  699.             }
  700.         }
  701.         return $options;
  702.     }
  703.     /**
  704.      * Convert the character encoding
  705.      *
  706.      * @param string $str  The input string
  707.      * @param string $to   The target character set
  708.      * @param string $from An optional source character set
  709.      *
  710.      * @return string The converted string
  711.      */
  712.     public static function convertEncoding($str$to$from=null)
  713.     {
  714.         if ($str !== null && !\is_scalar($str) && !(\is_object($str) && method_exists($str'__toString')))
  715.         {
  716.             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.');
  717.             return '';
  718.         }
  719.         $str = (string) $str;
  720.         if ('' === $str)
  721.         {
  722.             return $str;
  723.         }
  724.         if (!$from)
  725.         {
  726.             $from mb_detect_encoding($str'ASCII,ISO-2022-JP,UTF-8,EUC-JP,ISO-8859-1');
  727.         }
  728.         if ($from == $to)
  729.         {
  730.             return $str;
  731.         }
  732.         return mb_convert_encoding($str$to$from);
  733.     }
  734.     /**
  735.      * Convert special characters to HTML entities preventing double conversions
  736.      *
  737.      * @param string  $strString          The input string
  738.      * @param boolean $blnStripInsertTags True to strip insert tags
  739.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  740.      *
  741.      * @return string The converted string
  742.      */
  743.     public static function specialchars($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  744.     {
  745.         if ($blnStripInsertTags)
  746.         {
  747.             $strString = static::stripInsertTags($strString);
  748.         }
  749.         return htmlspecialchars((string) $strStringENT_QUOTES ENT_SUBSTITUTE ENT_HTML5$GLOBALS['TL_CONFIG']['characterSet'] ?? 'UTF-8'$blnDoubleEncode);
  750.     }
  751.     /**
  752.      * Encodes specialchars and nested insert tags for attributes
  753.      *
  754.      * @param string  $strString          The input string
  755.      * @param boolean $blnStripInsertTags True to strip insert tags
  756.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  757.      *
  758.      * @return string The converted string
  759.      */
  760.     public static function specialcharsAttribute($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  761.     {
  762.         $strString self::specialchars($strString$blnStripInsertTags$blnDoubleEncode);
  763.         // Improve compatibility with JSON in attributes if no insert tags are present
  764.         if ($strString === self::stripInsertTags($strString))
  765.         {
  766.             $strString str_replace('}}''&#125;&#125;'$strString);
  767.         }
  768.         // Encode insert tags too
  769.         $strString preg_replace('/(?:\|attr)?}}/''|attr}}'$strString);
  770.         $strString str_replace('|urlattr|attr}}''|urlattr}}'$strString);
  771.         // Encode all remaining single closing curly braces
  772.         return preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === $match[0] : '&#125;'$strString);
  773.     }
  774.     /**
  775.      * Encodes disallowed protocols and specialchars for URL attributes
  776.      *
  777.      * @param string  $strString          The input string
  778.      * @param boolean $blnStripInsertTags True to strip insert tags
  779.      * @param boolean $blnDoubleEncode    True to encode existing html entities
  780.      *
  781.      * @return string The converted string
  782.      */
  783.     public static function specialcharsUrl($strString$blnStripInsertTags=false$blnDoubleEncode=false)
  784.     {
  785.         $strString self::specialchars($strString$blnStripInsertTags$blnDoubleEncode);
  786.         // Encode insert tags too
  787.         $strString preg_replace('/(?:\|urlattr|\|attr)?}}/''|urlattr}}'$strString);
  788.         // Encode all remaining single closing curly braces
  789.         $strString preg_replace_callback('/}}?/', static fn ($match) => \strlen($match[0]) === $match[0] : '&#125;'$strString);
  790.         $colonRegEx '('
  791.             ':'                 // Plain text colon
  792.             '|'                 // OR
  793.             '&colon;'           // Named entity
  794.             '|'                 // OR
  795.             '&#(?:'             // Start of entity
  796.                 'x0*+3a'        // Hex number 3A
  797.                 '(?![0-9a-f])'  // Must not be followed by another hex digit
  798.                 '|'             // OR
  799.                 '0*+58'         // Decimal number 58
  800.                 '(?![0-9])'     // Must not be followed by another digit
  801.             ');?'               // Optional semicolon
  802.         ')i';
  803.         $arrAllowedUrlProtocols System::getContainer()->getParameter('contao.sanitizer.allowed_url_protocols');
  804.         // URL-encode colon to prevent disallowed protocols
  805.         if (
  806.             !preg_match('(^(?:' implode('|'array_map('preg_quote'$arrAllowedUrlProtocols)) . '):)i'self::decodeEntities($strString))
  807.             && preg_match($colonRegExself::stripInsertTags($strString))
  808.         ) {
  809.             $arrChunks preg_split('/({{[^{}]*}})/'$strString, -1PREG_SPLIT_DELIM_CAPTURE);
  810.             $strString '';
  811.             foreach ($arrChunks as $index => $strChunk)
  812.             {
  813.                 $strString .= ($index 2) ? $strChunk preg_replace($colonRegEx'%3A'$strChunk);
  814.             }
  815.         }
  816.         return $strString;
  817.     }
  818.     /**
  819.      * Remove Contao insert tags from a string
  820.      *
  821.      * @param string $strString The input string
  822.      *
  823.      * @return string The converted string
  824.      */
  825.     public static function stripInsertTags($strString)
  826.     {
  827.         $count 0;
  828.         do
  829.         {
  830.             $strString preg_replace('/{{[^{}]*}}/'''$strString, -1$count);
  831.         }
  832.         while ($count 0);
  833.         return $strString;
  834.     }
  835.     /**
  836.      * Standardize a parameter (strip special characters and convert spaces)
  837.      *
  838.      * @param string  $strString            The input string
  839.      * @param boolean $blnPreserveUppercase True to preserver uppercase characters
  840.      *
  841.      * @return string The converted string
  842.      */
  843.     public static function standardize($strString$blnPreserveUppercase=false)
  844.     {
  845.         $arrSearch = array('/[^\pN\pL \.\&\/_-]+/u''/[ \.\&\/-]+/');
  846.         $arrReplace = array('''-');
  847.         $strString html_entity_decode($strStringENT_QUOTES ENT_SUBSTITUTE ENT_HTML5$GLOBALS['TL_CONFIG']['characterSet'] ?? 'UTF-8');
  848.         $strString = static::stripInsertTags($strString);
  849.         $strString preg_replace($arrSearch$arrReplace$strString);
  850.         if (is_numeric(substr($strString01)))
  851.         {
  852.             $strString 'id-' $strString;
  853.         }
  854.         if (!$blnPreserveUppercase)
  855.         {
  856.             $strString mb_strtolower($strString);
  857.         }
  858.         return trim($strString'-');
  859.     }
  860.     /**
  861.      * Return an unserialized array or the argument
  862.      *
  863.      * @param mixed   $varValue      The serialized string
  864.      * @param boolean $blnForceArray True to always return an array
  865.      *
  866.      * @return mixed The unserialized array or the unprocessed input value
  867.      */
  868.     public static function deserialize($varValue$blnForceArray=false)
  869.     {
  870.         // Already an array
  871.         if (\is_array($varValue))
  872.         {
  873.             return $varValue;
  874.         }
  875.         // Null
  876.         if ($varValue === null)
  877.         {
  878.             return $blnForceArray ? array() : null;
  879.         }
  880.         // Not a string
  881.         if (!\is_string($varValue))
  882.         {
  883.             return $blnForceArray ? array($varValue) : $varValue;
  884.         }
  885.         // Empty string
  886.         if (trim($varValue) === '')
  887.         {
  888.             return $blnForceArray ? array() : '';
  889.         }
  890.         // Not a serialized array (see #1486)
  891.         if (strncmp($varValue'a:'2) !== 0)
  892.         {
  893.             return $blnForceArray ? array($varValue) : $varValue;
  894.         }
  895.         // Potentially including an object (see #6724)
  896.         if (preg_match('/[OoC]:\+?[0-9]+:"/'$varValue))
  897.         {
  898.             trigger_error('StringUtil::deserialize() does not allow serialized objects'E_USER_WARNING);
  899.             return $blnForceArray ? array($varValue) : $varValue;
  900.         }
  901.         $varUnserialized = @unserialize($varValue, array('allowed_classes' => false));
  902.         if (\is_array($varUnserialized))
  903.         {
  904.             $varValue $varUnserialized;
  905.         }
  906.         elseif ($blnForceArray)
  907.         {
  908.             $varValue = array($varValue);
  909.         }
  910.         return $varValue;
  911.     }
  912.     /**
  913.      * Split a string into fragments, remove whitespace and return fragments as array
  914.      *
  915.      * @param string $strPattern The split pattern
  916.      * @param string $strString  The input string
  917.      *
  918.      * @return array The fragments array
  919.      */
  920.     public static function trimsplit($strPattern$strString)
  921.     {
  922.         // Split
  923.         if (\strlen($strPattern) == 1)
  924.         {
  925.             $arrFragments array_map('trim'explode($strPattern$strString));
  926.         }
  927.         else
  928.         {
  929.             $arrFragments array_map('trim'preg_split('/' $strPattern '/ui'$strString));
  930.         }
  931.         // Empty array
  932.         if (\count($arrFragments) < && !\strlen($arrFragments[0]))
  933.         {
  934.             $arrFragments = array();
  935.         }
  936.         return $arrFragments;
  937.     }
  938.     /**
  939.      * Strip the Contao root dir from the given absolute path
  940.      *
  941.      * @param string $path
  942.      *
  943.      * @return string
  944.      *
  945.      * @throws \InvalidArgumentException
  946.      */
  947.     public static function stripRootDir($path)
  948.     {
  949.         // Compare normalized version of the paths
  950.         $projectDir Path::normalize(System::getContainer()->getParameter('kernel.project_dir'));
  951.         $normalizedPath Path::normalize($path);
  952.         $length \strlen($projectDir);
  953.         if (strncmp($normalizedPath$projectDir$length) !== || \strlen($normalizedPath) <= $length || $normalizedPath[$length] !== '/')
  954.         {
  955.             throw new \InvalidArgumentException(sprintf('Path "%s" is not inside the Contao root dir "%s"'$path$projectDir));
  956.         }
  957.         return (string) substr($path$length 1);
  958.     }
  959.     /**
  960.      * Convert all ampersands into their HTML entity (default) or unencoded value
  961.      *
  962.      * @param string  $strString
  963.      * @param boolean $blnEncode
  964.      *
  965.      * @return string
  966.      */
  967.     public static function ampersand($strString$blnEncode=true): string
  968.     {
  969.         return preg_replace('/&(amp;)?/i', ($blnEncode '&amp;' '&'), $strString);
  970.     }
  971.     /**
  972.      * Convert an input-encoded string back to the raw UTF-8 value it originated from
  973.      *
  974.      * It handles all Contao input encoding specifics like basic entities and encoded entities.
  975.      */
  976.     public static function revertInputEncoding(string $strValue): string
  977.     {
  978.         $strValue = static::restoreBasicEntities($strValue);
  979.         $strValue = static::decodeEntities($strValue);
  980.         // Ensure valid UTF-8
  981.         if (preg_match('//u'$strValue) !== 1)
  982.         {
  983.             $substituteCharacter mb_substitute_character();
  984.             mb_substitute_character(0xFFFD);
  985.             $strValue mb_convert_encoding($strValue'UTF-8''UTF-8');
  986.             mb_substitute_character($substituteCharacter);
  987.         }
  988.         return $strValue;
  989.     }
  990.     /**
  991.      * Convert an input-encoded string to plain text UTF-8
  992.      *
  993.      * Strips or replaces insert tags, strips HTML tags, decodes entities, escapes insert tag braces.
  994.      *
  995.      * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them
  996.      *
  997.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5;
  998.      *             use the Contao\CoreBundle\String\HtmlDecoder service instead
  999.      */
  1000.     public static function inputEncodedToPlainText(string $strValuebool $blnRemoveInsertTags false): string
  1001.     {
  1002.         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.');
  1003.         return System::getContainer()->get('contao.string.html_decoder')->inputEncodedToPlainText($strValue$blnRemoveInsertTags);
  1004.     }
  1005.     /**
  1006.      * Convert an HTML string to plain text with normalized white space
  1007.      *
  1008.      * It handles all Contao input encoding specifics like insert tags, basic
  1009.      * entities and encoded entities and is meant to be used with content from
  1010.      * fields that have the allowHtml flag enabled.
  1011.      *
  1012.      * @param bool $blnRemoveInsertTags True to remove insert tags instead of replacing them
  1013.      *
  1014.      * @deprecated Deprecated since Contao 4.13, to be removed in Contao 5;
  1015.      *             use the Contao\CoreBundle\String\HtmlDecoder service instead
  1016.      */
  1017.     public static function htmlToPlainText(string $strValuebool $blnRemoveInsertTags false): string
  1018.     {
  1019.         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.');
  1020.         return System::getContainer()->get('contao.string.html_decoder')->htmlToPlainText($strValue$blnRemoveInsertTags);
  1021.     }
  1022.     /**
  1023.      * @param float|int $number
  1024.      */
  1025.     public static function numberToString($number, ?int $precision null): string
  1026.     {
  1027.         if (\is_int($number))
  1028.         {
  1029.             if (null === $precision)
  1030.             {
  1031.                 return (string) $number;
  1032.             }
  1033.             $number = (float) $number;
  1034.         }
  1035.         if (!\is_float($number))
  1036.         {
  1037.             throw new \TypeError(sprintf('Argument 1 passed to %s() must be of the type int|float, %s given'__METHOD__get_debug_type($number)));
  1038.         }
  1039.         if ($precision === null)
  1040.         {
  1041.             $precision = (int) \ini_get('precision');
  1042.         }
  1043.         // Special value from PHP ini
  1044.         if ($precision === -1)
  1045.         {
  1046.             $precision 14;
  1047.         }
  1048.         if ($precision <= 1)
  1049.         {
  1050.             throw new \InvalidArgumentException(sprintf('Precision must be greater than 1, "%s" given.'$precision));
  1051.         }
  1052.         if (!preg_match('/^(-?)(\d)\.(\d+)e([+-]\d+)$/'sprintf('%.' . ($precision 1) . 'e'$number), $match))
  1053.         {
  1054.             throw new \InvalidArgumentException(sprintf('Unable to convert "%s" into a string representation.'$number));
  1055.         }
  1056.         $significantDigits rtrim($match[2] . $match[3], '0');
  1057.         $shiftBy = (int) $match[4] + 1;
  1058.         $signPart $match[1];
  1059.         $wholePart substr(str_pad($significantDigits$shiftBy'0'), 0max(0$shiftBy)) ?: '0';
  1060.         $decimalPart str_repeat('0'max(0, -$shiftBy)) . substr($significantDigitsmax(0$shiftBy));
  1061.         return rtrim("$signPart$wholePart.$decimalPart"'.');
  1062.     }
  1063. }
  1064. class_alias(StringUtil::class, 'StringUtil');