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\NodeVisitor;
13
14use Twig\Environment;
15use Twig\Extension\EscaperExtension;
16use Twig\Node\AutoEscapeNode;
17use Twig\Node\BlockNode;
18use Twig\Node\BlockReferenceNode;
19use Twig\Node\Expression\AbstractExpression;
20use Twig\Node\Expression\ConstantExpression;
21use Twig\Node\Expression\FilterExpression;
22use Twig\Node\Expression\OperatorEscapeInterface;
23use Twig\Node\ImportNode;
24use Twig\Node\ModuleNode;
25use Twig\Node\Node;
26use Twig\Node\Nodes;
27use Twig\Node\PrintNode;
28use Twig\NodeTraverser;
29
30/**
31 * @author Fabien Potencier <fabien@symfony.com>
32 *
33 * @internal
34 */
35final class EscaperNodeVisitor implements NodeVisitorInterface
36{
37    private $statusStack = [];
38    private $blocks = [];
39    private $safeAnalysis;
40    private $traverser;
41    private $defaultStrategy = false;
42    private $safeVars = [];
43
44    public function __construct()
45    {
46        $this->safeAnalysis = new SafeAnalysisNodeVisitor();
47    }
48
49    public function enterNode(Node $node, Environment $env): Node
50    {
51        if ($node instanceof ModuleNode) {
52            if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) {
53                $this->defaultStrategy = $defaultStrategy;
54            }
55            $this->safeVars = [];
56            $this->blocks = [];
57        } elseif ($node instanceof AutoEscapeNode) {
58            $this->statusStack[] = $node->getAttribute('value');
59        } elseif ($node instanceof BlockNode) {
60            $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping();
61        } elseif ($node instanceof ImportNode) {
62            $this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name');
63        }
64
65        return $node;
66    }
67
68    public function leaveNode(Node $node, Environment $env): ?Node
69    {
70        if ($node instanceof ModuleNode) {
71            $this->defaultStrategy = false;
72            $this->safeVars = [];
73            $this->blocks = [];
74        } elseif ($node instanceof FilterExpression) {
75            return $this->preEscapeFilterNode($node, $env);
76        } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) {
77            $expression = $node->getNode('expr');
78            if ($expression instanceof OperatorEscapeInterface) {
79                $this->escapeConditional($expression, $env, $type);
80            } else {
81                $node->setNode('expr', $this->escapeExpression($expression, $env, $type));
82            }
83
84            return $node;
85        }
86
87        if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) {
88            array_pop($this->statusStack);
89        } elseif ($node instanceof BlockReferenceNode) {
90            $this->blocks[$node->getAttribute('name')] = $this->needEscaping();
91        }
92
93        return $node;
94    }
95
96    /**
97     * @param AbstractExpression&OperatorEscapeInterface $expression
98     */
99    private function escapeConditional($expression, Environment $env, string $type): void
100    {
101        foreach ($expression->getOperandNamesToEscape() as $name) {
102            /** @var AbstractExpression $operand */
103            $operand = $expression->getNode($name);
104            if ($operand instanceof OperatorEscapeInterface) {
105                $this->escapeConditional($operand, $env, $type);
106            } else {
107                $expression->setNode($name, $this->escapeExpression($operand, $env, $type));
108            }
109        }
110    }
111
112    private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression
113    {
114        return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression);
115    }
116
117    private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression
118    {
119        if ($filter->hasAttribute('twig_callable')) {
120            $type = $filter->getAttribute('twig_callable')->getPreEscape();
121        } else {
122            // legacy
123            $name = $filter->getNode('filter', false)->getAttribute('value');
124            $type = $env->getFilter($name)->getPreEscape();
125        }
126
127        if (null === $type) {
128            return $filter;
129        }
130
131        /** @var AbstractExpression $node */
132        $node = $filter->getNode('node');
133        if ($this->isSafeFor($type, $node, $env)) {
134            return $filter;
135        }
136
137        $filter->setNode('node', $this->getEscaperFilter($env, $type, $node));
138
139        return $filter;
140    }
141
142    private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool
143    {
144        $safe = $this->safeAnalysis->getSafe($expression);
145
146        if (!$safe) {
147            if (null === $this->traverser) {
148                $this->traverser = new NodeTraverser($env, [$this->safeAnalysis]);
149            }
150
151            $this->safeAnalysis->setSafeVars($this->safeVars);
152
153            $this->traverser->traverse($expression);
154            $safe = $this->safeAnalysis->getSafe($expression);
155        }
156
157        return \in_array($type, $safe, true) || \in_array('all', $safe, true);
158    }
159
160    /**
161     * @return string|false
162     */
163    private function needEscaping(): string|bool
164    {
165        if (\count($this->statusStack)) {
166            return $this->statusStack[\count($this->statusStack) - 1];
167        }
168
169        return $this->defaultStrategy ?: false;
170    }
171
172    private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression
173    {
174        $line = $node->getTemplateLine();
175        $filter = $env->getFilter('escape');
176        $args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]);
177
178        return new FilterExpression($node, $filter, $args, $line);
179    }
180
181    public function getPriority(): int
182    {
183        return 0;
184    }
185}
186