Issues (287)

src/web/twig/TemplateCommentsParser.php (79 issues)

1
<?php
2
/**
3
 * Template Comments plugin for Craft CMS
4
 *
5
 * Adds a HTML comment to demarcate each Twig template that is included or extended.
6
 *
7
 * @link      https://nystudio107.com/
0 ignored issues
show
The tag in position 1 should be the @copyright tag
Loading history...
8
 * @copyright Copyright (c)  nystudio107
0 ignored issues
show
@copyright tag must contain a year and the name of the copyright holder
Loading history...
9
 */
0 ignored issues
show
PHP version not specified
Loading history...
Missing @category tag in file comment
Loading history...
Missing @package tag in file comment
Loading history...
Missing @author tag in file comment
Loading history...
Missing @license tag in file comment
Loading history...
10
11
/**
12
 * This is a wholesale copy pasta of the Twig Parser class; with one modification so that it
13
 * does not throw a "A block definition cannot be nested under non-capturing nodes" SyntaxError exception
14
 *
15
 * This exception is explained in detail here: https://github.com/twigphp/Twig/issues/3926
16
 *
17
 * So that Template Comments can add HTML comments that indicate any {% include %} or {% extends %} templates
18
 * with execution timing, it uses its own CommentTemplateLoader which wraps the loaded template
19
 * with {% comments 'templateName' %}<loaded-template>{% endcomments %}
20
 *
21
 * There is a corresponding CommentsTokenParser that takes care of parsing the {% comments %} tags
22
 *
23
 * This worked in Twig 1.x and 2.x, but they added a check for block definitions nested under non-capturing
24
 * blocks in Twig 3.x, which causes it to throw an exception in that case. So if you end up with something like:
25
 *
26
 * {% comments 'index' %}
27
 *     {% block conent %}
28
 *     {% endblock %}
29
 * {% encomments %}
30
 *
31
 * ...the SyntaxError exception will be thrown.
32
 *
33
 * I tried adding implements NodeCaptureInterface to the CommentsNode but that resulted in the Parser returning
34
 * the node, causing duplicate rendering for every {% include %} or {% extends %} that was wrapped in a
35
 * {% comments %} tag
36
 *
37
 * We can't just subclass the Parser class, because the properties and methods are private
38
 *
39
 * So here we are.
40
 *
41
 * Don't judge me
42
 */
43
44
namespace nystudio107\templatecomments\web\twig;
45
46
use Twig\Environment;
47
use Twig\Error\SyntaxError;
48
use Twig\ExpressionParser;
49
use Twig\Node\BlockNode;
50
use Twig\Node\BlockReferenceNode;
51
use Twig\Node\BodyNode;
52
use Twig\Node\Expression\AbstractExpression;
53
use Twig\Node\MacroNode;
54
use Twig\Node\ModuleNode;
55
use Twig\Node\Node;
56
use Twig\Node\NodeCaptureInterface;
57
use Twig\Node\NodeOutputInterface;
58
use Twig\Node\PrintNode;
59
use Twig\Node\TextNode;
60
use Twig\NodeTraverser;
61
use Twig\Parser;
62
use Twig\Token;
63
use Twig\TokenParser\TokenParserInterface;
64
use Twig\TokenStream;
65
use function chr;
66
use function count;
67
use function get_class;
68
use function is_array;
69
70
/**
0 ignored issues
show
Missing short description in doc comment
Loading history...
71
 * @author Fabien Potencier <[email protected]>
72
 */
0 ignored issues
show
Missing @category tag in class comment
Loading history...
Missing @package tag in class comment
Loading history...
Missing @license tag in class comment
Loading history...
Missing @link tag in class comment
Loading history...
73
class TemplateCommentsParser extends Parser
74
{
75
    private $stack = [];
0 ignored issues
show
Private member variable "stack" must be prefixed with an underscore
Loading history...
76
    private $stream;
0 ignored issues
show
Private member variable "stream" must be prefixed with an underscore
Loading history...
77
    private $parent;
0 ignored issues
show
Private member variable "parent" must be prefixed with an underscore
Loading history...
78
    private $visitors;
0 ignored issues
show
Private member variable "visitors" must be prefixed with an underscore
Loading history...
79
    private $expressionParser;
0 ignored issues
show
Private member variable "expressionParser" must be prefixed with an underscore
Loading history...
80
    private $blocks;
0 ignored issues
show
Private member variable "blocks" must be prefixed with an underscore
Loading history...
81
    private $blockStack;
0 ignored issues
show
Private member variable "blockStack" must be prefixed with an underscore
Loading history...
82
    private $macros;
0 ignored issues
show
Private member variable "macros" must be prefixed with an underscore
Loading history...
83
    private $env;
0 ignored issues
show
Private member variable "env" must be prefixed with an underscore
Loading history...
84
    private $importedSymbols;
0 ignored issues
show
Private member variable "importedSymbols" must be prefixed with an underscore
Loading history...
85
    private $traits;
0 ignored issues
show
Private member variable "traits" must be prefixed with an underscore
Loading history...
86
    private $embeddedTemplates = [];
0 ignored issues
show
Private member variable "embeddedTemplates" must be prefixed with an underscore
Loading history...
87
    private $varNameSalt = 0;
0 ignored issues
show
Private member variable "varNameSalt" must be prefixed with an underscore
Loading history...
88
89
    public function __construct(Environment $env)
0 ignored issues
show
Missing doc comment for function __construct()
Loading history...
90
    {
91
        $this->env = $env;
92
    }
93
94
    public function getVarName(): string
0 ignored issues
show
Missing doc comment for function getVarName()
Loading history...
95
    {
96
        return sprintf('__internal_parse_%d', $this->varNameSalt++);
97
    }
98
99
    public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
0 ignored issues
show
Missing doc comment for function parse()
Loading history...
100
    {
101
        $vars = get_object_vars($this);
102
        unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['varNameSalt']);
103
        $this->stack[] = $vars;
104
105
        // node visitors
106
        if (null === $this->visitors) {
107
            $this->visitors = $this->env->getNodeVisitors();
108
        }
109
110
        if (null === $this->expressionParser) {
111
            $this->expressionParser = new ExpressionParser($this, $this->env);
112
        }
113
114
        $this->stream = $stream;
115
        $this->parent = null;
116
        $this->blocks = [];
117
        $this->macros = [];
118
        $this->traits = [];
119
        $this->blockStack = [];
120
        $this->importedSymbols = [[]];
121
        $this->embeddedTemplates = [];
122
123
        try {
124
            $body = $this->subparse($test, $dropNeedle);
125
126
            if (null !== $this->parent && null === $body = $this->filterBodyNodes($body)) {
127
                $body = new Node();
128
            }
129
        } catch (SyntaxError $e) {
130
            if (!$e->getSourceContext()) {
131
                $e->setSourceContext($this->stream->getSourceContext());
132
            }
133
134
            if (!$e->getTemplateLine()) {
135
                $e->setTemplateLine($this->stream->getCurrent()->getLine());
136
            }
137
138
            throw $e;
139
        }
140
141
        $node = new ModuleNode(new BodyNode([$body]), $this->parent, new Node($this->blocks), new Node($this->macros), new Node($this->traits), $this->embeddedTemplates, $stream->getSourceContext());
142
143
        $traverser = new NodeTraverser($this->env, $this->visitors);
144
145
        /** @var ModuleNode $node */
0 ignored issues
show
The open comment tag must be the only content on the line
Loading history...
Missing short description in doc comment
Loading history...
The close comment tag must be the only content on the line
Loading history...
146
        $node = $traverser->traverse($node);
147
148
        // restore previous stack so previous parse() call can resume working
149
        foreach (array_pop($this->stack) as $key => $val) {
150
            $this->$key = $val;
151
        }
152
153
        return $node;
154
    }
155
156
    public function subparse($test, bool $dropNeedle = false): Node
0 ignored issues
show
Missing doc comment for function subparse()
Loading history...
157
    {
158
        $lineno = $this->getCurrentToken()->getLine();
159
        $rv = [];
160
        while (!$this->stream->isEOF()) {
161
            switch ($this->getCurrentToken()->getType()) {
162
                case /* Token::TEXT_TYPE */ 0:
0 ignored issues
show
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
163
                    $token = $this->stream->next();
164
                    $rv[] = new TextNode($token->getValue(), $token->getLine());
165
                    break;
166
167
                case /* Token::VAR_START_TYPE */ 2:
0 ignored issues
show
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
168
                    $token = $this->stream->next();
169
                    $expr = $this->expressionParser->parseExpression();
170
                    $this->stream->expect(/* Token::VAR_END_TYPE */ 4);
171
                    $rv[] = new PrintNode($expr, $token->getLine());
172
                    break;
173
174
                case /* Token::BLOCK_START_TYPE */ 1:
0 ignored issues
show
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
175
                    $this->stream->next();
176
                    $token = $this->getCurrentToken();
177
178
                    if (/* Token::NAME_TYPE */ 5 !== $token->getType()) {
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
179
                        throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext());
180
                    }
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
181
182
                    if (null !== $test && $test($token)) {
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
183
                        if ($dropNeedle) {
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
184
                            $this->stream->next();
185
                        }
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
186
187
                        if (1 === count($rv)) {
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
188
                            return $rv[0];
189
                        }
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
190
191
                        return new Node($rv, [], $lineno);
192
                    }
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
193
194
                    if (!$subparser = $this->env->getTokenParser($token->getValue())) {
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
195
                        if (null !== $test) {
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
196
                            $e = new SyntaxError(sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
197
198
                            if (is_array($test) && isset($test[0]) && $test[0] instanceof TokenParserInterface) {
0 ignored issues
show
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
199
                                $e->appendMessage(sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $test[0]->getTag(), $lineno));
200
                            }
0 ignored issues
show
Line indented incorrectly; expected 24 spaces, found 28
Loading history...
201
                        } else {
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
202
                            $e = new SyntaxError(sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext());
203
                            $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers()));
204
                        }
0 ignored issues
show
Line indented incorrectly; expected 20 spaces, found 24
Loading history...
205
206
                        throw $e;
207
                    }
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
208
209
                    $this->stream->next();
210
211
                    $subparser->setParser($this);
212
                    $node = $subparser->parse($token);
213
                    if (null !== $node) {
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
214
                        $rv[] = $node;
215
                    }
0 ignored issues
show
Line indented incorrectly; expected 16 spaces, found 20
Loading history...
216
                    break;
217
218
                default:
0 ignored issues
show
Line indented incorrectly; expected 12 spaces, found 16
Loading history...
219
                    throw new SyntaxError('Lexer or parser ended up in unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext());
220
            }
221
        }
222
223
        if (1 === count($rv)) {
224
            return $rv[0];
225
        }
226
227
        return new Node($rv, [], $lineno);
228
    }
229
230
    public function getBlockStack(): array
0 ignored issues
show
Missing doc comment for function getBlockStack()
Loading history...
231
    {
232
        return $this->blockStack;
233
    }
234
235
    public function peekBlockStack()
0 ignored issues
show
Missing doc comment for function peekBlockStack()
Loading history...
236
    {
237
        return $this->blockStack[count($this->blockStack) - 1] ?? null;
238
    }
239
240
    public function popBlockStack(): void
0 ignored issues
show
Missing doc comment for function popBlockStack()
Loading history...
241
    {
242
        array_pop($this->blockStack);
243
    }
244
245
    public function pushBlockStack($name): void
0 ignored issues
show
Missing doc comment for function pushBlockStack()
Loading history...
246
    {
247
        $this->blockStack[] = $name;
248
    }
249
250
    public function hasBlock(string $name): bool
0 ignored issues
show
Missing doc comment for function hasBlock()
Loading history...
251
    {
252
        return isset($this->blocks[$name]);
253
    }
254
255
    public function getBlock(string $name): Node
0 ignored issues
show
Missing doc comment for function getBlock()
Loading history...
256
    {
257
        return $this->blocks[$name];
258
    }
259
260
    public function setBlock(string $name, BlockNode $value): void
0 ignored issues
show
Missing doc comment for function setBlock()
Loading history...
261
    {
262
        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
263
    }
264
265
    public function hasMacro(string $name): bool
0 ignored issues
show
Missing doc comment for function hasMacro()
Loading history...
266
    {
267
        return isset($this->macros[$name]);
268
    }
269
270
    public function setMacro(string $name, MacroNode $node): void
0 ignored issues
show
Missing doc comment for function setMacro()
Loading history...
271
    {
272
        $this->macros[$name] = $node;
273
    }
274
275
    public function addTrait($trait): void
0 ignored issues
show
Missing doc comment for function addTrait()
Loading history...
276
    {
277
        $this->traits[] = $trait;
278
    }
279
280
    public function hasTraits(): bool
0 ignored issues
show
Missing doc comment for function hasTraits()
Loading history...
281
    {
282
        return count($this->traits) > 0;
283
    }
284
285
    public function embedTemplate(ModuleNode $template)
0 ignored issues
show
Missing doc comment for function embedTemplate()
Loading history...
286
    {
287
        $template->setIndex(mt_rand());
288
289
        $this->embeddedTemplates[] = $template;
290
    }
291
292
    public function addImportedSymbol(string $type, string $alias, string $name = null, AbstractExpression $node = null): void
0 ignored issues
show
Missing doc comment for function addImportedSymbol()
Loading history...
293
    {
294
        $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
295
    }
296
297
    public function getImportedSymbol(string $type, string $alias)
0 ignored issues
show
Missing doc comment for function getImportedSymbol()
Loading history...
298
    {
299
        // if the symbol does not exist in the current scope (0), try in the main/global scope (last index)
300
        return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[count($this->importedSymbols) - 1][$type][$alias] ?? null);
301
    }
302
303
    public function isMainScope(): bool
0 ignored issues
show
Missing doc comment for function isMainScope()
Loading history...
304
    {
305
        return 1 === count($this->importedSymbols);
306
    }
307
308
    public function pushLocalScope(): void
0 ignored issues
show
Missing doc comment for function pushLocalScope()
Loading history...
309
    {
310
        array_unshift($this->importedSymbols, []);
311
    }
312
313
    public function popLocalScope(): void
0 ignored issues
show
Missing doc comment for function popLocalScope()
Loading history...
314
    {
315
        array_shift($this->importedSymbols);
316
    }
317
318
    public function getExpressionParser(): ExpressionParser
0 ignored issues
show
Missing doc comment for function getExpressionParser()
Loading history...
319
    {
320
        return $this->expressionParser;
321
    }
322
323
    public function getParent(): ?Node
0 ignored issues
show
Missing doc comment for function getParent()
Loading history...
324
    {
325
        return $this->parent;
326
    }
327
328
    public function setParent(?Node $parent): void
0 ignored issues
show
Missing doc comment for function setParent()
Loading history...
329
    {
330
        $this->parent = $parent;
331
    }
332
333
    public function getStream(): TokenStream
0 ignored issues
show
Missing doc comment for function getStream()
Loading history...
334
    {
335
        return $this->stream;
336
    }
337
338
    public function getCurrentToken(): Token
0 ignored issues
show
Missing doc comment for function getCurrentToken()
Loading history...
339
    {
340
        return $this->stream->getCurrent();
341
    }
342
343
    private function filterBodyNodes(Node $node, bool $nested = false): ?Node
0 ignored issues
show
Missing doc comment for function filterBodyNodes()
Loading history...
Private method name "TemplateCommentsParser::filterBodyNodes" must be prefixed with an underscore
Loading history...
344
    {
345
        // check that the body does not contain non-empty output nodes
346
        if (
0 ignored issues
show
First condition of a multi-line IF statement must directly follow the opening parenthesis
Loading history...
347
            ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
0 ignored issues
show
Each line in a multi-line IF statement must begin with a boolean operator
Loading history...
348
            || (!$node instanceof TextNode && !$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface)
349
        ) {
350
            if (str_contains((string)$node, chr(0xEF) . chr(0xBB) . chr(0xBF))) {
351
                $t = substr($node->getAttribute('data'), 3);
352
                if ('' === $t || ctype_space($t)) {
353
                    // bypass empty nodes starting with a BOM
354
                    return null;
355
                }
356
            }
357
358
            throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $node->getTemplateLine(), $this->stream->getSourceContext());
359
        }
360
361
        // bypass nodes that "capture" the output
362
        if ($node instanceof NodeCaptureInterface) {
363
            // a "block" tag in such a node will serve as a block definition AND be displayed in place as well
364
            return $node;
365
        }
366
367
        /**
368
         * We intentionally skip this check to avoid throwing an exception, so our {% comments %} tag can
369
         * render correctly
370
         * // "block" tags that are not captured (see above) are only used for defining
371
         * // the content of the block. In such a case, nesting it does not work as
372
         * // expected as the definition is not part of the default template code flow.
373
         * if ($nested && $node instanceof BlockReferenceNode) {
374
         * throw new SyntaxError('A block definition cannot be nested under non-capturing nodes.', $node->getTemplateLine(), $this->stream->getSourceContext());
375
         * }
376
         */
377
        if ($node instanceof NodeOutputInterface) {
378
            return null;
379
        }
380
381
        // here, $nested means "being at the root level of a child template"
382
        // we need to discard the wrapping "Node" for the "body" node
383
        $nested = $nested || Node::class !== get_class($node);
384
        foreach ($node as $k => $n) {
385
            if (null !== $n && null === $this->filterBodyNodes($n, $nested)) {
386
                $node->removeNode($k);
387
            }
388
        }
389
390
        return $node;
391
    }
392
}
393