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

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()
  33.     {
  34.         return [new AutoEscapeTokenParser()];
  35.     }
  36.     public function getNodeVisitors()
  37.     {
  38.         return [new EscaperNodeVisitor()];
  39.     }
  40.     public function getFilters()
  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)
  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($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. class_alias('Twig\Extension\EscaperExtension''Twig_Extension_Escaper');
  119. }
  120. namespace {
  121. use Twig\Environment;
  122. use Twig\Error\RuntimeError;
  123. use Twig\Extension\CoreExtension;
  124. use Twig\Extension\EscaperExtension;
  125. use Twig\Markup;
  126. use Twig\Node\Expression\ConstantExpression;
  127. use Twig\Node\Node;
  128. /**
  129.  * Marks a variable as being safe.
  130.  *
  131.  * @param string $string A PHP variable
  132.  *
  133.  * @return string
  134.  */
  135. function twig_raw_filter($string)
  136. {
  137.     return $string;
  138. }
  139. /**
  140.  * Escapes a string.
  141.  *
  142.  * @param mixed  $string     The value to be escaped
  143.  * @param string $strategy   The escaping strategy
  144.  * @param string $charset    The charset
  145.  * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
  146.  *
  147.  * @return string
  148.  */
  149. function twig_escape_filter(Environment $env$string$strategy 'html'$charset null$autoescape false)
  150. {
  151.     if ($autoescape && $string instanceof Markup) {
  152.         return $string;
  153.     }
  154.     if (!\is_string($string)) {
  155.         if (\is_object($string) && method_exists($string'__toString')) {
  156.             if ($autoescape) {
  157.                 $c \get_class($string);
  158.                 $ext $env->getExtension(EscaperExtension::class);
  159.                 if (!isset($ext->safeClasses[$c])) {
  160.                     $ext->safeClasses[$c] = [];
  161.                     foreach (class_parents($string) + class_implements($string) as $class) {
  162.                         if (isset($ext->safeClasses[$class])) {
  163.                             $ext->safeClasses[$c] = array_unique(array_merge($ext->safeClasses[$c], $ext->safeClasses[$class]));
  164.                             foreach ($ext->safeClasses[$class] as $s) {
  165.                                 $ext->safeLookup[$s][$c] = true;
  166.                             }
  167.                         }
  168.                     }
  169.                 }
  170.                 if (isset($ext->safeLookup[$strategy][$c]) || isset($ext->safeLookup['all'][$c])) {
  171.                     return (string) $string;
  172.                 }
  173.             }
  174.             $string = (string) $string;
  175.         } elseif (\in_array($strategy, ['html''js''css''html_attr''url'])) {
  176.             return $string;
  177.         }
  178.     }
  179.     if ('' === $string) {
  180.         return '';
  181.     }
  182.     if (null === $charset) {
  183.         $charset $env->getCharset();
  184.     }
  185.     switch ($strategy) {
  186.         case 'html':
  187.             // see https://www.php.net/htmlspecialchars
  188.             // Using a static variable to avoid initializing the array
  189.             // each time the function is called. Moving the declaration on the
  190.             // top of the function slow downs other escaping strategies.
  191.             static $htmlspecialcharsCharsets = [
  192.                 'ISO-8859-1' => true'ISO8859-1' => true,
  193.                 'ISO-8859-15' => true'ISO8859-15' => true,
  194.                 'utf-8' => true'UTF-8' => true,
  195.                 'CP866' => true'IBM866' => true'866' => true,
  196.                 'CP1251' => true'WINDOWS-1251' => true'WIN-1251' => true,
  197.                 '1251' => true,
  198.                 'CP1252' => true'WINDOWS-1252' => true'1252' => true,
  199.                 'KOI8-R' => true'KOI8-RU' => true'KOI8R' => true,
  200.                 'BIG5' => true'950' => true,
  201.                 'GB2312' => true'936' => true,
  202.                 'BIG5-HKSCS' => true,
  203.                 'SHIFT_JIS' => true'SJIS' => true'932' => true,
  204.                 'EUC-JP' => true'EUCJP' => true,
  205.                 'ISO8859-5' => true'ISO-8859-5' => true'MACROMAN' => true,
  206.             ];
  207.             if (isset($htmlspecialcharsCharsets[$charset])) {
  208.                 return htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE$charset);
  209.             }
  210.             if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
  211.                 // cache the lowercase variant for future iterations
  212.                 $htmlspecialcharsCharsets[$charset] = true;
  213.                 return htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE$charset);
  214.             }
  215.             $string twig_convert_encoding($string'UTF-8'$charset);
  216.             $string htmlspecialchars($string\ENT_QUOTES \ENT_SUBSTITUTE'UTF-8');
  217.             return iconv('UTF-8'$charset$string);
  218.         case 'js':
  219.             // escape all non-alphanumeric characters
  220.             // into their \x or \uHHHH representations
  221.             if ('UTF-8' !== $charset) {
  222.                 $string twig_convert_encoding($string'UTF-8'$charset);
  223.             }
  224.             if (!preg_match('//u'$string)) {
  225.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  226.             }
  227.             $string preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
  228.                 $char $matches[0];
  229.                 /*
  230.                  * A few characters have short escape sequences in JSON and JavaScript.
  231.                  * Escape sequences supported only by JavaScript, not JSON, are omitted.
  232.                  * \" is also supported but omitted, because the resulting string is not HTML safe.
  233.                  */
  234.                 static $shortMap = [
  235.                     '\\' => '\\\\',
  236.                     '/' => '\\/',
  237.                     "\x08" => '\b',
  238.                     "\x0C" => '\f',
  239.                     "\x0A" => '\n',
  240.                     "\x0D" => '\r',
  241.                     "\x09" => '\t',
  242.                 ];
  243.                 if (isset($shortMap[$char])) {
  244.                     return $shortMap[$char];
  245.                 }
  246.                 $codepoint mb_ord($char'UTF-8');
  247.                 if (0x10000 $codepoint) {
  248.                     return sprintf('\u%04X'$codepoint);
  249.                 }
  250.                 // Split characters outside the BMP into surrogate pairs
  251.                 // https://tools.ietf.org/html/rfc2781.html#section-2.1
  252.                 $u $codepoint 0x10000;
  253.                 $high 0xD800 | ($u >> 10);
  254.                 $low 0xDC00 | ($u 0x3FF);
  255.                 return sprintf('\u%04X\u%04X'$high$low);
  256.             }, $string);
  257.             if ('UTF-8' !== $charset) {
  258.                 $string iconv('UTF-8'$charset$string);
  259.             }
  260.             return $string;
  261.         case 'css':
  262.             if ('UTF-8' !== $charset) {
  263.                 $string twig_convert_encoding($string'UTF-8'$charset);
  264.             }
  265.             if (!preg_match('//u'$string)) {
  266.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  267.             }
  268.             $string preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
  269.                 $char $matches[0];
  270.                 return sprintf('\\%X '=== \strlen($char) ? \ord($char) : mb_ord($char'UTF-8'));
  271.             }, $string);
  272.             if ('UTF-8' !== $charset) {
  273.                 $string iconv('UTF-8'$charset$string);
  274.             }
  275.             return $string;
  276.         case 'html_attr':
  277.             if ('UTF-8' !== $charset) {
  278.                 $string twig_convert_encoding($string'UTF-8'$charset);
  279.             }
  280.             if (!preg_match('//u'$string)) {
  281.                 throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
  282.             }
  283.             $string preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
  284.                 /**
  285.                  * This function is adapted from code coming from Zend Framework.
  286.                  *
  287.                  * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
  288.                  * @license   https://framework.zend.com/license/new-bsd New BSD License
  289.                  */
  290.                 $chr $matches[0];
  291.                 $ord \ord($chr);
  292.                 /*
  293.                  * The following replaces characters undefined in HTML with the
  294.                  * hex entity for the Unicode replacement character.
  295.                  */
  296.                 if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
  297.                     return '&#xFFFD;';
  298.                 }
  299.                 /*
  300.                  * Check if the current character to escape has a name entity we should
  301.                  * replace it with while grabbing the hex value of the character.
  302.                  */
  303.                 if (=== \strlen($chr)) {
  304.                     /*
  305.                      * While HTML supports far more named entities, the lowest common denominator
  306.                      * has become HTML5's XML Serialisation which is restricted to the those named
  307.                      * entities that XML supports. Using HTML entities would result in this error:
  308.                      *     XML Parsing Error: undefined entity
  309.                      */
  310.                     static $entityMap = [
  311.                         34 => '&quot;'/* quotation mark */
  312.                         38 => '&amp;',  /* ampersand */
  313.                         60 => '&lt;',   /* less-than sign */
  314.                         62 => '&gt;',   /* greater-than sign */
  315.                     ];
  316.                     if (isset($entityMap[$ord])) {
  317.                         return $entityMap[$ord];
  318.                     }
  319.                     return sprintf('&#x%02X;'$ord);
  320.                 }
  321.                 /*
  322.                  * Per OWASP recommendations, we'll use hex entities for any other
  323.                  * characters where a named entity does not exist.
  324.                  */
  325.                 return sprintf('&#x%04X;'mb_ord($chr'UTF-8'));
  326.             }, $string);
  327.             if ('UTF-8' !== $charset) {
  328.                 $string iconv('UTF-8'$charset$string);
  329.             }
  330.             return $string;
  331.         case 'url':
  332.             return rawurlencode($string);
  333.         default:
  334.             // check the ones set on CoreExtension for BC (to be removed in 3.0)
  335.             $legacyEscapers $env->getExtension(CoreExtension::class)->getEscapers(false);
  336.             if (array_key_exists($strategy$legacyEscapers)) {
  337.                 return $legacyEscapers[$strategy]($env$string$charset);
  338.             }
  339.             $escapers $env->getExtension(EscaperExtension::class)->getEscapers();
  340.             if (array_key_exists($strategy$escapers)) {
  341.                 return $escapers[$strategy]($env$string$charset);
  342.             }
  343.             $escapers array_merge($legacyEscapers$escapers);
  344.             $validStrategies implode(', 'array_merge(['html''js''url''css''html_attr'], array_keys($escapers)));
  345.             throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).'$strategy$validStrategies));
  346.     }
  347. }
  348. /**
  349.  * @internal
  350.  */
  351. function twig_escape_filter_is_safe(Node $filterArgs)
  352. {
  353.     foreach ($filterArgs as $arg) {
  354.         if ($arg instanceof ConstantExpression) {
  355.             return [$arg->getAttribute('value')];
  356.         }
  357.         return [];
  358.     }
  359.     return ['html'];
  360. }
  361. }