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