1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Twig\Extension; 13 14use Twig\Environment; 15use Twig\FileExtensionEscapingStrategy; 16use Twig\Node\Expression\ConstantExpression; 17use Twig\Node\Expression\Filter\RawFilter; 18use Twig\Node\Node; 19use Twig\NodeVisitor\EscaperNodeVisitor; 20use Twig\Runtime\EscaperRuntime; 21use Twig\TokenParser\AutoEscapeTokenParser; 22use Twig\TwigFilter; 23 24final class EscaperExtension extends AbstractExtension 25{ 26 private $environment; 27 private $escapers = []; 28 private $escaper; 29 private $defaultStrategy; 30 31 /** 32 * @param string|false|callable $defaultStrategy An escaping strategy 33 * 34 * @see setDefaultStrategy() 35 */ 36 public function __construct($defaultStrategy = 'html') 37 { 38 $this->setDefaultStrategy($defaultStrategy); 39 } 40 41 public function getTokenParsers(): array 42 { 43 return [new AutoEscapeTokenParser()]; 44 } 45 46 public function getNodeVisitors(): array 47 { 48 return [new EscaperNodeVisitor()]; 49 } 50 51 public function getFilters(): array 52 { 53 return [ 54 new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), 55 new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), 56 new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]), 57 ]; 58 } 59 60 public function getLastModified(): int 61 { 62 return max( 63 parent::getLastModified(), 64 filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()), 65 ); 66 } 67 68 /** 69 * @deprecated since Twig 3.10 70 */ 71 public function setEnvironment(Environment $environment): void 72 { 73 $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; 74 if ($triggerDeprecation) { 75 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); 76 } 77 78 $this->environment = $environment; 79 $this->escaper = null; 80 } 81 82 /** 83 * @return void 84 * 85 * @deprecated since Twig 3.10 86 */ 87 public function setEscaperRuntime(EscaperRuntime $escaper) 88 { 89 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); 90 91 $this->escaper = $escaper; 92 } 93 94 /** 95 * Sets the default strategy to use when not defined by the user. 96 * 97 * The strategy can be a valid PHP callback that takes the template 98 * name as an argument and returns the strategy to use. 99 * 100 * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy 101 */ 102 public function setDefaultStrategy($defaultStrategy): void 103 { 104 if ('name' === $defaultStrategy) { 105 $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; 106 } 107 108 $this->defaultStrategy = $defaultStrategy; 109 } 110 111 /** 112 * Gets the default strategy to use when not defined by the user. 113 * 114 * @param string $name The template name 115 * 116 * @return string|false The default strategy to use for the template 117 */ 118 public function getDefaultStrategy(string $name) 119 { 120 // disable string callables to avoid calling a function named html or js, 121 // or any other upcoming escaping strategy 122 if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { 123 return \call_user_func($this->defaultStrategy, $name); 124 } 125 126 return $this->defaultStrategy; 127 } 128 129 /** 130 * Defines a new escaper to be used via the escape filter. 131 * 132 * @param string $strategy The strategy name that should be used as a strategy in the escape call 133 * @param callable(Environment, string, string): string $callable A valid PHP callable 134 * 135 * @return void 136 * 137 * @deprecated since Twig 3.10 138 */ 139 public function setEscaper($strategy, callable $callable) 140 { 141 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); 142 143 $escaper = $this->getEscaper(__METHOD__); 144 145 $this->escapers[$strategy] = $callable; 146 $callable = function ($string, $charset) use ($callable) { 147 return $callable($this->environment, $string, $charset); 148 }; 149 150 $escaper->setEscaper($strategy, $callable); 151 } 152 153 /** 154 * Gets all defined escapers. 155 * 156 * @return array<string, callable(Environment, string, string): string> An array of escapers 157 * 158 * @deprecated since Twig 3.10 159 */ 160 public function getEscapers() 161 { 162 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__); 163 164 return $this->escapers; 165 } 166 167 /** 168 * @return void 169 * 170 * @deprecated since Twig 3.10 171 */ 172 public function setSafeClasses(array $safeClasses = []) 173 { 174 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); 175 176 $this->getEscaper(__METHOD__)->setSafeClasses($safeClasses); 177 } 178 179 /** 180 * @return void 181 * 182 * @deprecated since Twig 3.10 183 */ 184 public function addSafeClass(string $class, array $strategies) 185 { 186 trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); 187 188 $this->getEscaper(__METHOD__)->addSafeClass($class, $strategies); 189 } 190 191 private function getEscaper(string $fromMethod): EscaperRuntime 192 { 193 if (isset($this->escaper)) { 194 return $this->escaper; 195 } 196 197 if (isset($this->environment)) { 198 return $this->escaper = $this->environment->getRuntime(EscaperRuntime::class); 199 } 200 201 throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', $fromMethod)); 202 } 203 204 /** 205 * @internal 206 * 207 * @return array<string> 208 */ 209 public static function escapeFilterIsSafe(Node $filterArgs) 210 { 211 foreach ($filterArgs as $arg) { 212 if ($arg instanceof ConstantExpression) { 213 return [$arg->getAttribute('value')]; 214 } 215 216 return []; 217 } 218 219 return ['html']; 220 } 221} 222