Issues (287)

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

Checks scope indent incorrect exact

Coding Style Informational
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/
8
 * @copyright Copyright (c)  nystudio107
9
 */
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
/**
71
 * @author Fabien Potencier <[email protected]>
72
 */
73
class TemplateCommentsParser extends Parser
74
{
75
    private $stack = [];
76
    private $stream;
77
    private $parent;
78
    private $visitors;
79
    private $expressionParser;
80
    private $blocks;
81
    private $blockStack;
82
    private $macros;
83
    private $env;
84
    private $importedSymbols;
85
    private $traits;
86
    private $embeddedTemplates = [];
87
    private $varNameSalt = 0;
88
89
    public function __construct(Environment $env)
90
    {
91
        $this->env = $env;
92
    }
93
94
    public function getVarName(): string
95
    {
96
        return sprintf('__internal_parse_%d', $this->varNameSalt++);
97
    }
98
99
    public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode
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 */
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
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
231
    {
232
        return $this->blockStack;
233
    }
234
235
    public function peekBlockStack()
236
    {
237
        return $this->blockStack[count($this->blockStack) - 1] ?? null;
238
    }
239
240
    public function popBlockStack(): void
241
    {
242
        array_pop($this->blockStack);
243
    }
244
245
    public function pushBlockStack($name): void
246
    {
247
        $this->blockStack[] = $name;
248
    }
249
250
    public function hasBlock(string $name): bool
251
    {
252
        return isset($this->blocks[$name]);
253
    }
254
255
    public function getBlock(string $name): Node
256
    {
257
        return $this->blocks[$name];
258
    }
259
260
    public function setBlock(string $name, BlockNode $value): void
261
    {
262
        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
263
    }
264
265
    public function hasMacro(string $name): bool
266
    {
267
        return isset($this->macros[$name]);
268
    }
269
270
    public function setMacro(string $name, MacroNode $node): void
271
    {
272
        $this->macros[$name] = $node;
273
    }
274
275
    public function addTrait($trait): void
276
    {
277
        $this->traits[] = $trait;
278
    }
279
280
    public function hasTraits(): bool
281
    {
282
        return count($this->traits) > 0;
283
    }
284
285
    public function embedTemplate(ModuleNode $template)
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
293
    {
294
        $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $node];
295
    }
296
297
    public function getImportedSymbol(string $type, string $alias)
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
304
    {
305
        return 1 === count($this->importedSymbols);
306
    }
307
308
    public function pushLocalScope(): void
309
    {
310
        array_unshift($this->importedSymbols, []);
311
    }
312
313
    public function popLocalScope(): void
314
    {
315
        array_shift($this->importedSymbols);
316
    }
317
318
    public function getExpressionParser(): ExpressionParser
319
    {
320
        return $this->expressionParser;
321
    }
322
323
    public function getParent(): ?Node
324
    {
325
        return $this->parent;
326
    }
327
328
    public function setParent(?Node $parent): void
329
    {
330
        $this->parent = $parent;
331
    }
332
333
    public function getStream(): TokenStream
334
    {
335
        return $this->stream;
336
    }
337
338
    public function getCurrentToken(): Token
339
    {
340
        return $this->stream->getCurrent();
341
    }
342
343
    private function filterBodyNodes(Node $node, bool $nested = false): ?Node
344
    {
345
        // check that the body does not contain non-empty output nodes
346
        if (
347
            ($node instanceof TextNode && !ctype_space($node->getAttribute('data')))
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