local/modules/TheliaSmarty/Template/SmartyParser.php line 534

  1. <?php
  2. /*
  3.  * This file is part of the Thelia package.
  4.  * http://www.thelia.net
  5.  *
  6.  * (c) OpenStudio <info@thelia.net>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace TheliaSmarty\Template;
  12. use Imagine\Exception\InvalidArgumentException;
  13. use Smarty;
  14. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\HttpFoundation\RequestStack;
  17. use Thelia\Core\HttpFoundation\Session\Session;
  18. use Thelia\Core\Template\Exception\ResourceNotFoundException;
  19. use Thelia\Core\Template\ParserContext;
  20. use Thelia\Core\Template\ParserInterface;
  21. use Thelia\Core\Template\TemplateDefinition;
  22. use Thelia\Core\Template\TemplateHelperInterface;
  23. use Thelia\Core\Translation\Translator;
  24. use Thelia\Log\Tlog;
  25. use Thelia\Model\ConfigQuery;
  26. use Thelia\Model\Lang;
  27. /**
  28.  * @author Franck Allimant <franck@cqfdev.fr>
  29.  * @author Etienne Roudeix <eroudeix@openstudio.fr>
  30.  */
  31. class SmartyParser extends \Smarty implements ParserInterface
  32. {
  33.     public $plugins = [];
  34.     /** @var EventDispatcherInterface */
  35.     protected $dispatcher;
  36.     /** @var ParserContext */
  37.     protected $parserContext;
  38.     /** @var TemplateHelperInterface */
  39.     protected $templateHelper;
  40.     /** @var RequestStack */
  41.     protected $requestStack;
  42.     protected $backOfficeTemplateDirectories = [];
  43.     protected $frontOfficeTemplateDirectories = [];
  44.     protected $templateDirectories = [];
  45.     /** @var TemplateDefinition */
  46.     protected $templateDefinition;
  47.     /** @var bool if true, resources will also be searched in the default template */
  48.     protected $fallbackToDefaultTemplate false;
  49.     /** @var int */
  50.     protected $status 200;
  51.     /** @var string */
  52.     protected $env;
  53.     /** @var bool */
  54.     protected $debug;
  55.     /** @var array The template stack */
  56.     protected $tplStack = [];
  57.     /** @var bool */
  58.     protected $useMethodCallWrapper false;
  59.     /**
  60.      * @param string $kernelEnvironment
  61.      * @param bool   $kernelDebug
  62.      *
  63.      * @throws \SmartyException
  64.      */
  65.     public function __construct(
  66.         RequestStack $requestStack,
  67.         EventDispatcherInterface $dispatcher,
  68.         ParserContext $parserContext,
  69.         TemplateHelperInterface $templateHelper,
  70.         $kernelEnvironment 'prod',
  71.         $kernelDebug false
  72.     ) {
  73.         parent::__construct();
  74.         $this->requestStack $requestStack;
  75.         $this->dispatcher $dispatcher;
  76.         $this->parserContext $parserContext;
  77.         $this->templateHelper $templateHelper;
  78.         $this->env $kernelEnvironment;
  79.         $this->debug $kernelDebug;
  80.         // Use method call compatibility wrapper ?
  81.         $this->useMethodCallWrapper version_compare(self::SMARTY_VERSION'3.1.33''>=');
  82.         // Configure basic Smarty parameters
  83.         $compile_dir THELIA_CACHE_DIR.DS.$kernelEnvironment.DS.'smarty'.DS.'compile';
  84.         if (!is_dir($compile_dir)) {
  85.             @mkdir($compile_dir0777true);
  86.         }
  87.         $cache_dir THELIA_CACHE_DIR.DS.$kernelEnvironment.DS.'smarty'.DS.'cache';
  88.         if (!is_dir($cache_dir)) {
  89.             @mkdir($cache_dir0777true);
  90.         }
  91.         $this->setCompileDir($compile_dir);
  92.         $this->setCacheDir($cache_dir);
  93.         // Prevent smarty ErrorException: Notice: Undefined index bla bla bla...
  94.         $this->error_reporting \E_ALL \E_NOTICE;
  95.         // The default HTTP status
  96.         $this->status 200;
  97.         $this->registerFilter('output', [$this'trimWhitespaces']);
  98.         $this->registerFilter('variable', [__CLASS__'theliaEscape']);
  99.     }
  100.     /**
  101.      * Return the current request or null if no request exists.
  102.      *
  103.      * @return Request|null
  104.      */
  105.     public function getRequest()
  106.     {
  107.         return $this->requestStack->getCurrentRequest();
  108.     }
  109.     /**
  110.      * Trim whitespaces from the HTML output, preserving required ones in pre, textarea, javascript.
  111.      * This methois uses 3 levels of trimming :.
  112.      *
  113.      *    - 0 : whitespaces are not trimmed, code remains as is.
  114.      *    - 1 : only blank lines are trimmed, code remains indented and human-readable (the default)
  115.      *    - 2 or more : all unnecessary whitespace are removed. Code is very hard to read.
  116.      *
  117.      * The trim level is defined by the configuration variable html_output_trim_level
  118.      *
  119.      * @param string $source the HTML source
  120.      *
  121.      * @return string
  122.      */
  123.     public function trimWhitespaces($source/* @noinspection PhpUnusedParameterInspection */ \Smarty_Internal_Template $template)
  124.     {
  125.         $compressionMode ConfigQuery::read('html_output_trim_level'1);
  126.         if ($compressionMode == 0) {
  127.             return $source;
  128.         }
  129.         $store = [];
  130.         $_store 0;
  131.         $_offset 0;
  132.         // Unify Line-Breaks to \n
  133.         $source preg_replace("/\015\012|\015|\012/""\n"$source);
  134.         // capture Internet Explorer Conditional Comments
  135.         if ($compressionMode == 1) {
  136.             $expressions = [
  137.                 // remove spaces between attributes (but not in attribute values!)
  138.                 '#(([a-z0-9]\s*=\s*(["\'])[^\3]*?\3)|<[a-z0-9_]+)\s+([a-z/>])#is' => '\1 \4',
  139.                 '/(^[\n]*|[\n]+)[\s\t]*[\n]+/' => "\n",
  140.             ];
  141.         } elseif ($compressionMode >= 2) {
  142.             if (preg_match_all('#<!--\[[^\]]+\]>.*?<!\[[^\]]+\]-->#is'$source$matches\PREG_OFFSET_CAPTURE \PREG_SET_ORDER)) {
  143.                 foreach ($matches as $match) {
  144.                     $store[] = $match[0][0];
  145.                     $_length \strlen($match[0][0]);
  146.                     $replace '@!@SMARTY:'.$_store.':SMARTY@!@';
  147.                     $source substr_replace($source$replace$match[0][1] - $_offset$_length);
  148.                     $_offset += $_length \strlen($replace);
  149.                     ++$_store;
  150.                 }
  151.             }
  152.             // Strip all HTML-Comments
  153.             // yes, even the ones in <script> - see http://stackoverflow.com/a/808850/515124
  154.             $source preg_replace('#<!--.*?-->#ms'''$source);
  155.             $expressions = [
  156.                 // replace multiple spaces between tags by a single space
  157.                 // can't remove them entirely, becaue that might break poorly implemented CSS display:inline-block elements
  158.                 '#(:SMARTY@!@|>)\s+(?=@!@SMARTY:|<)#s' => '\1 \2',
  159.                 // remove spaces between attributes (but not in attribute values!)
  160.                 '#(([a-z0-9]\s*=\s*(["\'])[^\3]*?\3)|<[a-z0-9_]+)\s+([a-z/>])#is' => '\1 \4',
  161.                 // note: for some very weird reason trim() seems to remove spaces inside attributes.
  162.                 // maybe a \0 byte or something is interfering?
  163.                 '#^\s+<#Ss' => '<',
  164.                 '#>\s+$#Ss' => '>',
  165.             ];
  166.         } else {
  167.             $expressions = [];
  168.         }
  169.         // capture html elements not to be messed with
  170.         $_offset 0;
  171.         if (preg_match_all('#<(script|pre|textarea)[^>]*>.*?</\\1>#is'$source$matches\PREG_OFFSET_CAPTURE \PREG_SET_ORDER)) {
  172.             foreach ($matches as $match) {
  173.                 $store[] = $match[0][0];
  174.                 $_length \strlen($match[0][0]);
  175.                 $replace '@!@SMARTY:'.$_store.':SMARTY@!@';
  176.                 $source substr_replace($source$replace$match[0][1] - $_offset$_length);
  177.                 $_offset += $_length \strlen($replace);
  178.                 ++$_store;
  179.             }
  180.         }
  181.         // Protect output against a potential regex execution error (e.g., PREG_BACKTRACK_LIMIT_ERROR)
  182.         if (null !== $tmp preg_replace(array_keys($expressions), array_values($expressions), $source)) {
  183.             $source $tmp;
  184.             unset($tmp);
  185.         } else {
  186.             Tlog::getInstance()->error('Failed to trim whitespaces from parser output: '.preg_last_error());
  187.         }
  188.         // capture html elements not to be messed with
  189.         $_offset 0;
  190.         if (preg_match_all('#@!@SMARTY:([0-9]+):SMARTY@!@#is'$source$matches\PREG_OFFSET_CAPTURE \PREG_SET_ORDER)) {
  191.             foreach ($matches as $match) {
  192.                 $store[] = $match[0][0];
  193.                 $_length \strlen($match[0][0]);
  194.                 $replace array_shift($store);
  195.                 $source substr_replace($source$replace$match[0][1] + $_offset$_length);
  196.                 $_offset += \strlen($replace) - $_length;
  197.                 ++$_store;
  198.             }
  199.         }
  200.         return $source;
  201.     }
  202.     /**
  203.      * Add a template directory to the current template list.
  204.      *
  205.      * @param int    $templateType      the template type (a TemplateDefinition type constant)
  206.      * @param string $templateName      the template name
  207.      * @param string $templateDirectory path to the template directory
  208.      * @param string $key               ???
  209.      * @param bool   $addAtBeginning    if true, the template definition should be added at the beginning of the template directory list
  210.      */
  211.     public function addTemplateDirectory($templateType$templateName$templateDirectory$key$addAtBeginning false): void
  212.     {
  213.         Tlog::getInstance()->addDebug("Adding template directory $templateDirectory, type:$templateType name:$templateName, key: $key");
  214.         if (true === $addAtBeginning && isset($this->templateDirectories[$templateType][$templateName])) {
  215.             // When using array_merge, the key was set to 0. Use + instead.
  216.             $this->templateDirectories[$templateType][$templateName] =
  217.                 [$key => $templateDirectory] + $this->templateDirectories[$templateType][$templateName]
  218.             ;
  219.         } else {
  220.             $this->templateDirectories[$templateType][$templateName][$key] = $templateDirectory;
  221.         }
  222.     }
  223.     /**
  224.      * Return the registered template directories for a given template type.
  225.      *
  226.      * @param int $templateType
  227.      *
  228.      * @throws InvalidArgumentException
  229.      *
  230.      * @return mixed:
  231.      */
  232.     public function getTemplateDirectories($templateType)
  233.     {
  234.         if (!isset($this->templateDirectories[$templateType])) {
  235.             throw new InvalidArgumentException('Failed to get template type %'$templateType);
  236.         }
  237.         return $this->templateDirectories[$templateType];
  238.     }
  239.     public static function theliaEscape($content/* @noinspection PhpUnusedParameterInspection */ $smarty)
  240.     {
  241.         if (\is_scalar($content)) {
  242.             return htmlspecialchars($content\ENT_QUOTES\Smarty::$_CHARSET);
  243.         }
  244.         return $content;
  245.     }
  246.     /**
  247.      * Set a new template definition, and save the current one.
  248.      *
  249.      * @param bool $fallbackToDefaultTemplate if true, resources will be also searched in the "default" template
  250.      */
  251.     public function pushTemplateDefinition(TemplateDefinition $templateDefinition$fallbackToDefaultTemplate false): void
  252.     {
  253.         if (null !== $this->templateDefinition) {
  254.             $this->tplStack[] = [$this->templateDefinition$this->fallbackToDefaultTemplate];
  255.         }
  256.         $this->setTemplateDefinition($templateDefinition$fallbackToDefaultTemplate);
  257.     }
  258.     /**
  259.      * Restore the previous stored template definition, if one exists.
  260.      */
  261.     public function popTemplateDefinition(): void
  262.     {
  263.         if (\count($this->tplStack) > 0) {
  264.             [$templateDefinition$fallbackToDefaultTemplate] = array_pop($this->tplStack);
  265.             $this->setTemplateDefinition($templateDefinition$fallbackToDefaultTemplate);
  266.         }
  267.     }
  268.     /**
  269.      * Configure the parser to use the template defined by $templateDefinition.
  270.      *
  271.      * @param bool $fallbackToDefaultTemplate if true, resources will be also searched in the "default" template
  272.      */
  273.     public function setTemplateDefinition(TemplateDefinition $templateDefinition$fallbackToDefaultTemplate false): void
  274.     {
  275.         $this->templateDefinition $templateDefinition;
  276.         $this->fallbackToDefaultTemplate $fallbackToDefaultTemplate;
  277.         // Clear the current Smarty template path list
  278.         $this->setTemplateDir([]);
  279.         // -------------------------------------------------------------------------------------------------------------
  280.         // Add current template and its parent to the registered template list
  281.         // using "*template-assets" keys.
  282.         // -------------------------------------------------------------------------------------------------------------
  283.         $templateList = ['' => $templateDefinition] + $templateDefinition->getParentList();
  284.         /** @var TemplateDefinition $template */
  285.         foreach (array_reverse($templateList) as $template) {
  286.             // Add template directories  in the current template, in order to get assets
  287.             $this->addTemplateDirectory(
  288.                 $templateDefinition->getType(),
  289.                 $template->getName(), // $templateDefinition->getName(), // We add the template definition in the main template directory
  290.                 $template->getAbsolutePath(),
  291.                 self::TEMPLATE_ASSETS_KEY// $templateKey,
  292.                 true
  293.             );
  294.         }
  295.         // -------------------------------------------------------------------------------------------------------------
  296.         // Add template and its parent pathes to the Smarty template path list
  297.         // using "*template-assets" keys.
  298.         // -------------------------------------------------------------------------------------------------------------
  299.         /**
  300.          * @var string             $keyPrefix
  301.          * @var TemplateDefinition $template
  302.          */
  303.         foreach ($templateList as $keyPrefix => $template) {
  304.             $templateKey $keyPrefix.self::TEMPLATE_ASSETS_KEY;
  305.             // Add the template directory to the Smarty search path
  306.             $this->addTemplateDir($template->getAbsolutePath(), $templateKey);
  307.             // Also add the configuration directory
  308.             $this->addConfigDir(
  309.                 $template->getAbsolutePath().DS.'configs',
  310.                 $templateKey
  311.             );
  312.         }
  313.         // -------------------------------------------------------------------------------------------------------------
  314.         // Add all modules template directories foreach of the template list to the Smarty search path.
  315.         // -------------------------------------------------------------------------------------------------------------
  316.         $type $templateDefinition->getType();
  317.         foreach ($templateList as $keyPrefix => $template) {
  318.             if (isset($this->templateDirectories[$type][$template->getName()])) {
  319.                 foreach ($this->templateDirectories[$type][$template->getName()] as $key => $directory) {
  320.                     if (null === $this->getTemplateDir($key)) {
  321.                         $this->addTemplateDir($directory$key);
  322.                         $this->addConfigDir($directory.DS.'configs'$key);
  323.                     }
  324.                 }
  325.             }
  326.         }
  327.         // -------------------------------------------------------------------------------------------------------------
  328.         // Add the "default" modules template directories if we have to fallback to "default"
  329.         // -------------------------------------------------------------------------------------------------------------
  330.         if ($fallbackToDefaultTemplate) {
  331.             if (isset($this->templateDirectories[$type]['default'])) {
  332.                 foreach ($this->templateDirectories[$type]['default'] as $key => $directory) {
  333.                     if (null === $this->getTemplateDir($key)) {
  334.                         $this->addTemplateDir($directory$key);
  335.                         $this->addConfigDir($directory.DS.'configs'$key);
  336.                     }
  337.                 }
  338.             }
  339.         }
  340.     }
  341.     /**
  342.      * Get template definition.
  343.      *
  344.      * @param bool|string $webAssetTemplateName false to use the current template path, or a template name to
  345.      *                                          load assets from this template instead of the current one
  346.      *
  347.      * @return TemplateDefinition
  348.      */
  349.     public function getTemplateDefinition($webAssetTemplateName false)
  350.     {
  351.         // Deep clone of template definition. We could change the template descriptor of template definition,
  352.         // and we don't want to change the current template definition.
  353.         /** @var TemplateDefinition $ret */
  354.         $ret unserialize(serialize($this->templateDefinition));
  355.         if (false !== $webAssetTemplateName) {
  356.             $customPath str_replace($ret->getName(), $webAssetTemplateName$ret->getPath());
  357.             $ret->setName($webAssetTemplateName);
  358.             $ret->setPath($customPath);
  359.         }
  360.         return $ret;
  361.     }
  362.     /**
  363.      * Check if template definition is not null.
  364.      *
  365.      * @return bool
  366.      */
  367.     public function hasTemplateDefinition()
  368.     {
  369.         return $this->templateDefinition !== null;
  370.     }
  371.     /**
  372.      * Get the current status of the fallback to "default" feature.
  373.      *
  374.      * @return bool
  375.      */
  376.     public function getFallbackToDefaultTemplate()
  377.     {
  378.         return $this->fallbackToDefaultTemplate;
  379.     }
  380.     /**
  381.      * @return string the template path
  382.      */
  383.     public function getTemplatePath()
  384.     {
  385.         return $this->templateDefinition->getPath();
  386.     }
  387.     /**
  388.      * Return a rendered template, either from file or from a string.
  389.      *
  390.      * @param string $resourceType    either 'string' (rendering from a string) or 'file' (rendering a file)
  391.      * @param string $resourceContent the resource content (a text, or a template file name)
  392.      * @param array  $parameters      an associative array of names / value pairs
  393.      * @param bool   $compressOutput  if true, te output is compressed using trimWhitespaces. If false, no compression occurs
  394.      *
  395.      * @throws \Exception
  396.      * @throws \SmartyException
  397.      *
  398.      * @return string the rendered template text
  399.      */
  400.     protected function internalRenderer($resourceType$resourceContent, array $parameters$compressOutput true)
  401.     {
  402.         // If we have to diable the output compression, just unregister the output filter temporarly
  403.         if ($compressOutput == false) {
  404.             $this->unregisterFilter('output', [$this'trimWhitespaces']);
  405.         }
  406.         // Prepare common template variables
  407.         /** @var Session $session */
  408.         $session $this->getRequest()->getSession();
  409.         $lang $session $session->getLang() : Lang::getDefaultLanguage();
  410.         $parameters array_merge($parameters, [
  411.             'locale' => $lang->getLocale(),
  412.             'lang_code' => $lang->getCode(),
  413.             'lang_id' => $lang->getId(),
  414.             'current_url' => $this->getRequest()->getUri(),
  415.             'app' => (object) [
  416.                 'environment' => $this->env,
  417.                 'request' => $this->getRequest(),
  418.                 'session' => $session,
  419.                 'debug' => $this->debug,
  420.             ],
  421.         ]);
  422.         // Assign the parserContext variables
  423.         foreach ($this->parserContext as $var => $value) {
  424.             $this->assign($var$value);
  425.         }
  426.         $this->assign($parameters);
  427.         if (ConfigQuery::read('smarty_mute_undefined_or_null'0)) {
  428.             $this->muteUndefinedOrNullWarnings();
  429.         }
  430.         $output $this->fetch($resourceType.':'.$resourceContent);
  431.         if (!$compressOutput) {
  432.             $this->registerFilter('output', [$this'trimWhitespaces']);
  433.         }
  434.         return $output;
  435.     }
  436.     /**
  437.      * Return a rendered template file.
  438.      *
  439.      * @param string $realTemplateName the template name (from the template directory)
  440.      * @param array  $parameters       an associative array of names / value pairs
  441.      * @param bool   $compressOutput   if true, te output is compressed using trimWhitespaces. If false, no compression occurs
  442.      *
  443.      * @throws ResourceNotFoundException if the template cannot be found
  444.      * @throws \Exception
  445.      * @throws \SmartyException
  446.      *
  447.      * @return string the rendered template text
  448.      */
  449.     public function render($realTemplateName, array $parameters = [], $compressOutput true)
  450.     {
  451.         if (false === $this->templateExists($realTemplateName) || false === $this->checkTemplate($realTemplateName)) {
  452.             throw new ResourceNotFoundException(Translator::getInstance()->trans('Template file %file cannot be found.', ['%file' => $realTemplateName]));
  453.         }
  454.         return $this->internalRenderer('file'$realTemplateName$parameters$compressOutput);
  455.     }
  456.     private function checkTemplate($fileName)
  457.     {
  458.         $templates $this->getTemplateDir();
  459.         $found true;
  460.         /* @noinspection PhpUnusedLocalVariableInspection */
  461.         foreach ($templates as $key => $value) {
  462.             $absolutePath rtrim(realpath(\dirname($value.$fileName)), '/');
  463.             $templateDir rtrim(realpath($value), '/');
  464.             if (!empty($absolutePath) && strpos($absolutePath$templateDir) !== 0) {
  465.                 $found false;
  466.             }
  467.         }
  468.         return $found;
  469.     }
  470.     /**
  471.      * Return a rendered template text.
  472.      *
  473.      * @param string $templateText   the template text
  474.      * @param array  $parameters     an associative array of names / value pairs
  475.      * @param bool   $compressOutput if true, te output is compressed using trimWhitespaces. If false, no compression occurs
  476.      *
  477.      * @throws \Exception
  478.      * @throws \SmartyException
  479.      *
  480.      * @return string the rendered template text
  481.      */
  482.     public function renderString($templateText, array $parameters = [], $compressOutput true)
  483.     {
  484.         return $this->internalRenderer('string'$templateText$parameters$compressOutput);
  485.     }
  486.     /**
  487.      * @return int the status of the response
  488.      */
  489.     public function getStatus()
  490.     {
  491.         return $this->status;
  492.     }
  493.     /**
  494.      * status HTTP of the response.
  495.      *
  496.      * @param int $status
  497.      */
  498.     public function setStatus($status): void
  499.     {
  500.         $this->status $status;
  501.     }
  502.     public function addPlugins(AbstractSmartyPlugin $plugin): void
  503.     {
  504.         $this->plugins[] = $plugin;
  505.     }
  506.     /**
  507.      * From Smarty 3.1.33, we cannot pass parameters by reference to plugin mehods, and declarations like the
  508.      * following will throw the error "Warning: Parameter 2 to <method> expected to be a reference, value given",
  509.      * because Smarty uses call_user_func_array() to call plugins methods.
  510.      *
  511.      *     public function categoryDataAccess($params, &$smarty)
  512.      *
  513.      * We use now a wrapper to provide compatibility with this declaration style
  514.      *
  515.      * @see AbstractSmartyPlugin::__call() for details
  516.      *
  517.      * @throws \SmartyException
  518.      */
  519.     public function registerPlugins(): void
  520.     {
  521.         /** @var AbstractSmartyPlugin $register_plugin */
  522.         foreach ($this->plugins as $register_plugin) {
  523.             $plugins $register_plugin->getPluginDescriptors();
  524.             if (!\is_array($plugins)) {
  525.                 $plugins = [$plugins];
  526.             }
  527.             /** @var SmartyPluginDescriptor $plugin */
  528.             foreach ($plugins as $plugin) {
  529.                 // Use the wrapper to ensure Smarty 3.1.33 compatibility
  530.                 $methodName $this->useMethodCallWrapper && $plugin->getType() === 'function' ?
  531.                     AbstractSmartyPlugin::WRAPPED_METHOD_PREFIX.$plugin->getMethod() :
  532.                     $plugin->getMethod()
  533.                 ;
  534.                 $this->registerPlugin(
  535.                     $plugin->getType(),
  536.                     $plugin->getName(),
  537.                     [
  538.                         $plugin->getClass(),
  539.                         $methodName,
  540.                     ]
  541.                 );
  542.             }
  543.         }
  544.     }
  545.     /**
  546.      * @return \Thelia\Core\Template\TemplateHelperInterface the parser template helper instance
  547.      */
  548.     public function getTemplateHelper()
  549.     {
  550.         return $this->templateHelper;
  551.     }
  552. }