vendor/contao/core-bundle/src/Resources/contao/library/Contao/InsertTags.php line 60

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 Contao\CoreBundle\Controller\InsertTagsController;
  11. use Contao\CoreBundle\Image\Studio\FigureRenderer;
  12. use Contao\CoreBundle\Intl\Countries;
  13. use Contao\CoreBundle\Intl\Locales;
  14. use Contao\CoreBundle\Routing\ResponseContext\HtmlHeadBag\HtmlHeadBag;
  15. use Contao\CoreBundle\Routing\ResponseContext\ResponseContextAccessor;
  16. use Contao\CoreBundle\Util\LocaleUtil;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpKernel\Controller\ControllerReference;
  19. use Symfony\Component\HttpKernel\Fragment\FragmentHandler;
  20. use Webmozart\PathUtil\Path;
  21. /**
  22.  * A static class to replace insert tags
  23.  *
  24.  * Usage:
  25.  *
  26.  *     $it = new InsertTags();
  27.  *     echo $it->replace($text);
  28.  *
  29.  * @author Leo Feyer <https://github.com/leofeyer>
  30.  */
  31. class InsertTags extends Controller
  32. {
  33.     /**
  34.      * @var array
  35.      */
  36.     protected static $arrItCache = array();
  37.     /**
  38.      * Make the constructor public
  39.      */
  40.     public function __construct()
  41.     {
  42.         parent::__construct();
  43.     }
  44.     /**
  45.      * Recursively replace insert tags with their values
  46.      *
  47.      * @param string  $strBuffer The text with the tags to be replaced
  48.      * @param boolean $blnCache  If false, non-cacheable tags will be replaced
  49.      *
  50.      * @return string The text with the replaced tags
  51.      */
  52.     public function replace($strBuffer$blnCache=true)
  53.     {
  54.         $strBuffer $this->doReplace($strBuffer$blnCache);
  55.         // Run the replacement recursively (see #8172)
  56.         while (strpos($strBuffer'{{') !== false && ($strTmp $this->doReplace($strBuffer$blnCache)) != $strBuffer)
  57.         {
  58.             $strBuffer $strTmp;
  59.         }
  60.         return $strBuffer;
  61.     }
  62.     /**
  63.      * Reset the insert tag cache
  64.      */
  65.     public static function reset()
  66.     {
  67.         static::$arrItCache = array();
  68.     }
  69.     /**
  70.      * Replace insert tags with their values
  71.      *
  72.      * @param string  $strBuffer The text with the tags to be replaced
  73.      * @param boolean $blnCache  If false, non-cacheable tags will be replaced
  74.      *
  75.      * @return string The text with the replaced tags
  76.      */
  77.     protected function doReplace($strBuffer$blnCache)
  78.     {
  79.         /** @var PageModel $objPage */
  80.         global $objPage;
  81.         // Preserve insert tags
  82.         if (Config::get('disableInsertTags'))
  83.         {
  84.             return StringUtil::restoreBasicEntities($strBuffer);
  85.         }
  86.         $strBuffer self::encodeHtmlAttributes($strBuffer);
  87.         // The first letter must not be a reserved character of Twig, Mustache or similar template engines (see #805)
  88.         $tags preg_split('~{{([a-zA-Z0-9\x80-\xFF][^{}]*)}}~'$strBuffer, -1PREG_SPLIT_DELIM_CAPTURE);
  89.         if (\count($tags) < 2)
  90.         {
  91.             return StringUtil::restoreBasicEntities($strBuffer);
  92.         }
  93.         $strBuffer '';
  94.         $container System::getContainer();
  95.         $blnFeUserLoggedIn $container->get('contao.security.token_checker')->hasFrontendUser();
  96.         // Create one cache per cache setting (see #7700)
  97.         $arrCache = &static::$arrItCache[$blnCache];
  98.         for ($_rit=0$_cnt=\count($tags); $_rit<$_cnt$_rit+=2)
  99.         {
  100.             $strBuffer .= $tags[$_rit];
  101.             // Skip empty tags
  102.             if (empty($tags[$_rit+1]))
  103.             {
  104.                 continue;
  105.             }
  106.             $strTag $tags[$_rit+1];
  107.             $flags explode('|'$strTag);
  108.             $tag array_shift($flags);
  109.             $elements explode('::'$tag);
  110.             // Load the value from cache
  111.             if (isset($arrCache[$strTag]) && $elements[0] != 'page' && !\in_array('refresh'$flags))
  112.             {
  113.                 $strBuffer .= $arrCache[$strTag];
  114.                 continue;
  115.             }
  116.             // Skip certain elements if the output will be cached
  117.             if ($blnCache)
  118.             {
  119.                 if ($elements[0] == 'date' || $elements[0] == 'ua' || $elements[0] == 'post' || ($elements[1] ?? null) == 'back' || ($elements[1] ?? null) == 'referer' || \in_array('uncached'$flags) || strncmp($elements[0], 'cache_'6) === 0)
  120.                 {
  121.                     /** @var FragmentHandler $fragmentHandler */
  122.                     $fragmentHandler $container->get('fragment.handler');
  123.                     $attributes = array('insertTag' => '{{' $strTag '}}');
  124.                     /** @var Request|null $request */
  125.                     $request $container->get('request_stack')->getCurrentRequest();
  126.                     if (null !== $request && ($scope $request->attributes->get('_scope')))
  127.                     {
  128.                         $attributes['_scope'] = $scope;
  129.                     }
  130.                     $strBuffer .= $fragmentHandler->render(
  131.                         new ControllerReference(
  132.                             InsertTagsController::class . '::renderAction',
  133.                             $attributes,
  134.                             array('clientCache' => (int) $objPage->clientCache'pageId' => $objPage->id'request' => Environment::get('request'))
  135.                         ),
  136.                         'esi',
  137.                         array('ignore_errors'=>false// see #48
  138.                     );
  139.                     continue;
  140.                 }
  141.             }
  142.             $arrCache[$strTag] = '';
  143.             // Replace the tag
  144.             switch (strtolower($elements[0]))
  145.             {
  146.                 // Date
  147.                 case 'date':
  148.                     $flags[] = 'attr';
  149.                     $arrCache[$strTag] = Date::parse($elements[1] ?: $objPage->dateFormat);
  150.                     break;
  151.                 // Accessibility tags
  152.                 case 'lang':
  153.                     if (empty($elements[1]))
  154.                     {
  155.                         $arrCache[$strTag] = '</span>';
  156.                     }
  157.                     else
  158.                     {
  159.                         $arrCache[$strTag] = $arrCache[$strTag] = '<span lang="' StringUtil::specialchars($elements[1]) . '">';
  160.                     }
  161.                     break;
  162.                 // Line break
  163.                 case 'br':
  164.                     $arrCache[$strTag] = '<br>';
  165.                     break;
  166.                 // E-mail addresses
  167.                 case 'email':
  168.                 case 'email_open':
  169.                 case 'email_url':
  170.                     if (empty($elements[1]))
  171.                     {
  172.                         $arrCache[$strTag] = '';
  173.                         break;
  174.                     }
  175.                     $strEmail StringUtil::specialcharsUrl(StringUtil::encodeEmail($elements[1]));
  176.                     // Replace the tag
  177.                     switch (strtolower($elements[0]))
  178.                     {
  179.                         case 'email':
  180.                             $arrCache[$strTag] = '<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;' $strEmail '" class="email">' preg_replace('/\?.*$/'''$strEmail) . '</a>';
  181.                             break;
  182.                         case 'email_open':
  183.                             $arrCache[$strTag] = '<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;' $strEmail '" title="' $strEmail '" class="email">';
  184.                             break;
  185.                         case 'email_url':
  186.                             $arrCache[$strTag] = $strEmail;
  187.                             break;
  188.                     }
  189.                     break;
  190.                 // Label tags
  191.                 case 'label':
  192.                     $flags[] = 'attr';
  193.                     $keys explode(':'$elements[1]);
  194.                     if (\count($keys) < 2)
  195.                     {
  196.                         $arrCache[$strTag] = '';
  197.                         break;
  198.                     }
  199.                     if ($keys[0] == 'LNG' && \count($keys) == 2)
  200.                     {
  201.                         try
  202.                         {
  203.                             $arrCache[$strTag] = System::getContainer()->get(Locales::class)->getDisplayNames(array($keys[1]))[$keys[1]];
  204.                             break;
  205.                         }
  206.                         catch (\Throwable $exception)
  207.                         {
  208.                             // Fall back to loading the label via $GLOBALS['TL_LANG']
  209.                         }
  210.                     }
  211.                     if ($keys[0] == 'CNT' && \count($keys) == 2)
  212.                     {
  213.                         try
  214.                         {
  215.                             $arrCache[$strTag] = System::getContainer()->get(Countries::class)->getCountries()[strtoupper($keys[1])] ?? '';
  216.                             break;
  217.                         }
  218.                         catch (\Throwable $exception)
  219.                         {
  220.                             // Fall back to loading the label via $GLOBALS['TL_LANG']
  221.                         }
  222.                     }
  223.                     $file $keys[0];
  224.                     // Map the key (see #7217)
  225.                     switch ($file)
  226.                     {
  227.                         case 'CNT':
  228.                             $file 'countries';
  229.                             break;
  230.                         case 'LNG':
  231.                             $file 'languages';
  232.                             break;
  233.                         case 'MOD':
  234.                         case 'FMD':
  235.                             $file 'modules';
  236.                             break;
  237.                         case 'FFL':
  238.                             $file 'tl_form_field';
  239.                             break;
  240.                         case 'CACHE':
  241.                             $file 'tl_page';
  242.                             break;
  243.                         case 'XPL':
  244.                             $file 'explain';
  245.                             break;
  246.                         case 'XPT':
  247.                             $file 'exception';
  248.                             break;
  249.                         case 'MSC':
  250.                         case 'ERR':
  251.                         case 'CTE':
  252.                         case 'PTY':
  253.                         case 'FOP':
  254.                         case 'CHMOD':
  255.                         case 'DAYS':
  256.                         case 'MONTHS':
  257.                         case 'UNITS':
  258.                         case 'CONFIRM':
  259.                         case 'DP':
  260.                         case 'COLS':
  261.                         case 'SECTIONS':
  262.                         case 'DCA':
  263.                         case 'CRAWL':
  264.                             $file 'default';
  265.                             break;
  266.                     }
  267.                     try
  268.                     {
  269.                         System::loadLanguageFile($file);
  270.                     }
  271.                     catch (\InvalidArgumentException $exception)
  272.                     {
  273.                         $this->log('Invalid label insert tag {{' $strTag '}} on page ' Environment::get('uri') . ': ' $exception->getMessage(), __METHOD__TL_ERROR);
  274.                     }
  275.                     if (\count($keys) == 2)
  276.                     {
  277.                         $arrCache[$strTag] = $GLOBALS['TL_LANG'][$keys[0]][$keys[1]];
  278.                     }
  279.                     else
  280.                     {
  281.                         $arrCache[$strTag] = $GLOBALS['TL_LANG'][$keys[0]][$keys[1]][$keys[2]];
  282.                     }
  283.                     break;
  284.                 // Front end user
  285.                 case 'user':
  286.                     if ($blnFeUserLoggedIn)
  287.                     {
  288.                         $flags[] = 'attr';
  289.                         $this->import(FrontendUser::class, 'User');
  290.                         $value $this->User->{$elements[1]};
  291.                         if (!$value)
  292.                         {
  293.                             $arrCache[$strTag] = $value;
  294.                             break;
  295.                         }
  296.                         $this->loadDataContainer('tl_member');
  297.                         if (($GLOBALS['TL_DCA']['tl_member']['fields'][$elements[1]]['inputType'] ?? null) == 'password')
  298.                         {
  299.                             $arrCache[$strTag] = '';
  300.                             break;
  301.                         }
  302.                         $value StringUtil::deserialize($value);
  303.                         // Decrypt the value
  304.                         if ($GLOBALS['TL_DCA']['tl_member']['fields'][$elements[1]]['eval']['encrypt'] ?? null)
  305.                         {
  306.                             $value Encryption::decrypt($value);
  307.                         }
  308.                         $rgxp $GLOBALS['TL_DCA']['tl_member']['fields'][$elements[1]]['eval']['rgxp'] ?? null;
  309.                         $opts $GLOBALS['TL_DCA']['tl_member']['fields'][$elements[1]]['options'] ?? null;
  310.                         $rfrc $GLOBALS['TL_DCA']['tl_member']['fields'][$elements[1]]['reference'] ?? null;
  311.                         if ($rgxp == 'date')
  312.                         {
  313.                             $arrCache[$strTag] = Date::parse($objPage->dateFormat$value);
  314.                         }
  315.                         elseif ($rgxp == 'time')
  316.                         {
  317.                             $arrCache[$strTag] = Date::parse($objPage->timeFormat$value);
  318.                         }
  319.                         elseif ($rgxp == 'datim')
  320.                         {
  321.                             $arrCache[$strTag] = Date::parse($objPage->datimFormat$value);
  322.                         }
  323.                         elseif (\is_array($value))
  324.                         {
  325.                             $arrCache[$strTag] = implode(', '$value);
  326.                         }
  327.                         elseif (\is_array($opts) && ArrayUtil::isAssoc($opts))
  328.                         {
  329.                             $arrCache[$strTag] = $opts[$value] ?? $value;
  330.                         }
  331.                         elseif (\is_array($rfrc))
  332.                         {
  333.                             $arrCache[$strTag] = isset($rfrc[$value]) ? ((\is_array($rfrc[$value])) ? $rfrc[$value][0] : $rfrc[$value]) : $value;
  334.                         }
  335.                         else
  336.                         {
  337.                             $arrCache[$strTag] = $value;
  338.                         }
  339.                         // Convert special characters (see #1890)
  340.                         $arrCache[$strTag] = StringUtil::specialchars($arrCache[$strTag]);
  341.                     }
  342.                     break;
  343.                 // Link
  344.                 case 'link':
  345.                 case 'link_open':
  346.                 case 'link_url':
  347.                 case 'link_title':
  348.                 case 'link_target':
  349.                 case 'link_name':
  350.                     $strTarget null;
  351.                     $strClass '';
  352.                     // Back link
  353.                     if ($elements[1] == 'back')
  354.                     {
  355.                         @trigger_error('Using the link::back insert tag has been deprecated and will no longer work in Contao 5.0.'E_USER_DEPRECATED);
  356.                         $strUrl 'javascript:history.go(-1)';
  357.                         $strTitle $GLOBALS['TL_LANG']['MSC']['goBack'] ?? null;
  358.                         // No language files if the page is cached
  359.                         if (!$strTitle)
  360.                         {
  361.                             $strTitle 'Go back';
  362.                         }
  363.                         $strName $strTitle;
  364.                     }
  365.                     // External links
  366.                     elseif (strncmp($elements[1], 'http://'7) === || strncmp($elements[1], 'https://'8) === 0)
  367.                     {
  368.                         $strUrl StringUtil::specialcharsUrl($elements[1]);
  369.                         $strTitle $elements[1];
  370.                         $strName str_replace(array('http://''https://'), ''$strUrl);
  371.                     }
  372.                     // Regular link
  373.                     else
  374.                     {
  375.                         // User login page
  376.                         if ($elements[1] == 'login')
  377.                         {
  378.                             if (!$blnFeUserLoggedIn)
  379.                             {
  380.                                 break;
  381.                             }
  382.                             $this->import(FrontendUser::class, 'User');
  383.                             $elements[1] = $this->User->loginPage;
  384.                         }
  385.                         $objNextPage PageModel::findByIdOrAlias($elements[1]);
  386.                         if ($objNextPage === null)
  387.                         {
  388.                             // Prevent broken markup with link_open and link_close (see #92)
  389.                             if (strtolower($elements[0]) == 'link_open')
  390.                             {
  391.                                 $arrCache[$strTag] = '<a>';
  392.                             }
  393.                             break;
  394.                         }
  395.                         // Page type specific settings (thanks to Andreas Schempp)
  396.                         switch ($objNextPage->type)
  397.                         {
  398.                             case 'redirect':
  399.                                 $strUrl $objNextPage->url;
  400.                                 if (strncasecmp($strUrl'mailto:'7) === 0)
  401.                                 {
  402.                                     $strUrl StringUtil::encodeEmail($strUrl);
  403.                                 }
  404.                                 break;
  405.                             case 'forward':
  406.                                 if ($objNextPage->jumpTo)
  407.                                 {
  408.                                     $objNext PageModel::findPublishedById($objNextPage->jumpTo);
  409.                                 }
  410.                                 else
  411.                                 {
  412.                                     $objNext PageModel::findFirstPublishedRegularByPid($objNextPage->id);
  413.                                 }
  414.                                 if ($objNext instanceof PageModel)
  415.                                 {
  416.                                     $strUrl = \in_array('absolute', \array_slice($elements2), true) || \in_array('absolute'$flagstrue) ? $objNext->getAbsoluteUrl() : $objNext->getFrontendUrl();
  417.                                     break;
  418.                                 }
  419.                                 // no break
  420.                             default:
  421.                                 $strUrl = \in_array('absolute', \array_slice($elements2), true) || \in_array('absolute'$flagstrue) ? $objNextPage->getAbsoluteUrl() : $objNextPage->getFrontendUrl();
  422.                                 break;
  423.                         }
  424.                         $strName $objNextPage->title;
  425.                         $strTarget $objNextPage->target ' target="_blank" rel="noreferrer noopener"' '';
  426.                         $strClass $objNextPage->cssClass sprintf(' class="%s"'$objNextPage->cssClass) : '';
  427.                         $strTitle $objNextPage->pageTitle ?: $objNextPage->title;
  428.                     }
  429.                     // Replace the tag
  430.                     switch (strtolower($elements[0]))
  431.                     {
  432.                         case 'link':
  433.                             $arrCache[$strTag] = sprintf('<a href="%s" title="%s"%s%s>%s</a>'$strUrl ?: './'StringUtil::specialcharsAttribute($strTitle), $strClass$strTarget$strName);
  434.                             break;
  435.                         case 'link_open':
  436.                             $arrCache[$strTag] = sprintf('<a href="%s" title="%s"%s%s>'$strUrl ?: './'StringUtil::specialcharsAttribute($strTitle), $strClass$strTarget);
  437.                             break;
  438.                         case 'link_url':
  439.                             $arrCache[$strTag] = $strUrl ?: './';
  440.                             break;
  441.                         case 'link_title':
  442.                             $arrCache[$strTag] = StringUtil::specialcharsAttribute($strTitle);
  443.                             break;
  444.                         case 'link_target':
  445.                             trigger_deprecation('contao/core-bundle''4.4''Using the link_target insert tag has been deprecated and will no longer work in Contao 5.0.');
  446.                             $arrCache[$strTag] = $strTarget ' target=_blank rel=noreferrer&#32;noopener' $strTarget;
  447.                             break;
  448.                         case 'link_name':
  449.                             $arrCache[$strTag] = StringUtil::specialcharsAttribute($strName);
  450.                             break;
  451.                     }
  452.                     break;
  453.                 // Closing link tag
  454.                 case 'link_close':
  455.                 case 'email_close':
  456.                     $arrCache[$strTag] = '</a>';
  457.                     break;
  458.                 // Insert article
  459.                 case 'insert_article':
  460.                     if (($strOutput $this->getArticle($elements[1], falsetrue)) !== false)
  461.                     {
  462.                         $arrCache[$strTag] = ltrim($strOutput);
  463.                     }
  464.                     else
  465.                     {
  466.                         $arrCache[$strTag] = '<p class="error">' sprintf($GLOBALS['TL_LANG']['MSC']['invalidPage'], $elements[1]) . '</p>';
  467.                     }
  468.                     break;
  469.                 // Insert content element
  470.                 case 'insert_content':
  471.                     $arrCache[$strTag] = $this->getContentElement($elements[1]);
  472.                     break;
  473.                 // Insert module
  474.                 case 'insert_module':
  475.                     $arrCache[$strTag] = $this->getFrontendModule($elements[1]);
  476.                     break;
  477.                 // Insert form
  478.                 case 'insert_form':
  479.                     $arrCache[$strTag] = $this->getForm($elements[1]);
  480.                     break;
  481.                 // Article
  482.                 case 'article':
  483.                 case 'article_open':
  484.                 case 'article_url':
  485.                 case 'article_title':
  486.                     if (!(($objArticle ArticleModel::findByIdOrAlias($elements[1])) instanceof ArticleModel) || !(($objPid $objArticle->getRelated('pid')) instanceof PageModel))
  487.                     {
  488.                         break;
  489.                     }
  490.                     /** @var PageModel $objPid */
  491.                     $params '/articles/' . ($objArticle->alias ?: $objArticle->id);
  492.                     $strUrl = \in_array('absolute', \array_slice($elements2), true) || \in_array('absolute'$flagstrue) ? $objPid->getAbsoluteUrl($params) : $objPid->getFrontendUrl($params);
  493.                     // Replace the tag
  494.                     switch (strtolower($elements[0]))
  495.                     {
  496.                         case 'article':
  497.                             $arrCache[$strTag] = sprintf('<a href="%s" title="%s">%s</a>'$strUrlStringUtil::specialcharsAttribute($objArticle->title), $objArticle->title);
  498.                             break;
  499.                         case 'article_open':
  500.                             $arrCache[$strTag] = sprintf('<a href="%s" title="%s">'$strUrlStringUtil::specialcharsAttribute($objArticle->title));
  501.                             break;
  502.                         case 'article_url':
  503.                             $arrCache[$strTag] = $strUrl;
  504.                             break;
  505.                         case 'article_title':
  506.                             $arrCache[$strTag] = StringUtil::specialcharsAttribute($objArticle->title);
  507.                             break;
  508.                     }
  509.                     break;
  510.                 // Article teaser
  511.                 case 'article_teaser':
  512.                     $objTeaser ArticleModel::findByIdOrAlias($elements[1]);
  513.                     if ($objTeaser !== null)
  514.                     {
  515.                         $arrCache[$strTag] = StringUtil::toHtml5($objTeaser->teaser);
  516.                     }
  517.                     break;
  518.                 // Last update
  519.                 case 'last_update':
  520.                     $flags[] = 'attr';
  521.                     $strQuery "SELECT MAX(tstamp) AS tc";
  522.                     $bundles $container->getParameter('kernel.bundles');
  523.                     if (isset($bundles['ContaoNewsBundle']))
  524.                     {
  525.                         $strQuery .= ", (SELECT MAX(tstamp) FROM tl_news) AS tn";
  526.                     }
  527.                     if (isset($bundles['ContaoCalendarBundle']))
  528.                     {
  529.                         $strQuery .= ", (SELECT MAX(tstamp) FROM tl_calendar_events) AS te";
  530.                     }
  531.                     $strQuery .= " FROM tl_content";
  532.                     $objUpdate Database::getInstance()->query($strQuery);
  533.                     if ($objUpdate->numRows)
  534.                     {
  535.                         $arrCache[$strTag] = Date::parse($elements[1] ?: $objPage->datimFormatmax($objUpdate->tc$objUpdate->tn$objUpdate->te));
  536.                     }
  537.                     break;
  538.                 // Version
  539.                 case 'version':
  540.                     $arrCache[$strTag] = VERSION '.' BUILD;
  541.                     break;
  542.                 // Request token
  543.                 case 'request_token':
  544.                     $arrCache[$strTag] = REQUEST_TOKEN;
  545.                     break;
  546.                 // POST data
  547.                 case 'post':
  548.                     $flags[] = 'attr';
  549.                     $arrCache[$strTag] = Input::post($elements[1]);
  550.                     break;
  551.                 // Conditional tags (if, if not)
  552.                 case 'iflng':
  553.                 case 'ifnlng':
  554.                     if (!empty($elements[1]) && $this->languageMatches($elements[1]) === (strtolower($elements[0]) === 'ifnlng'))
  555.                     {
  556.                         // Skip everything until the next tag
  557.                         for (; $_rit<$_cnt$_rit+=2)
  558.                         {
  559.                             // Case insensitive match for iflng/ifnlng optionally followed by "::" or "|"
  560.                             if (=== preg_match('/^' preg_quote($elements[0], '/') . '(?:$|::|\|)/i'$tags[$_rit+3] ?? ''))
  561.                             {
  562.                                 $tags[$_rit+2] = '';
  563.                                 break;
  564.                             }
  565.                         }
  566.                     }
  567.                     // Does not output anything and the cache must not be used
  568.                     unset($arrCache[$strTag]);
  569.                     continue 2;
  570.                 // Environment
  571.                 case 'env':
  572.                     $flags[] = 'urlattr';
  573.                     switch ($elements[1])
  574.                     {
  575.                         case 'host':
  576.                             $arrCache[$strTag] = Idna::decode(Environment::get('host'));
  577.                             break;
  578.                         case 'http_host':
  579.                             $arrCache[$strTag] = Idna::decode(Environment::get('httpHost'));
  580.                             break;
  581.                         case 'url':
  582.                             $arrCache[$strTag] = Idna::decode(Environment::get('url'));
  583.                             break;
  584.                         case 'path':
  585.                             $arrCache[$strTag] = Idna::decode(Environment::get('base'));
  586.                             break;
  587.                         case 'request':
  588.                             $arrCache[$strTag] = Environment::get('indexFreeRequest');
  589.                             break;
  590.                         case 'ip':
  591.                             $arrCache[$strTag] = Environment::get('ip');
  592.                             break;
  593.                         case 'referer':
  594.                             $arrCache[$strTag] = $this->getReferer(true);
  595.                             break;
  596.                         case 'files_url':
  597.                             $arrCache[$strTag] = $container->get('contao.assets.files_context')->getStaticUrl();
  598.                             break;
  599.                         case 'assets_url':
  600.                         case 'plugins_url':
  601.                         case 'script_url':
  602.                             $arrCache[$strTag] = $container->get('contao.assets.assets_context')->getStaticUrl();
  603.                             break;
  604.                         case 'base_url':
  605.                             $arrCache[$strTag] = $container->get('request_stack')->getCurrentRequest()->getBaseUrl();
  606.                             break;
  607.                     }
  608.                     break;
  609.                 // Page
  610.                 case 'page':
  611.                     if (!$objPage->parentPageTitle && $elements[1] == 'parentPageTitle')
  612.                     {
  613.                         $elements[1] = 'parentTitle';
  614.                     }
  615.                     elseif (!$objPage->mainPageTitle && $elements[1] == 'mainPageTitle')
  616.                     {
  617.                         $elements[1] = 'mainTitle';
  618.                     }
  619.                     $responseContext System::getContainer()->get(ResponseContextAccessor::class)->getResponseContext();
  620.                     if ($responseContext && $responseContext->has(HtmlHeadBag::class) && \in_array($elements[1], array('pageTitle''description'), true))
  621.                     {
  622.                         /** @var HtmlHeadBag $htmlHeadBag */
  623.                         $htmlHeadBag $responseContext->get(HtmlHeadBag::class);
  624.                         switch ($elements[1])
  625.                         {
  626.                             case 'pageTitle':
  627.                                 $arrCache[$strTag] = htmlspecialchars($htmlHeadBag->getTitle());
  628.                                 break;
  629.                             case 'description':
  630.                                 $arrCache[$strTag] = htmlspecialchars($htmlHeadBag->getMetaDescription());
  631.                                 break;
  632.                         }
  633.                     }
  634.                     else
  635.                     {
  636.                         // Do not use StringUtil::specialchars() here (see #4687)
  637.                         if (!\in_array($elements[1], array('title''parentTitle''mainTitle''rootTitle''pageTitle''parentPageTitle''mainPageTitle''rootPageTitle'), true))
  638.                         {
  639.                             $flags[] = 'attr';
  640.                         }
  641.                         $arrCache[$strTag] = $objPage->{$elements[1]};
  642.                     }
  643.                     break;
  644.                 // User agent
  645.                 case 'ua':
  646.                     $flags[] = 'attr';
  647.                     $ua Environment::get('agent');
  648.                     if (!empty($elements[1]))
  649.                     {
  650.                         $arrCache[$strTag] = $ua->{$elements[1]};
  651.                     }
  652.                     else
  653.                     {
  654.                         $arrCache[$strTag] = '';
  655.                     }
  656.                     break;
  657.                 // Abbreviations
  658.                 case 'abbr':
  659.                 case 'acronym':
  660.                     if (!empty($elements[1]))
  661.                     {
  662.                         $arrCache[$strTag] = '<abbr title="' StringUtil::specialchars($elements[1]) . '">';
  663.                     }
  664.                     else
  665.                     {
  666.                         $arrCache[$strTag] = '</abbr>';
  667.                     }
  668.                     break;
  669.                 // Images
  670.                 case 'figure':
  671.                     // Expected format: {{figure::<from>[?<key>=<value>,[&<key>=<value>]*]}}
  672.                     list($from$configuration) = $this->parseUrlWithQueryString($elements[1] ?? '');
  673.                     if (null === $from || !== \count($elements) || Validator::isInsecurePath($from) || Path::isAbsolute($from))
  674.                     {
  675.                         $arrCache[$strTag] = '';
  676.                         break;
  677.                     }
  678.                     $size $configuration['size'] ?? null;
  679.                     $template $configuration['template'] ?? '@ContaoCore/Image/Studio/figure.html.twig';
  680.                     unset($configuration['size'], $configuration['template']);
  681.                     // Render the figure
  682.                     $figureRenderer $container->get(FigureRenderer::class);
  683.                     try
  684.                     {
  685.                         $arrCache[$strTag] = $figureRenderer->render($from$size$configuration$template) ?? '';
  686.                     }
  687.                     catch (\Throwable $e)
  688.                     {
  689.                         $arrCache[$strTag] = '';
  690.                     }
  691.                     break;
  692.                 case 'image':
  693.                 case 'picture':
  694.                     $width null;
  695.                     $height null;
  696.                     $alt '';
  697.                     $class '';
  698.                     $rel '';
  699.                     $strFile $elements[1];
  700.                     $mode '';
  701.                     $size null;
  702.                     $strTemplate 'picture_default';
  703.                     // Take arguments
  704.                     if (strpos($elements[1], '?') !== false)
  705.                     {
  706.                         $arrChunks explode('?'urldecode($elements[1]), 2);
  707.                         $strSource StringUtil::decodeEntities($arrChunks[1]);
  708.                         $strSource str_replace('[&]''&'$strSource);
  709.                         $arrParams explode('&'$strSource);
  710.                         foreach ($arrParams as $strParam)
  711.                         {
  712.                             list($key$value) = explode('='$strParam);
  713.                             switch ($key)
  714.                             {
  715.                                 case 'width':
  716.                                     $width = (int) $value;
  717.                                     break;
  718.                                 case 'height':
  719.                                     $height = (int) $value;
  720.                                     break;
  721.                                 case 'alt':
  722.                                     $alt $value;
  723.                                     break;
  724.                                 case 'class':
  725.                                     $class $value;
  726.                                     break;
  727.                                 case 'rel':
  728.                                     $rel $value;
  729.                                     break;
  730.                                 case 'mode':
  731.                                     $mode $value;
  732.                                     break;
  733.                                 case 'size':
  734.                                     $size is_numeric($value) ? (int) $value $value;
  735.                                     break;
  736.                                 case 'template':
  737.                                     $strTemplate preg_replace('/[^a-z0-9_]/i'''$value);
  738.                                     break;
  739.                             }
  740.                         }
  741.                         $strFile $arrChunks[0];
  742.                     }
  743.                     if (Validator::isUuid($strFile))
  744.                     {
  745.                         // Handle UUIDs
  746.                         $objFile FilesModel::findByUuid($strFile);
  747.                         if ($objFile === null)
  748.                         {
  749.                             $arrCache[$strTag] = '';
  750.                             break;
  751.                         }
  752.                         $strFile $objFile->path;
  753.                     }
  754.                     elseif (is_numeric($strFile))
  755.                     {
  756.                         // Handle numeric IDs (see #4805)
  757.                         $objFile FilesModel::findByPk($strFile);
  758.                         if ($objFile === null)
  759.                         {
  760.                             $arrCache[$strTag] = '';
  761.                             break;
  762.                         }
  763.                         $strFile $objFile->path;
  764.                     }
  765.                     elseif (Validator::isInsecurePath($strFile))
  766.                     {
  767.                         throw new \RuntimeException('Invalid path ' $strFile);
  768.                     }
  769.                     $maxImageWidth Config::get('maxImageWidth');
  770.                     // Check the maximum image width
  771.                     if ($maxImageWidth && $width $maxImageWidth)
  772.                     {
  773.                         trigger_deprecation('contao/core-bundle''4.0''Using a maximum front end width has been deprecated and will no longer work in Contao 5.0. Remove the "maxImageWidth" configuration and use responsive images instead.');
  774.                         $width $maxImageWidth;
  775.                         $height null;
  776.                     }
  777.                     // Use the alternative text from the image metadata if none is given
  778.                     if (!$alt && ($objFile FilesModel::findByPath($strFile)))
  779.                     {
  780.                         $arrMeta Frontend::getMetaData($objFile->meta$objPage->language);
  781.                         if (isset($arrMeta['alt']))
  782.                         {
  783.                             $alt $arrMeta['alt'];
  784.                         }
  785.                     }
  786.                     // Generate the thumbnail image
  787.                     try
  788.                     {
  789.                         // Image
  790.                         if (strtolower($elements[0]) == 'image')
  791.                         {
  792.                             $dimensions '';
  793.                             $src $container->get('contao.image.image_factory')->create($container->getParameter('kernel.project_dir') . '/' rawurldecode($strFile), array($width$height$mode))->getUrl($container->getParameter('kernel.project_dir'));
  794.                             $objFile = new File(rawurldecode($src));
  795.                             // Add the image dimensions
  796.                             if (isset($objFile->imageSize[0], $objFile->imageSize[1]))
  797.                             {
  798.                                 $dimensions ' width="' $objFile->imageSize[0] . '" height="' $objFile->imageSize[1] . '"';
  799.                             }
  800.                             $arrCache[$strTag] = '<img src="' StringUtil::specialcharsUrl(Controller::addFilesUrlTo($src)) . '" ' $dimensions ' alt="' StringUtil::specialcharsAttribute($alt) . '"' . ($class ' class="' StringUtil::specialcharsAttribute($class) . '"' '') . '>';
  801.                         }
  802.                         // Picture
  803.                         else
  804.                         {
  805.                             $staticUrl $container->get('contao.assets.files_context')->getStaticUrl();
  806.                             $picture $container->get('contao.image.picture_factory')->create($container->getParameter('kernel.project_dir') . '/' $strFile$size);
  807.                             $picture = array
  808.                             (
  809.                                 'img' => $picture->getImg($container->getParameter('kernel.project_dir'), $staticUrl),
  810.                                 'sources' => $picture->getSources($container->getParameter('kernel.project_dir'), $staticUrl)
  811.                             );
  812.                             $picture['alt'] = StringUtil::specialcharsAttribute($alt);
  813.                             $picture['class'] = StringUtil::specialcharsAttribute($class);
  814.                             $pictureTemplate = new FrontendTemplate($strTemplate);
  815.                             $pictureTemplate->setData($picture);
  816.                             $arrCache[$strTag] = $pictureTemplate->parse();
  817.                         }
  818.                         // Add a lightbox link
  819.                         if ($rel)
  820.                         {
  821.                             $arrCache[$strTag] = '<a href="' StringUtil::specialcharsUrl(Controller::addFilesUrlTo($strFile)) . '"' . ($alt ' title="' StringUtil::specialcharsAttribute($alt) . '"' '') . ' data-lightbox="' StringUtil::specialcharsAttribute($rel) . '">' $arrCache[$strTag] . '</a>';
  822.                         }
  823.                     }
  824.                     catch (\Exception $e)
  825.                     {
  826.                         $arrCache[$strTag] = '';
  827.                     }
  828.                     break;
  829.                 // Files (UUID or template path)
  830.                 case 'file':
  831.                     if (Validator::isUuid($elements[1]))
  832.                     {
  833.                         $objFile FilesModel::findByUuid($elements[1]);
  834.                         if ($objFile !== null)
  835.                         {
  836.                             $arrCache[$strTag] = System::urlEncode($objFile->path);
  837.                             break;
  838.                         }
  839.                     }
  840.                     $arrGet $_GET;
  841.                     Input::resetCache();
  842.                     $strFile $elements[1];
  843.                     // Take arguments and add them to the $_GET array
  844.                     if (strpos($elements[1], '?') !== false)
  845.                     {
  846.                         $arrChunks explode('?'urldecode($elements[1]));
  847.                         $strSource StringUtil::decodeEntities($arrChunks[1]);
  848.                         $strSource str_replace('[&]''&'$strSource);
  849.                         $arrParams explode('&'$strSource);
  850.                         foreach ($arrParams as $strParam)
  851.                         {
  852.                             $arrParam explode('='$strParam);
  853.                             $_GET[$arrParam[0]] = $arrParam[1];
  854.                         }
  855.                         $strFile $arrChunks[0];
  856.                     }
  857.                     // Check the path
  858.                     if (Validator::isInsecurePath($strFile))
  859.                     {
  860.                         throw new \RuntimeException('Invalid path ' $strFile);
  861.                     }
  862.                     // Include .php, .tpl, .xhtml and .html5 files
  863.                     if (preg_match('/\.(php|tpl|xhtml|html5)$/'$strFile) && file_exists($container->getParameter('kernel.project_dir') . '/templates/' $strFile))
  864.                     {
  865.                         ob_start();
  866.                         try
  867.                         {
  868.                             include $container->getParameter('kernel.project_dir') . '/templates/' $strFile;
  869.                             $arrCache[$strTag] = ob_get_contents();
  870.                         }
  871.                         finally
  872.                         {
  873.                             ob_end_clean();
  874.                         }
  875.                     }
  876.                     $_GET $arrGet;
  877.                     Input::resetCache();
  878.                     break;
  879.                 // HOOK: pass unknown tags to callback functions
  880.                 default:
  881.                     if (isset($GLOBALS['TL_HOOKS']['replaceInsertTags']) && \is_array($GLOBALS['TL_HOOKS']['replaceInsertTags']))
  882.                     {
  883.                         foreach ($GLOBALS['TL_HOOKS']['replaceInsertTags'] as $callback)
  884.                         {
  885.                             $this->import($callback[0]);
  886.                             $varValue $this->{$callback[0]}->{$callback[1]}($tag$blnCache$arrCache[$strTag], $flags$tags$arrCache$_rit$_cnt); // see #6672
  887.                             // Replace the tag and stop the loop
  888.                             if ($varValue !== false)
  889.                             {
  890.                                 $arrCache[$strTag] = $varValue;
  891.                                 break 2;
  892.                             }
  893.                         }
  894.                     }
  895.                     $this->log('Unknown insert tag {{' $strTag '}} on page ' Environment::get('uri'), __METHOD__TL_ERROR);
  896.                     break;
  897.             }
  898.             // Handle the flags
  899.             if (!empty($flags))
  900.             {
  901.                 foreach ($flags as $flag)
  902.                 {
  903.                     switch ($flag)
  904.                     {
  905.                         case 'addslashes':
  906.                         case 'standardize':
  907.                         case 'ampersand':
  908.                         case 'specialchars':
  909.                         case 'strtolower':
  910.                         case 'utf8_strtolower':
  911.                         case 'strtoupper':
  912.                         case 'utf8_strtoupper':
  913.                         case 'ucfirst':
  914.                         case 'lcfirst':
  915.                         case 'ucwords':
  916.                         case 'trim':
  917.                         case 'rtrim':
  918.                         case 'ltrim':
  919.                         case 'utf8_romanize':
  920.                         case 'urlencode':
  921.                         case 'rawurlencode':
  922.                             $arrCache[$strTag] = $flag($arrCache[$strTag]);
  923.                             break;
  924.                         case 'attr':
  925.                             $arrCache[$strTag] = StringUtil::specialcharsAttribute($arrCache[$strTag]);
  926.                             break;
  927.                         case 'urlattr':
  928.                             $arrCache[$strTag] = StringUtil::specialcharsUrl($arrCache[$strTag]);
  929.                             break;
  930.                         case 'nl2br_pre':
  931.                             trigger_deprecation('contao/core-bundle''4.0''Using nl2br_pre() has been deprecated and will no longer work in Contao 5.0.');
  932.                             // no break
  933.                         case 'nl2br':
  934.                             $arrCache[$strTag] = preg_replace('/\r?\n/''<br>'$arrCache[$strTag]);
  935.                             break;
  936.                         case 'encodeEmail':
  937.                             $arrCache[$strTag] = StringUtil::$flag($arrCache[$strTag]);
  938.                             break;
  939.                         case 'number_format':
  940.                             $arrCache[$strTag] = System::getFormattedNumber($arrCache[$strTag], 0);
  941.                             break;
  942.                         case 'currency_format':
  943.                             $arrCache[$strTag] = System::getFormattedNumber($arrCache[$strTag]);
  944.                             break;
  945.                         case 'readable_size':
  946.                             $arrCache[$strTag] = System::getReadableSize($arrCache[$strTag]);
  947.                             break;
  948.                         case 'flatten':
  949.                             if (!\is_array($arrCache[$strTag]))
  950.                             {
  951.                                 break;
  952.                             }
  953.                             $it = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($arrCache[$strTag]));
  954.                             $result = array();
  955.                             foreach ($it as $leafValue)
  956.                             {
  957.                                 $keys = array();
  958.                                 foreach (range(0$it->getDepth()) as $depth)
  959.                                 {
  960.                                     $keys[] = $it->getSubIterator($depth)->key();
  961.                                 }
  962.                                 $result[] = implode('.'$keys) . ': ' $leafValue;
  963.                             }
  964.                             $arrCache[$strTag] = implode(', '$result);
  965.                             break;
  966.                         case 'absolute':
  967.                             trigger_deprecation('contao/core-bundle''4.12''The insert tag flag "|absolute" has been deprecated and will no longer work in Contao 5.0. use "::absolute" instead.');
  968.                             // ignore
  969.                             break;
  970.                         case 'refresh':
  971.                         case 'uncached':
  972.                             // ignore
  973.                             break;
  974.                         // HOOK: pass unknown flags to callback functions
  975.                         default:
  976.                             if (isset($GLOBALS['TL_HOOKS']['insertTagFlags']) && \is_array($GLOBALS['TL_HOOKS']['insertTagFlags']))
  977.                             {
  978.                                 foreach ($GLOBALS['TL_HOOKS']['insertTagFlags'] as $callback)
  979.                                 {
  980.                                     $this->import($callback[0]);
  981.                                     $varValue $this->{$callback[0]}->{$callback[1]}($flag$tag$arrCache[$strTag], $flags$blnCache$tags$arrCache$_rit$_cnt); // see #5806
  982.                                     // Replace the tag and stop the loop
  983.                                     if ($varValue !== false)
  984.                                     {
  985.                                         $arrCache[$strTag] = $varValue;
  986.                                         break 2;
  987.                                     }
  988.                                 }
  989.                             }
  990.                             $this->log('Unknown insert tag flag "' $flag '" in {{' $strTag '}} on page ' Environment::get('uri'), __METHOD__TL_ERROR);
  991.                             break;
  992.                     }
  993.                 }
  994.             }
  995.             $strBuffer .= $arrCache[$strTag] ?? '';
  996.         }
  997.         return StringUtil::restoreBasicEntities($strBuffer);
  998.     }
  999.     /**
  1000.      * @return array<string|null, array>
  1001.      */
  1002.     private function parseUrlWithQueryString(string $url): array
  1003.     {
  1004.         // Restore [&]
  1005.         $url str_replace('[&]''&'$url);
  1006.         $base parse_url($urlPHP_URL_PATH) ?: null;
  1007.         $query parse_url($urlPHP_URL_QUERY) ?: '';
  1008.         parse_str($query$attributes);
  1009.         // Cast and encode values
  1010.         array_walk_recursive($attributes, static function (&$value)
  1011.         {
  1012.             if (is_numeric($value))
  1013.             {
  1014.                 $value = (int) $value;
  1015.                 return;
  1016.             }
  1017.             $value StringUtil::specialcharsAttribute($value);
  1018.         });
  1019.         return array($base$attributes);
  1020.     }
  1021.     /**
  1022.      * Add the specialchars flag to all insert tags used in HTML attributes
  1023.      *
  1024.      * @param string $html
  1025.      *
  1026.      * @return string The html with the encoded insert tags
  1027.      */
  1028.     private function encodeHtmlAttributes($html)
  1029.     {
  1030.         // Regular expression to match tags according to https://html.spec.whatwg.org/#tag-open-state
  1031.         $tagRegEx '('
  1032.             '<'                         // Tag start
  1033.             '/?'                        // Optional slash for closing element
  1034.             '([a-z][^\s/>]*+)'          // Tag name
  1035.             '(?:'                       // Attribute
  1036.                 '[\s/]*+'               // Optional white space including slash
  1037.                 '[^>\s/][^>\s/=]*+'     // Attribute name
  1038.                 '[\s]*+'                // Optional white space
  1039.                 '(?:='                  // Assignment
  1040.                     '[\s]*+'            // Optional white space
  1041.                     '(?:'               // Value
  1042.                         '"[^"]*"'       // Double quoted value
  1043.                         '|\'[^\']*\''   // Or single quoted value
  1044.                         '|[^>][^\s>]*+' // Or unquoted value
  1045.                     ')?+'               // Value is optional
  1046.                 ')?+'                   // Assignment is optional
  1047.             ')*+'                       // Attributes may occur zero or more times
  1048.             '[\s/]*+'                   // Optional white space including slash
  1049.             '>?'                        // Tag end (optional if EOF)
  1050.             '|<!--'                     // Or comment
  1051.             '|<!'                       // Or bogus ! comment
  1052.             '|<\?'                      // Or bogus ? comment
  1053.             '|</(?![a-z])'              // Or bogus / comment
  1054.         ')i';
  1055.         $htmlResult '';
  1056.         $offset 0;
  1057.         while (preg_match($tagRegEx$html$matchesPREG_OFFSET_CAPTURE$offset))
  1058.         {
  1059.             $htmlResult .= substr($html$offset$matches[0][1] - $offset);
  1060.             // Skip comments
  1061.             if ($matches[0][0] === '<!--' || $matches[0][0] === '<!' || $matches[0][0] === '</' || $matches[0][0] === '<?')
  1062.             {
  1063.                 $commentCloseString $matches[0][0] === '<!--' '-->' '>';
  1064.                 $commentClosePos strpos($html$commentCloseString$offset);
  1065.                 $offset $commentClosePos $commentClosePos + \strlen($commentCloseString) : \strlen($html);
  1066.                 // Encode insert tags in comments
  1067.                 $htmlResult .= str_replace(array('{{''}}'), array('[{]''[}]'), substr($html$matches[0][1], $offset $matches[0][1]));
  1068.                 continue;
  1069.             }
  1070.             $tag $matches[0][0];
  1071.             // Encode insert tags
  1072.             $tagPrefix substr($tag0$matches[1][1] - $matches[0][1] + \strlen($matches[1][0]));
  1073.             $tag $tagPrefix $this->fixUnclosedTagsAndUrlAttributes(substr($tag, \strlen($tagPrefix)));
  1074.             $tag preg_replace('/(?:\|attr)?}}/''|attr}}'$tag);
  1075.             $tag str_replace('|urlattr|attr}}''|urlattr}}'$tag);
  1076.             $offset $matches[0][1] + \strlen($matches[0][0]);
  1077.             $htmlResult .= $tag;
  1078.             // Skip RCDATA and RAWTEXT elements https://html.spec.whatwg.org/#rcdata-state
  1079.             if (
  1080.                 \in_array(strtolower($matches[1][0]), array('script''title''textarea''style''xmp''iframe''noembed''noframes''noscript'))
  1081.                 && preg_match('(</' preg_quote($matches[1][0]) . '[\s/>])i'$html$endTagMatchesPREG_OFFSET_CAPTURE$offset)
  1082.             ) {
  1083.                 $offset $endTagMatches[0][1] + \strlen($endTagMatches[0][0]);
  1084.                 $htmlResult .= substr($html$matches[0][1] + \strlen($matches[0][0]), $offset $matches[0][1] - \strlen($matches[0][0]));
  1085.             }
  1086.         }
  1087.         $htmlResult .= substr($html$offset);
  1088.         return $htmlResult;
  1089.     }
  1090.     /**
  1091.      * Detect strip and encode unclosed insert tags and add the urlattr flag to
  1092.      * all insert tags used in URL attributes
  1093.      *
  1094.      * @param string $attributes
  1095.      *
  1096.      * @return string The attributes html with the encoded insert tags
  1097.      */
  1098.     private function fixUnclosedTagsAndUrlAttributes($attributes)
  1099.     {
  1100.         $attrRegEx '('
  1101.             '[\s/]*+'               // Optional white space including slash
  1102.             '([^>\s/][^>\s/=]*+)'   // Attribute name
  1103.             '[\s]*+'                // Optional white space
  1104.             '(?:='                  // Assignment
  1105.                 '[\s]*+'            // Optional white space
  1106.                 '(?:'               // Value
  1107.                     '"[^"]*"'       // Double quoted value
  1108.                     '|\'[^\']*\''   // Or single quoted value
  1109.                     '|[^>][^\s>]*+' // Or unquoted value
  1110.                 ')?+'               // Value is optional
  1111.             ')?+'                   // Assignment is optional
  1112.         ')i';
  1113.         $attributesResult '';
  1114.         $offset 0;
  1115.         while (preg_match($attrRegEx$attributes$matchesPREG_OFFSET_CAPTURE$offset))
  1116.         {
  1117.             $attributesResult .= substr($attributes$offset$matches[0][1] - $offset);
  1118.             $offset $matches[0][1] + \strlen($matches[0][0]);
  1119.             // Strip unclosed iflng tags
  1120.             $intLastIflng strripos($matches[0][0], '{{iflng');
  1121.             if (
  1122.                 $intLastIflng !== strripos($matches[0][0], '{{iflng}}')
  1123.                 && $intLastIflng !== strripos($matches[0][0], '{{iflng|urlattr}}')
  1124.                 && $intLastIflng !== strripos($matches[0][0], '{{iflng|attr}}')
  1125.             ) {
  1126.                 $matches[0][0] = StringUtil::stripInsertTags($matches[0][0]);
  1127.             }
  1128.             // Strip unclosed ifnlng tags
  1129.             $intLastIfnlng strripos($matches[0][0], '{{ifnlng');
  1130.             if (
  1131.                 $intLastIfnlng !== strripos($matches[0][0], '{{ifnlng}}')
  1132.                 && $intLastIfnlng !== strripos($matches[0][0], '{{ifnlng|urlattr}}')
  1133.                 && $intLastIfnlng !== strripos($matches[0][0], '{{ifnlng|attr}}')
  1134.             ) {
  1135.                 $matches[0][0] = StringUtil::stripInsertTags($matches[0][0]);
  1136.             }
  1137.             // Strip unclosed insert tags
  1138.             $intLastOpen strrpos($matches[0][0], '{{');
  1139.             $intLastClose strrpos($matches[0][0], '}}');
  1140.             if ($intLastOpen !== false && ($intLastClose === false || $intLastClose $intLastOpen))
  1141.             {
  1142.                 $matches[0][0] = StringUtil::stripInsertTags($matches[0][0]);
  1143.                 $matches[0][0] = str_replace(array('{{''}}'), array('[{]''[}]'), $matches[0][0]);
  1144.             }
  1145.             elseif ($intLastOpen === false && $intLastClose !== false)
  1146.             {
  1147.                 // Improve compatibility with JSON in attributes
  1148.                 $matches[0][0] = str_replace('}}''&#125;&#125;'$matches[0][0]);
  1149.             }
  1150.             // Add the urlattr insert tags flag in URL attributes
  1151.             if (\in_array(strtolower($matches[1][0]), array('src''srcset''href''action''formaction''codebase''cite''background''longdesc''profile''usemap''classid''data''icon''manifest''poster''archive'), true))
  1152.             {
  1153.                 $matches[0][0] = preg_replace('/(?:\|(?:url)?attr)?}}/''|urlattr}}'$matches[0][0]);
  1154.                 // Backwards compatibility
  1155.                 if (trim($matches[0][0]) === 'href="{{link_url::back|urlattr}}"')
  1156.                 {
  1157.                     $matches[0][0] = str_replace('{{link_url::back|urlattr}}''{{link_url::back}}'$matches[0][0]);
  1158.                 }
  1159.             }
  1160.             $attributesResult .= $matches[0][0];
  1161.         }
  1162.         $attributesResult .= substr($attributes$offset);
  1163.         return $attributesResult;
  1164.     }
  1165.     /**
  1166.      * Check if the language matches
  1167.      *
  1168.      * @param string $language
  1169.      *
  1170.      * @return boolean
  1171.      */
  1172.     private function languageMatches($language)
  1173.     {
  1174.         $pageLanguage LocaleUtil::formatAsLocale($GLOBALS['objPage']->language);
  1175.         foreach (StringUtil::trimsplit(','$language) as $lang)
  1176.         {
  1177.             if ($pageLanguage === LocaleUtil::formatAsLocale($lang))
  1178.             {
  1179.                 return true;
  1180.             }
  1181.             if (substr($lang, -1) === '*' && === strncmp($pageLanguage$lang, \strlen($lang) - 1))
  1182.             {
  1183.                 return true;
  1184.             }
  1185.         }
  1186.         return false;
  1187.     }
  1188. }
  1189. class_alias(InsertTags::class, 'InsertTags');