1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
namespace TYPO3Fluid\Fluid\Core\Parser; |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file belongs to the package "TYPO3 Fluid". |
7
|
|
|
* See LICENSE.txt that was shipped with this package. |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
use TYPO3Fluid\Fluid\Component\Argument\ArgumentCollection; |
11
|
|
|
use TYPO3Fluid\Fluid\Component\ComponentInterface; |
12
|
|
|
use TYPO3Fluid\Fluid\Component\SequencingComponentInterface; |
13
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode; |
14
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode; |
15
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EntryNode; |
16
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\EscapingNode; |
17
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode; |
18
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode; |
19
|
|
|
use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode; |
20
|
|
|
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; |
21
|
|
|
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperResolver; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Sequencer for Fluid syntax |
25
|
|
|
* |
26
|
|
|
* Uses a NoRewindIterator around a sequence of byte values to |
27
|
|
|
* iterate over each syntax-relevant character and determine |
28
|
|
|
* which nodes to create. |
29
|
|
|
* |
30
|
|
|
* Passes the outer iterator between functions that perform the |
31
|
|
|
* iterations. Since the iterator is a NoRewindIterator it will |
32
|
|
|
* not be reset before the start of each loop - meaning that |
33
|
|
|
* when it is passed to a new function, that function continues |
34
|
|
|
* from the last touched index in the byte sequence. |
35
|
|
|
* |
36
|
|
|
* The circumstances around "break or return" in the switches is |
37
|
|
|
* very, very important to understand in context of how iterators |
38
|
|
|
* work. Returning does not advance the iterator like breaking |
39
|
|
|
* would and this causes a different position in the byte sequence |
40
|
|
|
* to be experienced in the method that uses the return value (it |
41
|
|
|
* sees the index of the symbol which terminated the expression, |
42
|
|
|
* not the next symbol after that). |
43
|
|
|
*/ |
44
|
|
|
class Sequencer |
45
|
|
|
{ |
46
|
|
|
public const BYTE_NULL = Splitter::BYTE_NULL; // Zero-byte for terminating documents |
47
|
|
|
public const BYTE_INLINE = 123; // The "{" character indicating an inline expression started |
48
|
|
|
public const BYTE_INLINE_END = 125; // The "}" character indicating an inline expression ended |
49
|
|
|
public const BYTE_PIPE = 124; // The "|" character indicating an inline expression pass operation |
50
|
|
|
public const BYTE_MINUS = 45; // The "-" character (for legacy pass operations) |
51
|
|
|
public const BYTE_TAG = 60; // The "<" character indicating a tag has started |
52
|
|
|
public const BYTE_TAG_END = 62; // The ">" character indicating a tag has ended |
53
|
|
|
public const BYTE_TAG_CLOSE = 47; // The "/" character indicating a tag is a closing tag |
54
|
|
|
public const BYTE_QUOTE_DOUBLE = 34; // The " (standard double-quote) character |
55
|
|
|
public const BYTE_QUOTE_SINGLE = 39; // The ' (standard single-quote) character |
56
|
|
|
public const BYTE_WHITESPACE_SPACE = 32; // A standard space character |
57
|
|
|
public const BYTE_WHITESPACE_TAB = 9; // A standard carriage-return character |
58
|
|
|
public const BYTE_WHITESPACE_RETURN = 13; // A standard tab character |
59
|
|
|
public const BYTE_WHITESPACE_EOL = 10; // A standard (UNIX) line-break character |
60
|
|
|
public const BYTE_SEPARATOR_EQUALS = 61; // The "=" character |
61
|
|
|
public const BYTE_SEPARATOR_COLON = 58; // The ":" character |
62
|
|
|
public const BYTE_SEPARATOR_COMMA = 44; // The "," character |
63
|
|
|
public const BYTE_PARENTHESIS_START = 40; // The "(" character |
64
|
|
|
public const BYTE_PARENTHESIS_END = 41; // The ")" character |
65
|
|
|
public const BYTE_ARRAY_START = 91; // The "[" character |
66
|
|
|
public const BYTE_ARRAY_END = 93; // The "]" character |
67
|
|
|
public const BYTE_BACKSLASH = 92; // The "\" character |
68
|
|
|
public const BYTE_BACKTICK = 96; // The "`" character |
69
|
|
|
public const BYTE_AT = 64; // The "@" character |
70
|
|
|
public const MASK_LINEBREAKS = 0 | (1 << self::BYTE_WHITESPACE_EOL) | (1 << self::BYTE_WHITESPACE_RETURN); |
71
|
|
|
|
72
|
|
|
private const INTERCEPT_OPENING_VIEWHELPER = 1; |
73
|
|
|
private const INTERCEPT_CLOSING_VIEWHELPER = 2; |
74
|
|
|
private const INTERCEPT_OBJECTACCESSOR = 4; |
75
|
|
|
private const INTERCEPT_EXPRESSION = 5; |
76
|
|
|
private const INTERCEPT_SELFCLOSING_VIEWHELPER = 6; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* A counter of nodes which currently disable the interceptor. |
80
|
|
|
* Needed to enable the interceptor again. |
81
|
|
|
* |
82
|
|
|
* @var int |
83
|
|
|
*/ |
84
|
|
|
protected $viewHelperNodesWhichDisableTheInterceptor = 0; |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @var RenderingContextInterface |
88
|
|
|
*/ |
89
|
|
|
public $renderingContext; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @var Contexts |
93
|
|
|
*/ |
94
|
|
|
public $contexts; |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @var Source |
98
|
|
|
*/ |
99
|
|
|
public $source; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* @var Splitter |
103
|
|
|
*/ |
104
|
|
|
public $splitter; |
105
|
|
|
|
106
|
|
|
/** @var \NoRewindIterator */ |
107
|
|
|
public $sequence; |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* @var Configuration |
111
|
|
|
*/ |
112
|
|
|
public $configuration; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @var ViewHelperResolver |
116
|
|
|
*/ |
117
|
|
|
protected $resolver; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Whether or not the escaping interceptors are active |
121
|
|
|
* |
122
|
|
|
* @var boolean |
123
|
|
|
*/ |
124
|
|
|
protected $escapingEnabled = true; |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @var ComponentInterface[] |
128
|
|
|
*/ |
129
|
|
|
protected $nodeStack = []; |
130
|
|
|
|
131
|
|
|
public function __construct( |
132
|
|
|
RenderingContextInterface $renderingContext, |
133
|
|
|
Contexts $contexts, |
134
|
|
|
Source $source, |
135
|
|
|
?Configuration $configuration = null |
136
|
|
|
) { |
137
|
|
|
$this->source = $source; |
138
|
|
|
$this->contexts = $contexts; |
139
|
|
|
$this->renderingContext = $renderingContext; |
140
|
|
|
$this->resolver = $renderingContext->getViewHelperResolver(); |
141
|
|
|
$this->configuration = $configuration ?? $renderingContext->getParserConfiguration(); |
142
|
|
|
$this->escapingEnabled = $this->configuration->isFeatureEnabled(Configuration::FEATURE_ESCAPING); |
143
|
|
|
$this->splitter = new Splitter($this->source, $this->contexts); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
public function sequence(): ComponentInterface |
147
|
|
|
{ |
148
|
|
|
// Root context - the only symbols that cause any context switching are curly brace open and tag start, but |
149
|
|
|
// only if they are not preceded by a backslash character; in which case the symbol is ignored and merely |
150
|
|
|
// collected as part of the output string. NULL bytes are ignored in this context (the Splitter will yield |
151
|
|
|
// a single NULL byte when end of source is reached). |
152
|
|
|
$this->nodeStack[] = (new EntryNode())->onOpen($this->renderingContext); |
153
|
|
|
$this->sequence = $this->splitter->parse(); |
|
|
|
|
154
|
|
|
$countedEscapes = 0; |
155
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
156
|
|
|
$node = end($this->nodeStack); |
157
|
|
|
$text = $captured . ($countedEscapes > 0 ? chr($symbol) : ''); |
158
|
|
|
if ($text !== '') { |
159
|
|
|
$node->addChild(new TextNode($text)); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
if ($countedEscapes > 0) { |
163
|
|
|
$countedEscapes = 0; |
164
|
|
|
continue; |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
switch ($symbol) { |
168
|
|
|
case self::BYTE_BACKSLASH: |
169
|
|
|
++$countedEscapes; |
170
|
|
|
break; |
171
|
|
|
|
172
|
|
|
case self::BYTE_INLINE: |
173
|
|
|
$countedEscapes = 0; |
174
|
|
|
$node->addChild($this->sequenceInlineNodes(false)); |
175
|
|
|
$this->splitter->switch($this->contexts->root); |
176
|
|
|
break; |
177
|
|
|
|
178
|
|
|
case self::BYTE_TAG: |
179
|
|
|
$countedEscapes = 0; |
180
|
|
|
$childNode = $this->sequenceTagNode(); |
181
|
|
|
$this->splitter->switch($this->contexts->root); |
182
|
|
|
|
183
|
|
|
if ($childNode) { |
184
|
|
|
end($this->nodeStack)->addChild($childNode); |
185
|
|
|
} |
186
|
|
|
break; |
187
|
|
|
|
188
|
|
|
case self::BYTE_NULL: |
189
|
|
|
break; |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// If there is more than a single node remaining in the stack this indicates an error. More precisely it |
194
|
|
|
// indicates that some function called in the above switch added a node to the stack but failed to remove it |
195
|
|
|
// before returning, which usually indicates that the template contains one or more incorrectly closed tags. |
196
|
|
|
// In order to report this as error we collect the classes of every remaining node in the stack. Unfortunately |
197
|
|
|
// we cannot report the position of where the closing tag was expected - this is simply not known to Fluid. |
198
|
|
|
if (count($this->nodeStack) !== 1) { |
199
|
|
|
$names = []; |
200
|
|
|
while (($unterminatedNode = array_pop($this->nodeStack))) { |
201
|
|
|
$names[] = get_class($unterminatedNode); |
202
|
|
|
} |
203
|
|
|
throw $this->createErrorAtPosition( |
204
|
|
|
'Unterminated node(s) detected: ' . implode(', ', array_reverse($names)), |
205
|
|
|
1562671632 |
206
|
|
|
); |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
// Finishing sequencing means returning the single node that remains in the node stack, firing the onClose |
210
|
|
|
// method on it and assigning the rendering context to the ArgumentCollection carried by the root node. |
211
|
|
|
$node = array_pop($this->nodeStack)->onClose($this->renderingContext); |
212
|
|
|
$node->getArguments()->setRenderingContext($this->renderingContext); |
213
|
|
|
return $node; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
public function sequenceUntilClosingTagAndIgnoreNested(ComponentInterface $parent, ?string $namespace, string $method): void |
217
|
|
|
{ |
218
|
|
|
// Special method of sequencing which completely ignores any and all Fluid code inside a tag if said tag is |
219
|
|
|
// associated with a Component that implements SequencingComponentInterface and calls this method as a default |
220
|
|
|
// implementation of an "ignore everything until closed" type of behavior. Exists in Sequencer since this is |
221
|
|
|
// the most common expected use case which would otherwise 1) be likely to become duplicated, or 2) require the |
222
|
|
|
// use of a trait or base class for this single method alone. Since the Component which implements the signal |
223
|
|
|
// interface already receives the Sequencer instance it is readily available without composition concerns. |
224
|
|
|
$matchingTag = $namespace ? $namespace . ':' . $method : $method; |
225
|
|
|
$matchingTagLength = strlen($matchingTag); |
226
|
|
|
$ignoredNested = 0; |
227
|
|
|
$this->splitter->switch($this->contexts->inactive); |
228
|
|
|
$this->sequence->next(); |
229
|
|
|
$text = ''; |
230
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
231
|
|
|
if ($symbol === self::BYTE_TAG_END && $captured !== null && strncmp($captured, $matchingTag, $matchingTagLength) === 0) { |
232
|
|
|
// An opening tag matching the parent tag - treat as text and add to ignored count. |
233
|
|
|
++$ignoredNested; |
234
|
|
|
} elseif ($symbol === self::BYTE_TAG_END && $captured === '/' . $matchingTag) { |
235
|
|
|
// A closing version of the parent tag. Check counter; if zero, finish. If not, decrease ignored count. |
236
|
|
|
if ($ignoredNested === 0) { |
237
|
|
|
$parent->addChild(new TextNode((string) substr($text, 0, -1))); |
238
|
|
|
return; |
239
|
|
|
} |
240
|
|
|
--$ignoredNested; |
241
|
|
|
} |
242
|
|
|
$text .= (string) $captured . chr($symbol); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
throw $this->createErrorAtPosition( |
246
|
|
|
'Unterminated inactive tag: ' . $matchingTag, |
247
|
|
|
1564665730 |
248
|
|
|
); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
protected function sequenceCharacterData(string $text): ComponentInterface |
252
|
|
|
{ |
253
|
|
|
$this->splitter->switch($this->contexts->data); |
254
|
|
|
$this->sequence->next(); |
255
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
256
|
|
|
$text .= $captured; |
257
|
|
|
if ($symbol === self::BYTE_TAG_END && substr($this->source->source, $this->splitter->index - 3, 2) === ']]') { |
258
|
|
|
$text .= '>'; |
259
|
|
|
break; |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
return new TextNode($text); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Sequence a Fluid feature toggle node. Does not return |
267
|
|
|
* any node, only toggles various features of the Fluid |
268
|
|
|
* parser configuration or assigns context parameters |
269
|
|
|
* like namespaces. |
270
|
|
|
* |
271
|
|
|
* For backwards compatibility we allow the toggle name |
272
|
|
|
* to be passed, which is used in an explicit check when |
273
|
|
|
* sequencing inline nodes to detect if a {namespace ...} |
274
|
|
|
* node was encountered, in which case, this is not known |
275
|
|
|
* until the "toggle" has already been captured. |
276
|
|
|
* |
277
|
|
|
* @param string|null $toggle |
278
|
|
|
*/ |
279
|
|
|
protected function sequenceToggleInstruction(?string $toggle = null): void |
280
|
|
|
{ |
281
|
|
|
$this->splitter->switch($this->contexts->toggle); |
282
|
|
|
$this->sequence->next(); |
283
|
|
|
$flag = null; |
|
|
|
|
284
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
285
|
|
|
switch ($symbol) { |
286
|
|
|
case self::BYTE_WHITESPACE_SPACE: |
287
|
|
|
$toggle = $toggle ?? $captured; |
288
|
|
|
break; |
289
|
|
|
case self::BYTE_INLINE_END: |
290
|
|
|
if ($toggle === 'namespace') { |
291
|
|
|
$parts = explode('=', (string) $captured); |
292
|
|
|
$this->resolver->addNamespace($parts[0], $parts[1] ?? null); |
293
|
|
|
return; |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
$this->configuration->setFeatureState($toggle, $captured ?? true); |
297
|
|
|
// Re-read the parser configuration and react accordingly to any flags that may have changed. |
298
|
|
|
$this->escapingEnabled = $this->configuration->isFeatureEnabled(Configuration::FEATURE_ESCAPING); |
299
|
|
|
if (!$this->configuration->isFeatureEnabled(Configuration::FEATURE_PARSING)) { |
300
|
|
|
throw (new PassthroughSourceException('Source must be represented as raw string', 1563379852)) |
301
|
|
|
->setSource((string)$this->sequenceRemainderAsText()); |
302
|
|
|
} |
303
|
|
|
return; |
304
|
|
|
} |
305
|
|
|
} |
306
|
|
|
throw $this->createErrorAtPosition('Unterminated feature toggle', 1563383038); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
protected function sequenceTagNode(): ?ComponentInterface |
310
|
|
|
{ |
311
|
|
|
$arguments = null; |
312
|
|
|
$definitions = null; |
313
|
|
|
$text = '<'; |
314
|
|
|
$key = null; |
315
|
|
|
$namespace = null; |
316
|
|
|
$method = null; |
317
|
|
|
$bytes = &$this->source->bytes; |
318
|
|
|
$node = new RootNode(); |
319
|
|
|
$closesNode = null; |
|
|
|
|
320
|
|
|
$selfClosing = false; |
321
|
|
|
$closing = false; |
322
|
|
|
$escapingEnabledBackup = $this->escapingEnabled; |
323
|
|
|
$viewHelperNode = null; |
324
|
|
|
|
325
|
|
|
$this->splitter->switch($this->contexts->tag); |
326
|
|
|
$this->sequence->next(); |
327
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
328
|
|
|
$text .= $captured; |
329
|
|
|
switch ($symbol) { |
330
|
|
|
case self::BYTE_ARRAY_START: |
331
|
|
|
// Possible P/CDATA section. Check text explicitly for match, if matched, begin parsing-insensitive |
332
|
|
|
// pass through sequenceCharacterDataNode() |
333
|
|
|
$text .= '['; |
334
|
|
|
if ($text === '<![CDATA[' || $text === '<![PCDATA[') { |
335
|
|
|
return $this->sequenceCharacterData($text); |
336
|
|
|
} |
337
|
|
|
break; |
338
|
|
|
|
339
|
|
|
case self::BYTE_INLINE: |
340
|
|
|
$contextBefore = $this->splitter->context; |
341
|
|
|
$collected = $this->sequenceInlineNodes(isset($namespace, $method)); |
342
|
|
|
$node->addChild(new TextNode($text)); |
343
|
|
|
$node->addChild($collected); |
344
|
|
|
$text = ''; |
345
|
|
|
$this->splitter->switch($contextBefore); |
346
|
|
|
break; |
347
|
|
|
|
348
|
|
|
case self::BYTE_SEPARATOR_EQUALS: |
349
|
|
|
$key = $key . $captured; |
350
|
|
|
$text .= '='; |
351
|
|
|
if ($key === '') { |
352
|
|
|
throw $this->createErrorAtPosition('Unexpected equals sign without preceding attribute/key name', 1561039838); |
353
|
|
|
} elseif ($definitions !== null && !isset($definitions[$key]) && !$viewHelperNode->allowUndeclaredArgument($key)) { |
354
|
|
|
$error = $this->createUnsupportedArgumentError($key, $definitions); |
355
|
|
|
return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error)); |
356
|
|
|
} |
357
|
|
|
break; |
358
|
|
|
|
359
|
|
|
case self::BYTE_QUOTE_DOUBLE: |
360
|
|
|
case self::BYTE_QUOTE_SINGLE: |
361
|
|
|
$text .= chr($symbol); |
362
|
|
|
if ($key === null) { |
363
|
|
|
throw $this->createErrorAtPosition('Quoted value without a key is not allowed in tags', 1558952412); |
364
|
|
|
} |
365
|
|
|
if ($arguments->isArgumentBoolean($key)) { |
366
|
|
|
$arguments[$key] = $this->sequenceBooleanNode()->flatten(true); |
367
|
|
|
} else { |
368
|
|
|
$arguments[$key] = $this->sequenceQuotedNode()->flatten(true); |
369
|
|
|
} |
370
|
|
|
$key = null; |
371
|
|
|
break; |
372
|
|
|
|
373
|
|
|
case self::BYTE_TAG_CLOSE: |
374
|
|
|
$method = $method ?? $captured; |
375
|
|
|
$text .= '/'; |
376
|
|
|
$closing = true; |
377
|
|
|
$selfClosing = $bytes[$this->splitter->index - 1] !== self::BYTE_TAG; |
378
|
|
|
|
379
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) { |
380
|
|
|
// Arguments may be pending: if $key is set we must create an ECMA literal style shorthand |
381
|
|
|
// (attribute value is variable of same name as attribute). Two arguments may be created in |
382
|
|
|
// this case, if both $key and $captured are non-null. The former contains a potentially |
383
|
|
|
// pending argument and the latter contains a captured value-less attribute right before the |
384
|
|
|
// tag closing character. |
385
|
|
|
if ($key !== null) { |
386
|
|
|
$arguments[$key] = new ObjectAccessorNode((string) $key); |
387
|
|
|
$key = null; |
388
|
|
|
} |
389
|
|
|
// (see comment above) Hence, the two conditions must not be compunded to else-if. |
390
|
|
|
if ($captured !== null) { |
391
|
|
|
$arguments[$captured] = new ObjectAccessorNode($captured); |
392
|
|
|
} |
393
|
|
|
} |
394
|
|
|
break; |
395
|
|
|
|
396
|
|
|
case self::BYTE_SEPARATOR_COLON: |
397
|
|
|
$text .= ':'; |
398
|
|
|
if (!$method) { |
399
|
|
|
// If we haven't encountered a method yet, then $method won't be set, and we can assign NS now |
400
|
|
|
$namespace = $namespace ?? $captured; |
401
|
|
|
} else { |
402
|
|
|
// If we do have a method this means we encountered a colon as part of an attribute name |
403
|
|
|
$key = $key ?? ($captured . ':'); |
404
|
|
|
} |
405
|
|
|
break; |
406
|
|
|
|
407
|
|
|
case self::BYTE_TAG_END: |
408
|
|
|
$text .= '>'; |
409
|
|
|
$method = $method ?? $captured; |
410
|
|
|
|
411
|
|
|
$this->escapingEnabled = $escapingEnabledBackup; |
412
|
|
|
|
413
|
|
|
if (($namespace === null && ($this->splitter->context->context === Context::CONTEXT_DEAD || !$this->resolver->isAliasRegistered((string) $method))) || $this->resolver->isNamespaceIgnored((string) $namespace)) { |
414
|
|
|
return $node->addChild(new TextNode($text))->flatten(); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
try { |
418
|
|
|
if (!$closing || $selfClosing) { |
419
|
|
|
$viewHelperNode = $viewHelperNode ?? $this->resolver->createViewHelperInstance($namespace, (string) $method); |
420
|
|
|
$viewHelperNode->onOpen($this->renderingContext)->getArguments()->validate(); |
421
|
|
|
} else { |
422
|
|
|
// $closing will be true and $selfClosing false; add to stack, continue with children. |
423
|
|
|
$viewHelperNode = array_pop($this->nodeStack); |
424
|
|
|
$expectedClass = $this->resolver->resolveViewHelperClassName($namespace, (string) $method); |
425
|
|
|
if (!$viewHelperNode instanceof $expectedClass) { |
426
|
|
|
throw $this->createErrorAtPosition( |
427
|
|
|
sprintf( |
428
|
|
|
'Mismatched closing tag. Expecting: %s:%s (%s). Found: (%s).', |
429
|
|
|
$namespace, |
430
|
|
|
$method, |
431
|
|
|
$expectedClass, |
432
|
|
|
get_class($viewHelperNode) |
433
|
|
|
), |
434
|
|
|
1557700789 |
435
|
|
|
); |
436
|
|
|
} |
437
|
|
|
} |
438
|
|
|
} catch (\TYPO3Fluid\Fluid\Core\Exception $exception) { |
439
|
|
|
$error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode()); |
440
|
|
|
return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error)); |
441
|
|
|
} |
442
|
|
|
|
443
|
|
|
// Possibly pending argument still needs to be processed since $key is not null. Create an ECMA |
444
|
|
|
// literal style associative array entry. Do the same for $captured. |
445
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) { |
446
|
|
|
if ($key !== null) { |
447
|
|
|
$value = new ObjectAccessorNode((string) $key); |
448
|
|
|
$arguments[$key] = $value; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
if ($captured !== null) { |
452
|
|
|
$value = new ObjectAccessorNode((string) $captured); |
453
|
|
|
$arguments[$captured] = $value; |
454
|
|
|
} |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
if (!$closing) { |
458
|
|
|
// The node is neither a closing or self-closing node (= an opening node expecting tag content). |
459
|
|
|
// Add it to the stack and return null to return the Sequencer to "root" context and continue |
460
|
|
|
// sequencing the tag's body - parsed nodes then get attached to this node as children. |
461
|
|
|
$viewHelperNode = $this->callInterceptor($viewHelperNode, self::INTERCEPT_OPENING_VIEWHELPER); |
462
|
|
|
if ($viewHelperNode instanceof SequencingComponentInterface) { |
463
|
|
|
// The Component will take over sequencing. It will return if encountering the right closing |
464
|
|
|
// tag - so when it returns, we reached the end of the Component and must pop the stack. |
465
|
|
|
$viewHelperNode->sequence($this, $namespace, (string) $method); |
466
|
|
|
return $viewHelperNode; |
467
|
|
|
} |
468
|
|
|
$this->nodeStack[] = $viewHelperNode; |
469
|
|
|
return null; |
470
|
|
|
} |
471
|
|
|
|
472
|
|
|
$viewHelperNode = $viewHelperNode->onClose($this->renderingContext); |
473
|
|
|
|
474
|
|
|
$viewHelperNode = $this->callInterceptor( |
475
|
|
|
$viewHelperNode, |
476
|
|
|
$selfClosing ? self::INTERCEPT_SELFCLOSING_VIEWHELPER : self::INTERCEPT_CLOSING_VIEWHELPER |
477
|
|
|
); |
478
|
|
|
|
479
|
|
|
return $viewHelperNode; |
480
|
|
|
|
481
|
|
|
case self::BYTE_WHITESPACE_TAB: |
482
|
|
|
case self::BYTE_WHITESPACE_RETURN: |
483
|
|
|
case self::BYTE_WHITESPACE_EOL: |
484
|
|
|
case self::BYTE_WHITESPACE_SPACE: |
485
|
|
|
$text .= chr($symbol); |
486
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_ATTRIBUTES) { |
487
|
|
|
if ($captured !== null) { |
488
|
|
|
// Encountering this case means we've collected a previous key and now collected a non-empty |
489
|
|
|
// string value before encountering an equals sign. This is treated as ECMA literal short |
490
|
|
|
// hand equivalent of having written `attr="{attr}"` in the Fluid template. |
491
|
|
|
$key = $captured; |
492
|
|
|
} |
493
|
|
|
} elseif ($namespace !== null || (!isset($namespace, $method) && $this->resolver->isAliasRegistered((string)$captured))) { |
494
|
|
|
$method = $captured; |
495
|
|
|
|
496
|
|
|
try { |
497
|
|
|
$viewHelperNode = $this->resolver->createViewHelperInstance($namespace, $method); |
498
|
|
|
$arguments = $viewHelperNode->getArguments(); |
499
|
|
|
$definitions = $arguments->getDefinitions(); |
500
|
|
|
} catch (\TYPO3Fluid\Fluid\Core\Exception $exception) { |
501
|
|
|
$error = $this->createErrorAtPosition($exception->getMessage(), $exception->getCode()); |
502
|
|
|
return new TextNode($this->renderingContext->getErrorHandler()->handleParserError($error)); |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
// Forcibly disable escaping OFF as default decision for whether or not to escape an argument. |
506
|
|
|
$this->escapingEnabled = false; |
507
|
|
|
$this->splitter->switch($this->contexts->attributes); |
508
|
|
|
break; |
509
|
|
|
} else { |
510
|
|
|
// A whitespace before a colon means the tag is not a namespaced tag. We will ignore everything |
511
|
|
|
// inside this tag, except for inline syntax, until the tag ends. For this we use a special, |
512
|
|
|
// limited variant of the root context where instead of scanning for "<" we scan for ">". |
513
|
|
|
// We continue in this same loop because it still matches the potential symbols being yielded. |
514
|
|
|
// Most importantly: this new reduced context will NOT match a colon which is the trigger symbol |
515
|
|
|
// for a ViewHelper tag. |
516
|
|
|
$this->splitter->switch($this->contexts->dead); |
517
|
|
|
} |
518
|
|
|
break; |
519
|
|
|
} |
520
|
|
|
} |
521
|
|
|
|
522
|
|
|
// This case on the surface of it, belongs as "default" case in the switch above. However, the only case that |
523
|
|
|
// would *actually* produce this error, is if the splitter reaches EOF (null byte) symbol before the tag was |
524
|
|
|
// closed. Literally every other possible error type will be thrown as more specific exceptions (e.g. invalid |
525
|
|
|
// argument, missing key, wrong quotes, bad inline and *everything* else with the exception of EOF). Even a |
526
|
|
|
// stray null byte would not be caught here as null byte is not part of the symbol collection for "tag" context. |
527
|
|
|
throw $this->createErrorAtPosition('Unexpected token in tag sequencing', 1557700786); |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
protected function sequenceInlineNodes(bool $allowArray = true): ComponentInterface |
531
|
|
|
{ |
532
|
|
|
$text = '{'; |
533
|
|
|
/** @var ComponentInterface|null $node */ |
534
|
|
|
$node = null; |
535
|
|
|
$key = null; |
536
|
|
|
$namespace = null; |
537
|
|
|
$method = null; |
|
|
|
|
538
|
|
|
$definitions = null; |
|
|
|
|
539
|
|
|
$potentialAccessor = null; |
540
|
|
|
$callDetected = false; |
541
|
|
|
$hasPass = false; |
542
|
|
|
$hasColon = null; |
543
|
|
|
$hasWhitespace = false; |
544
|
|
|
$isArray = false; |
545
|
|
|
$arguments = new ArgumentCollection(); |
546
|
|
|
$parts = []; |
547
|
|
|
$ignoredEndingBraces = 0; |
548
|
|
|
$countedEscapes = 0; |
549
|
|
|
$restore = $this->splitter->switch($this->contexts->inline); |
550
|
|
|
$this->sequence->next(); |
551
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
552
|
|
|
$text .= $captured; |
553
|
|
|
switch ($symbol) { |
554
|
|
|
case self::BYTE_AT: |
555
|
|
|
$this->sequenceToggleInstruction(); |
556
|
|
|
$this->splitter->switch($restore); |
557
|
|
|
return new TextNode(''); |
558
|
|
|
break; |
|
|
|
|
559
|
|
|
|
560
|
|
|
case self::BYTE_BACKSLASH: |
561
|
|
|
// Increase the number of counted escapes (is passed to sequenceNode() in the "QUOTE" cases and reset |
562
|
|
|
// after the quoted string is extracted). |
563
|
|
|
++$countedEscapes; |
564
|
|
|
if ($hasWhitespace) { |
565
|
|
|
$node = $node ?? new RootNode(); |
566
|
|
|
} else { |
567
|
|
|
$node = $node ?? new ObjectAccessorNode(); |
568
|
|
|
} |
569
|
|
|
if ($captured !== null) { |
570
|
|
|
$node->addChild(new TextNode((string) $captured)); |
571
|
|
|
} |
572
|
|
|
break; |
573
|
|
|
|
574
|
|
|
case self::BYTE_ARRAY_START: |
575
|
|
|
$text .= chr($symbol); |
576
|
|
|
$isArray = $allowArray; |
577
|
|
|
|
578
|
|
|
// Sequence the node. Pass the "use numeric keys?" boolean based on the current byte. Only array |
579
|
|
|
// start creates numeric keys. Inline start with keyless values creates ECMA style {foo:foo, bar:bar} |
580
|
|
|
// from {foo, bar}. |
581
|
|
|
$arguments[$key ?? $captured ?? 0] = $node = new ArrayNode(); |
582
|
|
|
$this->sequenceArrayNode($node, true); |
583
|
|
|
$key = null; |
584
|
|
|
break; |
585
|
|
|
|
586
|
|
|
case self::BYTE_INLINE: |
587
|
|
|
// Encountering this case can mean different things: sub-syntax like {foo.{index}} or array, depending |
588
|
|
|
// on presence of either a colon or comma before the inline. In protected mode it is simply added. |
589
|
|
|
$text .= '{'; |
590
|
|
|
$node = $node ?? new ObjectAccessorNode(); |
591
|
|
|
if ($countedEscapes > 0) { |
592
|
|
|
++$ignoredEndingBraces; |
593
|
|
|
$countedEscapes = 0; |
594
|
|
|
if ($captured !== null) { |
595
|
|
|
$node->addChild(new TextNode((string)$captured)); |
596
|
|
|
} |
597
|
|
|
} elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED) { |
598
|
|
|
// Ignore one ending additional curly brace. Subtracted in the BYTE_INLINE_END case below. |
599
|
|
|
// The expression in this case looks like {{inline}.....} and we capture the curlies. |
600
|
|
|
$potentialAccessor .= $captured; |
601
|
|
|
} elseif ($allowArray || $isArray) { |
602
|
|
|
$isArray = true; |
603
|
|
|
$captured = $key ?? $captured ?? $potentialAccessor; |
604
|
|
|
// This is a sub-syntax following a colon - meaning it is an array. |
605
|
|
|
if ($captured !== null) { |
606
|
|
|
$arguments[$key ?? $captured ?? 0] = $node = new ArrayNode(); |
607
|
|
|
$this->sequenceArrayNode($node); |
608
|
|
|
} |
609
|
|
|
} else { |
610
|
|
|
if ($captured !== null) { |
611
|
|
|
$node->addChild(new TextNode((string) $captured)); |
612
|
|
|
} |
613
|
|
|
$childNodeToAdd = $this->sequenceInlineNodes($allowArray); |
614
|
|
|
$node->addChild($childNodeToAdd); |
615
|
|
|
} |
616
|
|
|
break; |
617
|
|
|
|
618
|
|
|
case self::BYTE_MINUS: |
619
|
|
|
$text .= '-'; |
620
|
|
|
$potentialAccessor = $potentialAccessor ?? $captured; |
621
|
|
|
break; |
622
|
|
|
|
623
|
|
|
// Backtick may be encountered in two different contexts: normal inline context, in which case it has |
624
|
|
|
// the same meaning as any quote and causes sequencing of a quoted string. Or protected context, in |
625
|
|
|
// which case it also sequences a quoted node but appends the result instead of assigning to array. |
626
|
|
|
// Note that backticks do not support escapes (they are a new feature that does not require escaping). |
627
|
|
|
case self::BYTE_BACKTICK: |
628
|
|
|
if ($this->splitter->context->context === Context::CONTEXT_PROTECTED) { |
629
|
|
|
$node->addChild(new TextNode($text)); |
630
|
|
|
$node->addChild($this->sequenceQuotedNode()->flatten()); |
631
|
|
|
$text = ''; |
632
|
|
|
break; |
633
|
|
|
} |
634
|
|
|
// Fallthrough is intentional: if not in protected context, consider the backtick a normal quote. |
635
|
|
|
|
636
|
|
|
// Case not normally countered in straight up "inline" context, but when encountered, means we have |
637
|
|
|
// explicitly found a quoted array key - and we extract it. |
638
|
|
|
case self::BYTE_QUOTE_SINGLE: |
639
|
|
|
case self::BYTE_QUOTE_DOUBLE: |
640
|
|
|
if (!$allowArray) { |
641
|
|
|
$text .= chr($symbol); |
642
|
|
|
break; |
643
|
|
|
} |
644
|
|
|
if ($key !== null) { |
645
|
|
|
$arguments[$key] = $this->sequenceQuotedNode($countedEscapes)->flatten(true); |
646
|
|
|
$key = null; |
647
|
|
|
} else { |
648
|
|
|
$key = $this->sequenceQuotedNode($countedEscapes)->flatten(true); |
649
|
|
|
} |
650
|
|
|
$countedEscapes = 0; |
651
|
|
|
$isArray = $allowArray; |
652
|
|
|
break; |
653
|
|
|
|
654
|
|
|
case self::BYTE_SEPARATOR_COMMA: |
655
|
|
|
if (!$allowArray) { |
656
|
|
|
$text .= ','; |
657
|
|
|
break; |
658
|
|
|
} |
659
|
|
|
if ($captured !== null) { |
660
|
|
|
$arguments[$key ?? $captured] = new ObjectAccessorNode($captured); |
661
|
|
|
} |
662
|
|
|
$key = null; |
663
|
|
|
$isArray = $allowArray; |
664
|
|
|
break; |
665
|
|
|
|
666
|
|
|
case self::BYTE_SEPARATOR_EQUALS: |
667
|
|
|
$text .= '='; |
668
|
|
|
if (!$allowArray) { |
669
|
|
|
$node = new RootNode(); |
670
|
|
|
$this->splitter->switch($this->contexts->protected); |
671
|
|
|
break; |
672
|
|
|
} |
673
|
|
|
$key = $captured; |
674
|
|
|
$isArray = $allowArray; |
675
|
|
|
break; |
676
|
|
|
|
677
|
|
|
case self::BYTE_SEPARATOR_COLON: |
678
|
|
|
$text .= ':'; |
679
|
|
|
$hasColon = true; |
680
|
|
|
$namespace = $captured; |
681
|
|
|
$key = $key ?? $captured; |
682
|
|
|
$isArray = $isArray || ($allowArray && is_numeric($key)); |
683
|
|
|
if ($captured !== null) { |
684
|
|
|
$parts[] = $captured; |
685
|
|
|
} |
686
|
|
|
$parts[] = ':'; |
687
|
|
|
break; |
688
|
|
|
|
689
|
|
|
case self::BYTE_WHITESPACE_SPACE: |
690
|
|
|
case self::BYTE_WHITESPACE_EOL: |
691
|
|
|
case self::BYTE_WHITESPACE_RETURN: |
692
|
|
|
case self::BYTE_WHITESPACE_TAB: |
693
|
|
|
// If we already collected some whitespace we must enter protected context. |
694
|
|
|
$text .= $this->source->source[$this->splitter->index - 1]; |
695
|
|
|
|
696
|
|
|
if ($captured !== null) { |
697
|
|
|
// Store a captured part: a whitespace inside inline syntax will engage the expression matching |
698
|
|
|
// that occurs when the node is closed. Allows matching the various parts to create the appropriate |
699
|
|
|
// node type. |
700
|
|
|
$parts[] = $captured; |
701
|
|
|
} |
702
|
|
|
|
703
|
|
|
if ($hasWhitespace && !$hasPass && !$allowArray) { |
704
|
|
|
// Protection mode: this very limited context does not allow tags or inline syntax, and will |
705
|
|
|
// protect things like CSS and JS - and will only enter a more reactive context if encountering |
706
|
|
|
// the backtick character, meaning a quoted string will be sequenced. This backtick-quoted |
707
|
|
|
// string can then contain inline syntax like variable accessors. |
708
|
|
|
$node = $node ?? new RootNode(); |
709
|
|
|
$this->splitter->switch($this->contexts->protected); |
710
|
|
|
break; |
711
|
|
|
} |
712
|
|
|
|
713
|
|
|
if ($captured === 'namespace') { |
714
|
|
|
// Special case: we catch namespace definitions with {namespace xyz=foo} syntax here, although |
715
|
|
|
// the proper way with current code is to use {@namespace xyz=foo}. We have this case here since |
716
|
|
|
// it is relatively cheap (only happens when we see a space inside inline and a straight-up |
717
|
|
|
// string comparison with strict types enabled). We then return an empty TextNode which is |
718
|
|
|
// ignored by the parent node when attached so we don't create any output. |
719
|
|
|
$this->sequenceToggleInstruction('namespace'); |
720
|
|
|
$this->splitter->switch($restore); |
721
|
|
|
return new TextNode(''); |
722
|
|
|
} |
723
|
|
|
$key = $key ?? $captured; |
724
|
|
|
$hasWhitespace = true; |
725
|
|
|
$isArray = $allowArray && ($hasColon ?? $isArray ?? is_numeric($captured)); |
726
|
|
|
$potentialAccessor = ($potentialAccessor ?? $captured); |
727
|
|
|
break; |
728
|
|
|
|
729
|
|
|
case self::BYTE_TAG_END: |
730
|
|
|
case self::BYTE_PIPE: |
731
|
|
|
// If there is an accessor on the left side of the pipe and $node is not defined, we create $node |
732
|
|
|
// as an object accessor. If $node already exists we do nothing (and expect the VH trigger, the |
733
|
|
|
// parenthesis start case below, to add $node as childnode and create a new $node). |
734
|
|
|
$hasPass = true; |
735
|
|
|
$isArray = $allowArray; |
736
|
|
|
$callDetected = false; |
737
|
|
|
|
738
|
|
|
$text .= $this->source->source[$this->splitter->index - 1]; |
739
|
|
|
$node = $node ?? new ObjectAccessorNode(); |
740
|
|
|
if ($potentialAccessor ?? $captured) { |
741
|
|
|
$node->addChild(new TextNode($potentialAccessor . $captured)); |
742
|
|
|
} |
743
|
|
|
|
744
|
|
|
$potentialAccessor = $namespace = $method = $key = null; |
|
|
|
|
745
|
|
|
break; |
746
|
|
|
|
747
|
|
|
case self::BYTE_PARENTHESIS_START: |
748
|
|
|
$isArray = false; |
749
|
|
|
// Special case: if a parenthesis start was preceded by whitespace but had no pass operator we are |
750
|
|
|
// not dealing with a ViewHelper call and will continue the sequencing, grabbing the parenthesis as |
751
|
|
|
// part of the expression. |
752
|
|
|
$text .= '('; |
753
|
|
|
if (!$hasColon || ($hasWhitespace && !$hasPass)) { |
|
|
|
|
754
|
|
|
$this->splitter->switch($this->contexts->protected); |
755
|
|
|
$namespace = $method = null; |
|
|
|
|
756
|
|
|
break; |
757
|
|
|
} |
758
|
|
|
|
759
|
|
|
$callDetected = true; |
760
|
|
|
$method = $captured; |
761
|
|
|
$childNodeToAdd = $node; |
762
|
|
|
try { |
763
|
|
|
$node = $this->resolver->createViewHelperInstance($namespace, $method); |
764
|
|
|
$arguments = $node->getArguments(); |
765
|
|
|
$node = $this->callInterceptor($node, self::INTERCEPT_OPENING_VIEWHELPER); |
766
|
|
|
} catch (\TYPO3Fluid\Fluid\Core\Exception $exception) { |
767
|
|
|
throw $this->createErrorAtPosition($exception->getMessage(), $exception->getCode()); |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
$this->sequenceArrayNode($arguments); |
771
|
|
|
$arguments->setRenderingContext($this->renderingContext)->validate(); |
772
|
|
|
$node = $node->onOpen($this->renderingContext); |
773
|
|
|
|
774
|
|
|
if ($childNodeToAdd) { |
775
|
|
|
if ($childNodeToAdd instanceof ObjectAccessorNode) { |
776
|
|
|
$childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_OBJECTACCESSOR); |
777
|
|
|
} |
778
|
|
|
$node->addChild($childNodeToAdd); |
779
|
|
|
} |
780
|
|
|
$node = $node->onClose($this->renderingContext); |
781
|
|
|
$text .= ')'; |
782
|
|
|
$potentialAccessor = null; |
783
|
|
|
break; |
784
|
|
|
|
785
|
|
|
case self::BYTE_INLINE_END: |
786
|
|
|
$text .= '}'; |
787
|
|
|
|
788
|
|
|
if (--$ignoredEndingBraces >= 0) { |
789
|
|
|
if ($captured !== null) { |
790
|
|
|
$node->addChild(new TextNode('{' . $captured . '}')); |
791
|
|
|
} |
792
|
|
|
break; |
793
|
|
|
} |
794
|
|
|
|
795
|
|
|
if ($text === '{}') { |
796
|
|
|
// Edge case handling of empty JS objects |
797
|
|
|
return new TextNode('{}'); |
798
|
|
|
} |
799
|
|
|
|
800
|
|
|
$isArray = $allowArray && ($isArray ?: ($hasColon && !$hasPass && !$callDetected)); |
801
|
|
|
$potentialAccessor .= $captured; |
802
|
|
|
$interceptionPoint = self::INTERCEPT_OBJECTACCESSOR; |
803
|
|
|
|
804
|
|
|
// Decision: if we did not detect a ViewHelper we match the *entire* expression, from the cached |
805
|
|
|
// starting index, to see if it matches a known type of expression. If it does, we must return the |
806
|
|
|
// appropriate type of ExpressionNode. |
807
|
|
|
if ($isArray) { |
808
|
|
|
if ($captured !== null) { |
809
|
|
|
$arguments[$key ?? $captured] = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
810
|
|
|
} |
811
|
|
|
$this->splitter->switch($restore); |
812
|
|
|
return $arguments->toArrayNode(); |
813
|
|
|
} elseif ($callDetected) { |
814
|
|
|
// The first-priority check is for a ViewHelper used right before the inline expression ends, |
815
|
|
|
// in which case there is no further syntax to come. |
816
|
|
|
$arguments->validate(); |
817
|
|
|
$node = $node->onOpen($this->renderingContext)->onClose($this->renderingContext); |
818
|
|
|
$interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER; |
819
|
|
|
} elseif ($this->splitter->context->context === Context::CONTEXT_PROTECTED || ($hasWhitespace && !$callDetected && !$hasPass)) { |
820
|
|
|
// In order to qualify for potentially being an expression, the entire inline node must contain |
821
|
|
|
// whitespace, must not contain parenthesis, must not contain a colon and must not contain an |
822
|
|
|
// inline pass operand. This significantly limits the number of times this (expensive) routine |
823
|
|
|
// has to be executed. |
824
|
|
|
$parts[] = $captured; |
825
|
|
|
try { |
826
|
|
|
foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) { |
827
|
|
|
if ($expressionNodeTypeClassName::matches($parts)) { |
828
|
|
|
$childNodeToAdd = new $expressionNodeTypeClassName($parts); |
829
|
|
|
$childNodeToAdd = $this->callInterceptor($childNodeToAdd, self::INTERCEPT_EXPRESSION); |
830
|
|
|
break; |
831
|
|
|
} |
832
|
|
|
} |
833
|
|
|
} catch (ExpressionException $exception) { |
834
|
|
|
// ErrorHandler will either return a string or throw the exception anew, depending on the |
835
|
|
|
// exact implementation of ErrorHandlerInterface. When it returns a string we use that as |
836
|
|
|
// text content of a new TextNode so the message is output as part of the rendered result. |
837
|
|
|
$childNodeToAdd = new TextNode( |
838
|
|
|
$this->renderingContext->getErrorHandler()->handleExpressionError($exception) |
839
|
|
|
); |
840
|
|
|
} |
841
|
|
|
$node = $childNodeToAdd ?? ($node ?? new RootNode())->addChild(new TextNode($text)); |
842
|
|
|
return $node; |
843
|
|
|
} elseif (!$hasPass && !$callDetected) { |
844
|
|
|
$node = $node ?? new ObjectAccessorNode(); |
845
|
|
|
if ($potentialAccessor !== '') { |
846
|
|
|
$node->addChild(new TextNode((string) $potentialAccessor)); |
847
|
|
|
} |
848
|
|
|
} elseif ($hasPass && $this->resolver->isAliasRegistered((string) $potentialAccessor)) { |
849
|
|
|
// Fourth priority check is for a pass to a ViewHelper alias, e.g. "{value | raw}" in which case |
850
|
|
|
// we look for the alias used and create a ViewHelper with no arguments. |
851
|
|
|
$childNodeToAdd = $node; |
852
|
|
|
$node = $this->resolver->createViewHelperInstance(null, (string) $potentialAccessor); |
853
|
|
|
$arguments = $node->getArguments()->validate()->setRenderingContext($this->renderingContext); |
|
|
|
|
854
|
|
|
$node = $node->onOpen($this->renderingContext); |
855
|
|
|
$node->addChild($childNodeToAdd); |
856
|
|
|
$node->onClose( |
857
|
|
|
$this->renderingContext |
858
|
|
|
); |
859
|
|
|
$interceptionPoint = self::INTERCEPT_SELFCLOSING_VIEWHELPER; |
860
|
|
|
} else { |
861
|
|
|
# TODO: should this be an error case, or should it result in a TextNode? |
862
|
|
|
throw $this->createErrorAtPosition( |
863
|
|
|
'Invalid inline syntax - not accessor, not expression, not array, not ViewHelper, but ' . |
864
|
|
|
'contains the tokens used by these in a sequence that is not valid Fluid syntax. You can ' . |
865
|
|
|
'most likely avoid this by adding whitespace inside the curly braces before the first ' . |
866
|
|
|
'Fluid-like symbol in the expression. Symbols recognized as Fluid are: "' . |
867
|
|
|
addslashes(implode('","', array_map('chr', $this->contexts->inline->bytes))) . '"', |
868
|
|
|
1558782228 |
869
|
|
|
); |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
$node = $this->callInterceptor($node, $interceptionPoint); |
873
|
|
|
$this->splitter->switch($restore); |
874
|
|
|
return $node; |
875
|
|
|
} |
876
|
|
|
} |
877
|
|
|
|
878
|
|
|
// See note in sequenceTagNode() end of method body. TL;DR: this is intentionally here instead of as "default" |
879
|
|
|
// case in the switch above for a very specific reason: the case is only encountered if seeing EOF before the |
880
|
|
|
// inline expression was closed. |
881
|
|
|
throw $this->createErrorAtPosition('Unterminated inline syntax', 1557838506); |
882
|
|
|
} |
883
|
|
|
|
884
|
|
|
protected function sequenceBooleanNode(int $leadingEscapes = 0): BooleanNode |
885
|
|
|
{ |
886
|
|
|
$startingByte = $this->source->bytes[$this->splitter->index]; |
887
|
|
|
$closingByte = $startingByte === self::BYTE_PARENTHESIS_START ? self::BYTE_PARENTHESIS_END : $startingByte; |
888
|
|
|
$countedEscapes = 0; |
889
|
|
|
$node = new BooleanNode(); |
890
|
|
|
$restore = $this->splitter->switch($this->contexts->boolean); |
891
|
|
|
$this->sequence->next(); |
892
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
893
|
|
|
if ($captured !== null) { |
894
|
|
|
$node->addChild(new TextNode($captured)); |
895
|
|
|
} |
896
|
|
|
switch ($symbol) { |
897
|
|
|
case self::BYTE_INLINE: |
898
|
|
|
$node->addChild($this->sequenceInlineNodes(true)); |
899
|
|
|
break; |
900
|
|
|
|
901
|
|
|
case self::BYTE_QUOTE_DOUBLE: |
902
|
|
|
case self::BYTE_QUOTE_SINGLE: |
903
|
|
|
if ($symbol === $closingByte && $countedEscapes === $leadingEscapes) { |
904
|
|
|
$this->splitter->switch($restore); |
905
|
|
|
return $node; |
906
|
|
|
} |
907
|
|
|
// Sequence a quoted node and set the "quoted" flag on the resulting root node (which is not |
908
|
|
|
// flattened even if it contains a single child). This allows the BooleanNode to enforce a string |
909
|
|
|
// value whenever parts of the expression are quoted, indicating user explicitly wants string type. |
910
|
|
|
$node->addChild($this->sequenceQuotedNode($countedEscapes)->setQuoted(true)); |
911
|
|
|
break; |
912
|
|
|
|
913
|
|
|
case self::BYTE_PARENTHESIS_START: |
914
|
|
|
$node->addChild($this->sequenceBooleanNode()); |
915
|
|
|
break; |
916
|
|
|
|
917
|
|
|
case self::BYTE_PARENTHESIS_END: |
918
|
|
|
$this->splitter->switch($restore); |
919
|
|
|
return $node; |
920
|
|
|
|
921
|
|
|
case self::BYTE_WHITESPACE_SPACE: |
922
|
|
|
case self::BYTE_WHITESPACE_TAB: |
923
|
|
|
case self::BYTE_WHITESPACE_RETURN: |
924
|
|
|
case self::BYTE_WHITESPACE_EOL: |
925
|
|
|
break; |
926
|
|
|
|
927
|
|
|
case self::BYTE_BACKSLASH: |
928
|
|
|
++$countedEscapes; |
929
|
|
|
break; |
930
|
|
|
} |
931
|
|
|
if ($symbol !== self::BYTE_BACKSLASH) { |
932
|
|
|
$countedEscapes = 0; |
933
|
|
|
} |
934
|
|
|
} |
935
|
|
|
throw $this->createErrorAtPosition('Unterminated boolean expression', 1564159986); |
936
|
|
|
} |
937
|
|
|
|
938
|
|
|
protected function sequenceArrayNode(\ArrayAccess &$array, bool $numeric = false): void |
939
|
|
|
{ |
940
|
|
|
$definitions = null; |
941
|
|
|
if ($array instanceof ArgumentCollection) { |
942
|
|
|
$definitions = $array->getDefinitions(); |
943
|
|
|
} |
944
|
|
|
|
945
|
|
|
$keyOrValue = null; |
946
|
|
|
$key = null; |
947
|
|
|
$value = null; |
|
|
|
|
948
|
|
|
$itemCount = -1; |
949
|
|
|
$countedEscapes = 0; |
950
|
|
|
$escapingEnabledBackup = $this->escapingEnabled; |
951
|
|
|
|
952
|
|
|
$restore = $this->splitter->switch($this->contexts->array); |
953
|
|
|
$this->sequence->next(); |
954
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
955
|
|
|
switch ($symbol) { |
956
|
|
|
case self::BYTE_SEPARATOR_COLON: |
957
|
|
|
case self::BYTE_SEPARATOR_EQUALS: |
958
|
|
|
// Colon or equals has same meaning (which allows tag syntax as argument syntax). Encountering this |
959
|
|
|
// byte always means the preceding byte was a key. However, if nothing was captured before this, |
960
|
|
|
// it means colon or equals was used without a key which is a syntax error. |
961
|
|
|
$key = $key ?? $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null); |
962
|
|
|
if ($key === null) { |
963
|
|
|
throw $this->createErrorAtPosition('Unexpected colon or equals sign, no preceding key', 1559250839); |
964
|
|
|
} |
965
|
|
|
if ($definitions !== null && !$numeric && !isset($definitions[$key])) { |
966
|
|
|
throw $this->createUnsupportedArgumentError((string)$key, $definitions); |
967
|
|
|
} |
968
|
|
|
break; |
969
|
|
|
|
970
|
|
|
case self::BYTE_ARRAY_START: |
971
|
|
|
case self::BYTE_INLINE: |
972
|
|
|
// Minimal safeguards to improve error feedback. Theoretically such "garbage" could simply be ignored |
973
|
|
|
// without causing problems to the parser, but it is probably best to report it as it could indicate |
974
|
|
|
// the user expected X value but gets Y and doesn't notice why. |
975
|
|
|
if ($captured !== null) { |
976
|
|
|
throw $this->createErrorAtPosition('Unexpected content before array/inline start in associative array, ASCII: ' . ord($captured), 1559131849); |
977
|
|
|
} |
978
|
|
|
if ($key === null && !$numeric) { |
979
|
|
|
throw $this->createErrorAtPosition('Unexpected array/inline start in associative array without preceding key', 1559131848); |
980
|
|
|
} |
981
|
|
|
|
982
|
|
|
// Encountering a curly brace or square bracket start byte will both cause a sub-array to be sequenced, |
983
|
|
|
// the difference being that only the square bracket will cause third parameter ($numeric) passed to |
984
|
|
|
// sequenceArrayNode() to be true, which in turn causes key-less items to be added with numeric indexes. |
985
|
|
|
$key = $key ?? ++$itemCount; |
986
|
|
|
$arrayNode = new ArrayNode(); |
987
|
|
|
$this->sequenceArrayNode($arrayNode, $symbol === self::BYTE_ARRAY_START); |
988
|
|
|
$array[$key] = $arrayNode; |
989
|
|
|
$keyOrValue = null; |
990
|
|
|
$key = null; |
991
|
|
|
break; |
992
|
|
|
|
993
|
|
|
case self::BYTE_QUOTE_SINGLE: |
994
|
|
|
case self::BYTE_QUOTE_DOUBLE: |
995
|
|
|
// Safeguard: if anything is captured before a quote this indicates garbage leading content. As with |
996
|
|
|
// the garbage safeguards above, this one could theoretically be ignored in favor of silently making |
997
|
|
|
// the odd syntax "just work". |
998
|
|
|
if ($captured !== null) { |
999
|
|
|
throw $this->createErrorAtPosition('Unexpected content before quote start in associative array, ASCII: ' . ord($captured), 1559145560); |
1000
|
|
|
} |
1001
|
|
|
|
1002
|
|
|
// Quotes will always cause sequencing of the quoted string, but differs in behavior based on whether |
1003
|
|
|
// or not the $key is set. If $key is set, we know for sure we can assign a value. If it is not set |
1004
|
|
|
// we instead leave $keyOrValue defined so this will be processed by one of the next iterations. |
1005
|
|
|
if (isset($key, $definitions[$key]) && $definitions[$key]->getType() === 'boolean') { |
1006
|
|
|
$keyOrValue = $this->sequenceBooleanNode($countedEscapes); |
1007
|
|
|
} else { |
1008
|
|
|
$keyOrValue = $this->sequenceQuotedNode($countedEscapes); |
1009
|
|
|
} |
1010
|
|
|
if ($key !== null) { |
1011
|
|
|
$array[$key] = $keyOrValue->flatten(true); |
1012
|
|
|
$keyOrValue = null; |
1013
|
|
|
$key = null; |
1014
|
|
|
$countedEscapes = 0; |
1015
|
|
|
} |
1016
|
|
|
break; |
1017
|
|
|
|
1018
|
|
|
case self::BYTE_SEPARATOR_COMMA: |
1019
|
|
|
// Comma separator: if we've collected a key or value, use it. Otherwise, use captured string. |
1020
|
|
|
// If neither key nor value nor captured string exists, ignore the comma (likely a tailing comma). |
1021
|
|
|
$value = null; |
1022
|
|
|
if ($keyOrValue !== null) { |
1023
|
|
|
// Key or value came as quoted string and exists in $keyOrValue |
1024
|
|
|
$potentialValue = $keyOrValue->flatten(true); |
1025
|
|
|
$key = $numeric ? ++$itemCount : $potentialValue; |
1026
|
|
|
$value = $numeric ? $potentialValue : (is_numeric($key) ? $key + 0 : new ObjectAccessorNode((string) $key)); |
1027
|
|
|
} elseif ($captured !== null) { |
1028
|
|
|
$key = $key ?? ($numeric ? ++$itemCount : $captured); |
1029
|
|
|
if (!$numeric && $definitions !== null && !isset($definitions[$key])) { |
1030
|
|
|
throw $this->createUnsupportedArgumentError((string)$key, $definitions); |
1031
|
|
|
} |
1032
|
|
|
$value = is_numeric($captured) ? $captured + 0 : new ObjectAccessorNode($captured); |
1033
|
|
|
} |
1034
|
|
|
if ($value !== null) { |
1035
|
|
|
$array[$key] = $value; |
1036
|
|
|
} |
1037
|
|
|
$keyOrValue = null; |
1038
|
|
|
$value = null; |
|
|
|
|
1039
|
|
|
$key = null; |
1040
|
|
|
break; |
1041
|
|
|
|
1042
|
|
|
case self::BYTE_WHITESPACE_TAB: |
1043
|
|
|
case self::BYTE_WHITESPACE_RETURN: |
1044
|
|
|
case self::BYTE_WHITESPACE_EOL: |
1045
|
|
|
case self::BYTE_WHITESPACE_SPACE: |
1046
|
|
|
// Any whitespace attempts to set the key, if not already set. The captured string may be null as |
1047
|
|
|
// well, leaving the $key variable still null and able to be coalesced. |
1048
|
|
|
$key = $key ?? $captured; |
1049
|
|
|
break; |
1050
|
|
|
|
1051
|
|
|
case self::BYTE_BACKSLASH: |
1052
|
|
|
// Escapes are simply counted and passed to the sequenceQuotedNode() method, causing that method |
1053
|
|
|
// to ignore exactly this number of backslashes before a matching quote is seen as closing quote. |
1054
|
|
|
++$countedEscapes; |
1055
|
|
|
break; |
1056
|
|
|
|
1057
|
|
|
case self::BYTE_INLINE_END: |
1058
|
|
|
case self::BYTE_ARRAY_END: |
1059
|
|
|
case self::BYTE_PARENTHESIS_END: |
1060
|
|
|
// Array end indication. Check if anything was collected previously or was captured currently, |
1061
|
|
|
// assign that to the array and return an ArrayNode with the full array inside. |
1062
|
|
|
$captured = $captured ?? ($keyOrValue !== null ? $keyOrValue->flatten(true) : null); |
1063
|
|
|
$key = $key ?? ($numeric ? ++$itemCount : $captured); |
1064
|
|
|
if (isset($captured, $key)) { |
1065
|
|
|
if (is_numeric($captured)) { |
1066
|
|
|
$array[$key] = $captured + 0; |
1067
|
|
|
} elseif ($keyOrValue !== null) { |
1068
|
|
|
$array[$key] = $keyOrValue->flatten(); |
1069
|
|
|
} else { |
1070
|
|
|
$array[$key] = new ObjectAccessorNode((string) ($captured ?? $key)); |
1071
|
|
|
} |
1072
|
|
|
} |
1073
|
|
|
if (!$numeric && isset($key, $definitions) && !isset($definitions[$key])) { |
1074
|
|
|
throw $this->createUnsupportedArgumentError((string)$key, $definitions); |
1075
|
|
|
} |
1076
|
|
|
$this->escapingEnabled = $escapingEnabledBackup; |
1077
|
|
|
$this->splitter->switch($restore); |
1078
|
|
|
return; |
1079
|
|
|
} |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
throw $this->createErrorAtPosition( |
1083
|
|
|
'Unterminated array', |
1084
|
|
|
1557748574 |
1085
|
|
|
); |
1086
|
|
|
} |
1087
|
|
|
|
1088
|
|
|
/** |
1089
|
|
|
* Sequence a quoted value |
1090
|
|
|
* |
1091
|
|
|
* The return can be either of: |
1092
|
|
|
* |
1093
|
|
|
* 1. A string value if source was for example "string" |
1094
|
|
|
* 2. An integer if source was for example "1" |
1095
|
|
|
* 3. A float if source was for example "1.25" |
1096
|
|
|
* 4. A RootNode instance with multiple child nodes if source was for example "string {var}" |
1097
|
|
|
* |
1098
|
|
|
* The idea is to return the raw value if there is no reason for it to |
1099
|
|
|
* be a node as such - which is only necessary if the quoted expression |
1100
|
|
|
* contains other (dynamic) values like an inline syntax. |
1101
|
|
|
* |
1102
|
|
|
* @param int $leadingEscapes A backwards compatibility measure: when passed, this number of escapes must precede a closing quote for it to trigger node closing. |
1103
|
|
|
* @return RootNode |
1104
|
|
|
*/ |
1105
|
|
|
protected function sequenceQuotedNode(int $leadingEscapes = 0): RootNode |
1106
|
|
|
{ |
1107
|
|
|
$startingByte = $this->source->bytes[$this->splitter->index]; |
1108
|
|
|
$node = new RootNode(); |
1109
|
|
|
$countedEscapes = 0; |
1110
|
|
|
|
1111
|
|
|
$contextToRestore = $this->splitter->switch($this->contexts->quoted); |
1112
|
|
|
$this->sequence->next(); |
1113
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
1114
|
|
|
switch ($symbol) { |
1115
|
|
|
|
1116
|
|
|
case self::BYTE_ARRAY_START: |
1117
|
|
|
$countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored) |
1118
|
|
|
if ($captured === null) { |
1119
|
|
|
// Array start "[" only triggers array sequencing if it is the very first byte in the quoted |
1120
|
|
|
// string - otherwise, it is added as part of the text. |
1121
|
|
|
$child = new ArrayNode(); |
1122
|
|
|
$this->sequenceArrayNode($child, true); |
1123
|
|
|
$node->addChild($child); |
|
|
|
|
1124
|
|
|
} else { |
1125
|
|
|
$node->addChild(new TextNode($captured . '[')); |
1126
|
|
|
} |
1127
|
|
|
break; |
1128
|
|
|
|
1129
|
|
|
case self::BYTE_INLINE: |
1130
|
|
|
$countedEscapes = 0; // Theoretically not required but done in case of stray escapes (gets ignored) |
1131
|
|
|
// The quoted string contains a sub-expression. We extract the captured content so far and if it |
1132
|
|
|
// is not an empty string, add it as a child of the RootNode we're building, then we add the inline |
1133
|
|
|
// expression as next sibling and continue the loop. |
1134
|
|
|
if ($captured !== null) { |
1135
|
|
|
$childNode = new TextNode($captured); |
1136
|
|
|
$node->addChild($childNode); |
1137
|
|
|
} |
1138
|
|
|
|
1139
|
|
|
$node->addChild($this->sequenceInlineNodes()); |
1140
|
|
|
break; |
1141
|
|
|
|
1142
|
|
|
case self::BYTE_BACKSLASH: |
1143
|
|
|
$next = $this->source->bytes[$this->splitter->index + 1] ?? null; |
1144
|
|
|
++$countedEscapes; |
1145
|
|
|
if ($next === $startingByte || $next === self::BYTE_BACKSLASH) { |
1146
|
|
|
if ($captured !== null) { |
1147
|
|
|
$node->addChild(new TextNode($captured)); |
1148
|
|
|
} |
1149
|
|
|
} else { |
1150
|
|
|
$node->addChild(new TextNode($captured . str_repeat('\\', $countedEscapes))); |
1151
|
|
|
$countedEscapes = 0; |
1152
|
|
|
} |
1153
|
|
|
break; |
1154
|
|
|
|
1155
|
|
|
// Note: although "case $startingByte:" could have been used here, it would not compile the switch |
1156
|
|
|
// as a hash map and thus would not perform as well overall - when called frequently as it will be. |
1157
|
|
|
// Backtick will only be encountered if the context is "protected" (insensitive inline sequencing) |
1158
|
|
|
case self::BYTE_QUOTE_SINGLE: |
1159
|
|
|
case self::BYTE_QUOTE_DOUBLE: |
1160
|
|
|
case self::BYTE_BACKTICK: |
1161
|
|
|
if ($symbol !== $startingByte || $countedEscapes !== $leadingEscapes) { |
1162
|
|
|
$childNode = new TextNode($captured . chr($symbol)); |
1163
|
|
|
$node->addChild($childNode); |
1164
|
|
|
$countedEscapes = 0; // If number of escapes do not match expected, reset the counter |
1165
|
|
|
break; |
1166
|
|
|
} |
1167
|
|
|
if ($captured !== null) { |
1168
|
|
|
$childNode = new TextNode($captured); |
1169
|
|
|
$node->addChild($childNode); |
1170
|
|
|
} |
1171
|
|
|
$this->splitter->switch($contextToRestore); |
1172
|
|
|
return $node; |
1173
|
|
|
} |
1174
|
|
|
} |
1175
|
|
|
|
1176
|
|
|
throw $this->createErrorAtPosition('Unterminated expression inside quotes', 1557700793); |
1177
|
|
|
} |
1178
|
|
|
|
1179
|
|
|
/** |
1180
|
|
|
* Dead-end sequencing; if parsing is switched off it cannot be switched on again, |
1181
|
|
|
* and the remainder of the template source must be sequenced as dead text. |
1182
|
|
|
* |
1183
|
|
|
* @return string|null |
1184
|
|
|
*/ |
1185
|
|
|
protected function sequenceRemainderAsText(): ?string |
1186
|
|
|
{ |
1187
|
|
|
$this->sequence->next(); |
1188
|
|
|
$this->splitter->switch($this->contexts->empty); |
1189
|
|
|
$source = null; |
1190
|
|
|
foreach ($this->sequence as $symbol => $captured) { |
1191
|
|
|
$source .= $captured; |
1192
|
|
|
} |
1193
|
|
|
return $source; |
1194
|
|
|
} |
1195
|
|
|
|
1196
|
|
|
/** |
1197
|
|
|
* Call all interceptors registered for a given interception point. |
1198
|
|
|
* |
1199
|
|
|
* @param ComponentInterface $node The syntax tree node which can be modified by the interceptors. |
1200
|
|
|
* @param integer $interceptorPosition the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\self::INTERCEPT_* constants. |
1201
|
|
|
* @return ComponentInterface |
1202
|
|
|
*/ |
1203
|
|
|
protected function callInterceptor(ComponentInterface $node, int $interceptorPosition): ComponentInterface |
1204
|
|
|
{ |
1205
|
|
|
if (!$this->escapingEnabled) { |
1206
|
|
|
// Escaping is either explicitly disabled (for example, we may be parsing arguments) or there are |
1207
|
|
|
// at least 2 preceding ViewHelpers which have disabled child escaping. The reason for checking >1 |
1208
|
|
|
// instead of >0 is because escaping must happen if the value is decremented to zero, which is not |
1209
|
|
|
// possible unless the value is >1 to begin with. This early return avoids entering the switch and |
1210
|
|
|
// conditions below when they are simply not necessary to consult. |
1211
|
|
|
return $node; |
1212
|
|
|
} |
1213
|
|
|
|
1214
|
|
|
switch ($interceptorPosition) { |
1215
|
|
|
case self::INTERCEPT_OPENING_VIEWHELPER: |
1216
|
|
|
if (!$node->isChildrenEscapingEnabled()) { |
1217
|
|
|
++$this->viewHelperNodesWhichDisableTheInterceptor; |
1218
|
|
|
} |
1219
|
|
|
break; |
1220
|
|
|
|
1221
|
|
|
case self::INTERCEPT_CLOSING_VIEWHELPER: |
1222
|
|
|
if (!$node->isChildrenEscapingEnabled()) { |
1223
|
|
|
--$this->viewHelperNodesWhichDisableTheInterceptor; |
1224
|
|
|
} |
1225
|
|
|
if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) { |
1226
|
|
|
$node = new EscapingNode($node); |
1227
|
|
|
} |
1228
|
|
|
break; |
1229
|
|
|
|
1230
|
|
|
case self::INTERCEPT_SELFCLOSING_VIEWHELPER: |
1231
|
|
|
if ($this->viewHelperNodesWhichDisableTheInterceptor === 0 && $node->isOutputEscapingEnabled()) { |
1232
|
|
|
$node = new EscapingNode($node); |
1233
|
|
|
} |
1234
|
|
|
break; |
1235
|
|
|
|
1236
|
|
|
case self::INTERCEPT_OBJECTACCESSOR: |
1237
|
|
|
case self::INTERCEPT_EXPRESSION: |
1238
|
|
|
if ($this->viewHelperNodesWhichDisableTheInterceptor === 0) { |
1239
|
|
|
$node = new EscapingNode($node); |
1240
|
|
|
} |
1241
|
|
|
break; |
1242
|
|
|
} |
1243
|
|
|
return $node; |
1244
|
|
|
} |
1245
|
|
|
|
1246
|
|
|
/** |
1247
|
|
|
* Creates a dump, starting from the first line break before $position, |
1248
|
|
|
* to the next line break from $position, counting the lines and characters |
1249
|
|
|
* and inserting a marker pointing to the exact offending character. |
1250
|
|
|
* |
1251
|
|
|
* Is not very efficient - but adds bug tracing information. Should only |
1252
|
|
|
* be called when exceptions are raised during sequencing. |
1253
|
|
|
* |
1254
|
|
|
* @param int $index |
1255
|
|
|
* @return string |
1256
|
|
|
*/ |
1257
|
|
|
protected function extractSourceDumpOfLineAtPosition(int $index): string |
1258
|
|
|
{ |
1259
|
|
|
$lines = $this->countCharactersMatchingMask(self::MASK_LINEBREAKS, 1, $index) + 1; |
1260
|
|
|
$offset = $this->findBytePositionBeforeOffset(self::MASK_LINEBREAKS, $index); |
1261
|
|
|
$line = substr( |
1262
|
|
|
$this->source->source, |
1263
|
|
|
$offset, |
1264
|
|
|
$this->findBytePositionAfterOffset(self::MASK_LINEBREAKS, $index) |
1265
|
|
|
); |
1266
|
|
|
$character = $index - $offset - 1; |
1267
|
|
|
$string = 'Line ' . $lines . ' character ' . $character . PHP_EOL; |
1268
|
|
|
$string .= PHP_EOL; |
1269
|
|
|
$string .= str_repeat(' ', max($character, 0)) . 'v' . PHP_EOL; |
1270
|
|
|
$string .= trim($line) . PHP_EOL; |
1271
|
|
|
$string .= str_repeat(' ', max($character, 0)) . '^' . PHP_EOL; |
1272
|
|
|
return $string; |
1273
|
|
|
} |
1274
|
|
|
|
1275
|
|
|
protected function createErrorAtPosition(string $message, int $code): SequencingException |
1276
|
|
|
{ |
1277
|
|
|
$error = new SequencingException($message, $code); |
1278
|
|
|
$error->setExcerpt($this->extractSourceDumpOfLineAtPosition($this->splitter->index)); |
1279
|
|
|
$error->setByte($this->source->bytes[$this->splitter->index] ?? 0); |
1280
|
|
|
return $error; |
1281
|
|
|
} |
1282
|
|
|
|
1283
|
|
|
protected function createUnsupportedArgumentError(string $argument, array $definitions): SequencingException |
1284
|
|
|
{ |
1285
|
|
|
return $this->createErrorAtPosition( |
1286
|
|
|
sprintf( |
1287
|
|
|
'Undeclared argument: %s. Valid arguments are: %s', |
1288
|
|
|
$argument, |
1289
|
|
|
implode(', ', array_keys($definitions)) |
1290
|
|
|
), |
1291
|
|
|
1558298976 |
1292
|
|
|
); |
1293
|
|
|
} |
1294
|
|
|
|
1295
|
|
|
protected function countCharactersMatchingMask(int $primaryMask, int $offset, int $length): int |
|
|
|
|
1296
|
|
|
{ |
1297
|
|
|
$bytes = &$this->source->bytes; |
1298
|
|
|
$counted = 0; |
1299
|
|
|
for ($index = $offset; $index < $this->source->length; $index++) { |
1300
|
|
|
if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) { |
1301
|
|
|
$counted++; |
1302
|
|
|
} |
1303
|
|
|
} |
1304
|
|
|
return $counted; |
1305
|
|
|
} |
1306
|
|
|
|
1307
|
|
|
protected function findBytePositionBeforeOffset(int $primaryMask, int $offset): int |
1308
|
|
|
{ |
1309
|
|
|
$bytes = &$this->source->bytes; |
1310
|
|
|
for ($index = min($offset, $this->source->length); $index > 0; $index--) { |
1311
|
|
|
if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) { |
1312
|
|
|
return $index; |
1313
|
|
|
} |
1314
|
|
|
} |
1315
|
|
|
return 0; |
1316
|
|
|
} |
1317
|
|
|
|
1318
|
|
|
protected function findBytePositionAfterOffset(int $primaryMask, int $offset): int |
1319
|
|
|
{ |
1320
|
|
|
$bytes = &$this->source->bytes; |
1321
|
|
|
for ($index = $offset; $index < $this->source->length; $index++) { |
1322
|
|
|
if (($primaryMask & (1 << $bytes[$index])) && $bytes[$index] < 64) { |
1323
|
|
|
return $index; |
1324
|
|
|
} |
1325
|
|
|
} |
1326
|
|
|
return max($this->source->length, $offset); |
1327
|
|
|
} |
1328
|
|
|
} |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.