vendor/twig/twig/src/Extension/EscaperExtension.php line 193

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of Twig.
  4.  *
  5.  * (c) Fabien Potencier
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Twig\Extension {
  11. use Twig\FileExtensionEscapingStrategy;
  12. use Twig\NodeVisitor\EscaperNodeVisitor;
  13. use Twig\TokenParser\AutoEscapeTokenParser;
  14. use Twig\TwigFilter;
  15. final class EscaperExtension extends AbstractExtension
  16. {
  17.     private $defaultStrategy;
  18.     private $escapers = [];
  19.     /** @internal */
  20.     public $safeClasses = [];
  21.     /** @internal */
  22.     public $safeLookup = [];
  23.     /**
  24.      * @param string|false|callable $defaultStrategy An escaping strategy
  25.      *
  26.      * @see setDefaultStrategy()
  27.      */
  28.     public function __construct($defaultStrategy 'html')
  29.     {
  30.         $this->setDefaultStrategy($defaultStrategy);
  31.     }
  32.     public function getTokenParsers(): array
  33.     {
  34.         return [new AutoEscapeTokenParser()];
  35.     }
  36.     public function getNodeVisitors(): array
  37.     {
  38.         return [new EscaperNodeVisitor()];
  39.     }
  40.     public function getFilters(): array
  41.     {
  42.         return [
  43.             new TwigFilter('escape''twig_escape_filter', ['needs_environment' => true'is_safe_callback' => 'twig_escape_filter_is_safe']),
  44.             new TwigFilter('e''twig_escape_filter', ['needs_environment' => true'is_safe_callback' => 'twig_escape_filter_is_safe']),
  45.             new TwigFilter('raw''twig_raw_filter', ['is_safe' => ['all']]),
  46.         ];
  47.     }
  48.     /**
  49.      * Sets the default strategy to use when not defined by the user.
  50.      *
  51.      * The strategy can be a valid PHP callback that takes the template
  52.      * name as an argument and returns the strategy to use.
  53.      *
  54.      * @param string|false|callable $defaultStrategy An escaping strategy
  55.      */
  56.     public function setDefaultStrategy($defaultStrategy): void
  57.     {
  58.         if ('name' === $defaultStrategy) {
  59.             $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess'];
  60.         }
  61.         $this->defaultStrategy $defaultStrategy;
  62.     }
  63.     /**
  64.      * Gets the default strategy to use when not defined by the user.
  65.      *
  66.      * @param string $name The template name
  67.      *
  68.      * @return string|false The default strategy to use for the template
  69.      */
  70.     public function getDefaultStrategy(string $name)
  71.     {
  72.         // disable string callables to avoid calling a function named html or js,
  73.         // or any other upcoming escaping strategy
  74.         if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) {
  75.             return \call_user_func($this->defaultStrategy$name);
  76.         }
  77.         return $this->defaultStrategy;
  78.     }
  79.     /**
  80.      * Defines a new escaper to be used via the escape filter.
  81.      *
  82.      * @param string   $strategy The strategy name that should be used as a strategy in the escape call
  83.      * @param callable $callable A valid PHP callable
  84.      */
  85.     public function setEscaper($strategy, callable $callable)
  86.     {
  87.         $this->escapers[$strategy] = $callable;
  88.     }
  89.     /**
  90.      * Gets all defined escapers.
  91.      *
  92.      * @return callable[] An array of escapers
  93.      */
  94.     public function getEscapers()
  95.     {
  96.         return $this->escapers;
  97.     }
  98.     public function setSafeClasses(array $safeClasses = [])
  99.     {
  100.         $this->safeClasses = [];
  101.         $this->safeLookup = [];
  102.         foreach ($safeClasses as $class => $strategies) {
  103.             $this->addSafeClass($class$strategies);
  104.         }
  105.     }
  106.     public function addSafeClass(string $class, array $strategies)
  107.     {
  108.         $class ltrim($class'\\');
  109.         if (!isset($this->safeClasses[$class])) {
  110.             $this->safeClasses[$class] = [];
  111.         }
  112.         $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
  113.         foreach ($strategies as $strategy) {
  114.             $this->safeLookup[$strategy][$class] = true;
  115.         }
  116.     }
  117. }
  118. }
  119. namespace {
  120. use Twig\Environment;
  121. use Twig\Error\RuntimeError;
  122. use Twig\Extension\EscaperExtension;
  123. use Twig\Markup;
  124. use Twig\Node\Expression\ConstantExpression;
  125. use Twig\Node\Node;
  126. /**
  127.  * Marks a variable as being safe.
  128.  *
  129.  * @param string $string A PHP variable
  130.  */
  131. function twig_raw_filter($string)
  132. {
  133.     return $string;
  134. }
  135. /**
  136.  * Escapes a string.
  137.  *
  138.  * @param mixed  $string     The value to be escaped
  139.  * @param string $strategy   The escaping strategy
  140.  * @param string $charset    The charset
  141.  * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
  142.  *
  143.  * @return string
  144.  */
  145. function twig_escape_filter(Environment $env$string$strategy 'html'$charset null$autoescape false)
  146. {
  147.     if ($autoescape && $string instanceof Markup) {
  148.         return $string;
  149.     }
  150.     if (!\is_string($string)) {
  151.         if (\is_object($string) && method_exists($string'__toString')) {
  152.             if ($autoescape) {
  153.                 $c \get_class($string);
  154.                 $ext $env->getExtension(EscaperExtension::class);
  155.                 if (!isset($ext->safeClasses[$c])) {
  156.                     $ext->safeClasses[$c] = [];
  157.                     foreach (class_parents($string) + class_implements($string) as $class) {
  158.                         if (isset($ext->safeClasses[$class])) {
  159.                             $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
  160.                             foreach ($ext->safeClasses[$class] as $s) {
  161.                                 $ext->safeLookup[$s][$c] = true;
  162.                             }
  163.                         }
  164.                     }
  165.                 }
  166.                 if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
  167.                     return (string) $string;
  168.                 }
  169.             }
  170.             $string = (string) $string;
  171.         } elseif (\in_array($strategy, ['html''js''css''html_attr''url'])) {
  172.             return $string;
  173.         }
  174.     }
  175.     if ('' === $string) {
  176.         return '';
  177.     }
  178.     if (null === $charset) {
  179.         $charset $env->getCharset();
  180.     }
  181.     switch ($strategy) {
  182.         case 'html':
  183.             // see https://www.php.net/htmlspecialchars
  184.             // Using a static variable to avoid initializing the array
  185.             // each time the function is called. Moving the declaration on the
  186.             // top of the function slow downs other escaping strategies.
  187.             static $htmlspecialcharsCharsets = [
  188.                 'ISO-8859-1' => true'ISO8859-1' => true,
  189.                 'ISO-8859-15' => true'ISO8859-15' => true,
  190.                 'utf-8' => true'UTF-8' => true,
  191.                 'CP866' => true'IBM866' => true'866' => true,
  192.                 'CP1251' => true'WINDOWS-1251' => true'WIN-1251' => true,
  193.                 '1251' => true,
  194.                 'CP1252' => true'WINDOWS-1252' => true'1252' => true,
  195.                 'KOI8-R' => true'KOI8-RU' => true'KOI8R' => true,
  196.                 'BIG5' => true'950' => true,
  197.                 'GB2312' => true'936' => true,
  198.                 'BIG5-HKSCS' => true,
  199.                 'SHIFT_JIS' => true'SJIS' => true'932' => true,
  200.                 'EUC-JP' => true'EUCJP' => true,
  201.                 'ISO8859-5' => true'ISO-8859-5' => true'MACROMAN' => true,
  202.             ];
  203.             if (isset($htmlspecialcharsCharsets[$charset])) {
  204.                 return htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE$charset);
  205.             }
  206.             if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
  207.                 // cache the lowercase variant for future iterations
  208.                 $htmlspecialcharsCharsets[$charset] = true;
  209.                 return htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE$charset);
  210.             }
  211.             $string twig_convert_encoding($string'UTF-8'$charset);
  212.             $string htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE'UTF-8');
  213.             return iconv('UTF-8'$charset$string);
  214.         case 'js':
  215.             // escape all non-alphanumeric characters
  216.             // into their \x or \uHHHH representations
  217.             if ('UTF-8' !== $charset) {
  218.                 $string twig_convert_encoding($string'UTF-8'$charset);
  219.             }
  220.             if (!preg_match('//u'$string)) {
  221.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  222.             }
  223.             $string preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
  224.                 $char $matches[0];
  225.                 /*
  226.                  * A few characters have short escape sequences in JSON and JavaScript.
  227.                  * Escape sequences supported only by JavaScript, not JSON, are omitted.
  228.                  * \" is also supported but omitted, because the resulting string is not HTML safe.
  229.                  */
  230.                 static $shortMap = [
  231.                     '\\' => '\\\\',
  232.                     '/' => '\\/',
  233.                     "\x08" => '\b',
  234.                     "\x0C" => '\f',
  235.                     "\x0A" => '\n',
  236.                     "\x0D" => '\r',
  237.                     "\x09" => '\t',
  238.                 ];
  239.                 if (isset($shortMap[$char])) {
  240.                     return $shortMap[$char];
  241.                 }
  242.                 $codepoint mb_ord($char'UTF-8');
  243.                 if (0x10000 $codepoint) {
  244.                     return sprintf('\u%04X'$codepoint);
  245.                 }
  246.                 // Split characters outside the BMP into surrogate pairs
  247.                 // https://tools.ietf.org/html/rfc2781.html#section-2.1
  248.                 $u $codepoint 0x10000;
  249.                 $high 0xD800 | ($u >> 10);
  250.                 $low 0xDC00 | ($u 0x3FF);
  251.                 return sprintf('\u%04X\u%04X'$high$low);
  252.             }, $string);
  253.             if ('UTF-8' !== $charset) {
  254.                 $string iconv('UTF-8'$charset$string);
  255.             }
  256.             return $string;
  257.         case 'css':
  258.             if ('UTF-8' !== $charset) {
  259.                 $string twig_convert_encoding($string'UTF-8'$charset);
  260.             }
  261.             if (!preg_match('//u'$string)) {
  262.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  263.             }
  264.             $string preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
  265.                 $char $matches[0];
  266.                 return sprintf('\\%X '=== \strlen($char) ? \ord($char) : mb_ord($char'UTF-8'));
  267.             }, $string);
  268.             if ('UTF-8' !== $charset) {
  269.                 $string iconv('UTF-8'$charset$string);
  270.             }
  271.             return $string;
  272.         case 'html_attr':
  273.             if ('UTF-8' !== $charset) {
  274.                 $string twig_convert_encoding($string'UTF-8'$charset);
  275.             }
  276.             if (!preg_match('//u'$string)) {
  277.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  278.             }
  279.             $string preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
  280.                 /**
  281.                  * This function is adapted from code coming from Zend Framework.
  282.                  *
  283.                  * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
  284.                  * @license   https://framework.zend.com/license/new-bsd New BSD License
  285.                  */
  286.                 $chr $matches[0];
  287.                 $ord \ord($chr);
  288.                 /*
  289.                  * The following replaces characters undefined in HTML with the
  290.                  * hex entity for the Unicode replacement character.
  291.                  */
  292.                 if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) {
  293.                     return '&#xFFFD;';
  294.                 }
  295.                 /*
  296.                  * Check if the current character to escape has a name entity we should
  297.                  * replace it with while grabbing the hex value of the character.
  298.                  */
  299.                 if (=== \strlen($chr)) {
  300.                     /*
  301.                      * While HTML supports far more named entities, the lowest common denominator
  302.                      * has become HTML5's XML Serialisation which is restricted to the those named
  303.                      * entities that XML supports. Using HTML entities would result in this error:
  304.                      *     XML Parsing Error: undefined entity
  305.                      */
  306.                     static $entityMap = [
  307.                         34 => '&quot;'/* quotation mark */
  308.                         38 => '&amp;',  /* ampersand */
  309.                         60 => '&lt;',   /* less-than sign */
  310.                         62 => '&gt;',   /* greater-than sign */
  311.                     ];
  312.                     if (isset($entityMap[$ord])) {
  313.                         return $entityMap[$ord];
  314.                     }
  315.                     return sprintf('&#x%02X;'$ord);
  316.                 }
  317.                 /*
  318.                  * Per OWASP recommendations, we'll use hex entities for any other
  319.                  * characters where a named entity does not exist.
  320.                  */
  321.                 return sprintf('&#x%04X;'mb_ord($chr'UTF-8'));
  322.             }, $string);
  323.             if ('UTF-8' !== $charset) {
  324.                 $string iconv('UTF-8'$charset$string);
  325.             }
  326.             return $string;
  327.         case 'url':
  328.             return rawurlencode($string);
  329.         default:
  330.             $escapers $env->getExtension(EscaperExtension::class)->getEscapers();
  331.             if (\array_key_exists($strategy$escapers)) {
  332.                 return $escapers[$strategy]($env$string$charset);
  333.             }
  334.             $validStrategies implode(', 'array_merge(['html''js''url''css''html_attr'], array_keys($escapers)));
  335.             throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).'$strategy$validStrategies));
  336.     }
  337. }
  338. /**
  339.  * @internal
  340.  */
  341. function twig_escape_filter_is_safe(Node $filterArgs)
  342. {
  343.     foreach ($filterArgs as $arg) {
  344.         if ($arg instanceof ConstantExpression) {
  345.             return [$arg->getAttribute('value')];
  346.         }
  347.         return [];
  348.     }
  349.     return ['html'];
  350. }
  351. }