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