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