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\Node\Expression\BlockReferenceExpression;
16use Twig\Node\Expression\ConstantExpression;
17use Twig\Node\Expression\FilterExpression;
18use Twig\Node\Expression\FunctionExpression;
19use Twig\Node\Expression\GetAttrExpression;
20use Twig\Node\Expression\MacroReferenceExpression;
21use Twig\Node\Expression\MethodCallExpression;
22use Twig\Node\Expression\OperatorEscapeInterface;
23use Twig\Node\Expression\ParentExpression;
24use Twig\Node\Expression\Variable\ContextVariable;
25use Twig\Node\Node;
26
27/**
28 * @internal
29 */
30final class SafeAnalysisNodeVisitor implements NodeVisitorInterface
31{
32    private $data = [];
33    private $safeVars = [];
34
35    public function setSafeVars(array $safeVars): void
36    {
37        $this->safeVars = $safeVars;
38    }
39
40    /**
41     * @return array
42     */
43    public function getSafe(Node $node)
44    {
45        $hash = spl_object_id($node);
46        if (!isset($this->data[$hash])) {
47            return [];
48        }
49
50        foreach ($this->data[$hash] as $bucket) {
51            if ($bucket['key'] !== $node) {
52                continue;
53            }
54
55            if (\in_array('html_attr', $bucket['value'], true)) {
56                $bucket['value'][] = 'html';
57                $bucket['value'][] = 'html_attr_relaxed';
58            }
59
60            if (\in_array('html_attr_relaxed', $bucket['value'], true)) {
61                $bucket['value'][] = 'html';
62            }
63
64            return $bucket['value'];
65        }
66
67        return [];
68    }
69
70    private function setSafe(Node $node, array $safe): void
71    {
72        $hash = spl_object_id($node);
73        if (isset($this->data[$hash])) {
74            foreach ($this->data[$hash] as &$bucket) {
75                if ($bucket['key'] === $node) {
76                    $bucket['value'] = $safe;
77
78                    return;
79                }
80            }
81        }
82        $this->data[$hash][] = [
83            'key' => $node,
84            'value' => $safe,
85        ];
86    }
87
88    public function enterNode(Node $node, Environment $env): Node
89    {
90        return $node;
91    }
92
93    public function leaveNode(Node $node, Environment $env): ?Node
94    {
95        if ($node instanceof ConstantExpression) {
96            // constants are marked safe for all
97            $this->setSafe($node, ['all']);
98        } elseif ($node instanceof BlockReferenceExpression) {
99            // blocks are safe by definition
100            $this->setSafe($node, ['all']);
101        } elseif ($node instanceof ParentExpression) {
102            // parent block is safe by definition
103            $this->setSafe($node, ['all']);
104        } elseif ($node instanceof OperatorEscapeInterface) {
105            // intersect safeness of operands
106            $operands = $node->getOperandNamesToEscape();
107            if (2 < \count($operands)) {
108                throw new \LogicException(\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \count($operands)));
109            } elseif (2 === \count($operands)) {
110                $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1])));
111                $this->setSafe($node, $safe);
112            }
113        } elseif ($node instanceof FilterExpression) {
114            // filter expression is safe when the filter is safe
115            if ($node->hasAttribute('twig_callable')) {
116                $filter = $node->getAttribute('twig_callable');
117            } else {
118                // legacy
119                $filter = $env->getFilter($node->getAttribute('name'));
120            }
121
122            if ($filter) {
123                $safe = $filter->getSafe($node->getNode('arguments'));
124                if (null === $safe) {
125                    trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class);
126                    $safe = [];
127                }
128
129                if (!$safe) {
130                    $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety());
131                }
132                $this->setSafe($node, $safe);
133            }
134        } elseif ($node instanceof FunctionExpression) {
135            // function expression is safe when the function is safe
136            if ($node->hasAttribute('twig_callable')) {
137                $function = $node->getAttribute('twig_callable');
138            } else {
139                // legacy
140                $function = $env->getFunction($node->getAttribute('name'));
141            }
142
143            if ($function) {
144                $safe = $function->getSafe($node->getNode('arguments'));
145                if (null === $safe) {
146                    trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class);
147                    $safe = [];
148                }
149                $this->setSafe($node, $safe);
150            }
151        } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) {
152            // all macro calls are safe
153            $this->setSafe($node, ['all']);
154        } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof ContextVariable) {
155            $name = $node->getNode('node')->getAttribute('name');
156            if (\in_array($name, $this->safeVars, true)) {
157                $this->setSafe($node, ['all']);
158            }
159        }
160
161        return $node;
162    }
163
164    private function intersectSafe(array $a, array $b): array
165    {
166        if (!$a || !$b) {
167            return [];
168        }
169
170        if (\in_array('all', $a, true)) {
171            return $b;
172        }
173
174        if (\in_array('all', $b, true)) {
175            return $a;
176        }
177
178        return array_intersect($a, $b);
179    }
180
181    public function getPriority(): int
182    {
183        return 0;
184    }
185}
186