1<?php
2namespace TYPO3Fluid\Fluid\Core\Parser;
3
4/*
5 * This file belongs to the package "TYPO3 Fluid".
6 * See LICENSE.txt that was shipped with this package.
7 */
8
9use TYPO3Fluid\Fluid\Core\Compiler\StopCompilingException;
10use TYPO3Fluid\Fluid\Core\Compiler\UncompilableTemplateInterface;
11use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ArrayNode;
12use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode;
13use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionException;
14use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ExpressionNodeInterface;
15use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\Expression\ParseTimeEvaluatedExpressionNodeInterface;
16use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
17use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NumericNode;
18use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode;
19use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\RootNode;
20use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
21use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
22use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
23use TYPO3Fluid\Fluid\Core\ViewHelper\ArgumentDefinition;
24use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInterface;
25
26/**
27 * Template parser building up an object syntax tree
28 */
29class TemplateParser
30{
31
32    /**
33     * The following two constants are used for tracking whether we are currently
34     * parsing ViewHelper arguments or not. This is used to parse arrays only as
35     * ViewHelper argument.
36     */
37    const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
38    const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
39
40    /**
41     * Whether or not the escaping interceptors are active
42     *
43     * @var boolean
44     */
45    protected $escapingEnabled = true;
46
47    /**
48     * @var Configuration
49     */
50    protected $configuration;
51
52    /**
53     * @var array
54     */
55    protected $settings;
56
57    /**
58     * @var RenderingContextInterface
59     */
60    protected $renderingContext;
61
62    /**
63     * @var integer
64     */
65    protected $pointerLineNumber = 1;
66
67    /**
68     * @var integer
69     */
70    protected $pointerLineCharacter = 1;
71
72    /**
73     * @var string
74     */
75    protected $pointerTemplateCode = null;
76
77    /**
78     * @var ParsedTemplateInterface[]
79     */
80    protected $parsedTemplates = [];
81
82    /**
83     * @param RenderingContextInterface $renderingContext
84     * @return void
85     */
86    public function setRenderingContext(RenderingContextInterface $renderingContext)
87    {
88        $this->renderingContext = $renderingContext;
89        $this->configuration = $renderingContext->buildParserConfiguration();
90    }
91
92    /**
93     * Returns an array of current line number, character in line and reference template code;
94     * for extraction when catching parser-related Exceptions during parsing.
95     *
96     * @return array
97     */
98    public function getCurrentParsingPointers()
99    {
100        return [$this->pointerLineNumber, $this->pointerLineCharacter, $this->pointerTemplateCode];
101    }
102
103    /**
104     * @return boolean
105     */
106    public function isEscapingEnabled()
107    {
108        return $this->escapingEnabled;
109    }
110
111    /**
112     * @param boolean $escapingEnabled
113     * @return void
114     */
115    public function setEscapingEnabled($escapingEnabled)
116    {
117        $this->escapingEnabled = (boolean) $escapingEnabled;
118    }
119
120    /**
121     * Parses a given template string and returns a parsed template object.
122     *
123     * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
124     *
125     * Normally, you should use a subclass of AbstractTemplateView instead of calling the
126     * TemplateParser directly.
127     *
128     * @param string $templateString The template to parse as a string
129     * @param string|null $templateIdentifier If the template has an identifying string it can be passed here to improve error reporting.
130     * @return ParsingState Parsed template
131     * @throws Exception
132     */
133    public function parse($templateString, $templateIdentifier = null)
134    {
135        if (!is_string($templateString)) {
136            throw new Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
137        }
138        try {
139            $this->reset();
140
141            $templateString = $this->preProcessTemplateSource($templateString);
142
143            $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
144            $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
145        } catch (Exception $error) {
146            throw $this->createParsingRelatedExceptionWithContext($error, $templateIdentifier);
147        }
148        $this->parsedTemplates[$templateIdentifier] = $parsingState;
149        return $parsingState;
150    }
151
152    /**
153     * @param \Exception $error
154     * @param string $templateIdentifier
155     * @throws \Exception
156     */
157    public function createParsingRelatedExceptionWithContext(\Exception $error, $templateIdentifier)
158    {
159        list ($line, $character, $templateCode) = $this->getCurrentParsingPointers();
160        $exceptionClass = get_class($error);
161        return new $exceptionClass(
162            sprintf(
163                'Fluid parse error in template %s, line %d at character %d. Error: %s (error code %d). Template source chunk: %s',
164                $templateIdentifier,
165                $line,
166                $character,
167                $error->getMessage(),
168                $error->getCode(),
169                $templateCode
170            ),
171            $error->getCode(),
172            $error
173        );
174    }
175
176    /**
177     * @param string $templateIdentifier
178     * @param \Closure $templateSourceClosure Closure which returns the template source if needed
179     * @return ParsedTemplateInterface
180     */
181    public function getOrParseAndStoreTemplate($templateIdentifier, $templateSourceClosure)
182    {
183        $compiler = $this->renderingContext->getTemplateCompiler();
184        if (isset($this->parsedTemplates[$templateIdentifier])) {
185            $parsedTemplate = $this->parsedTemplates[$templateIdentifier];
186        } elseif ($compiler->has($templateIdentifier)) {
187            $parsedTemplate = $compiler->get($templateIdentifier);
188            if ($parsedTemplate instanceof UncompilableTemplateInterface) {
189                $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
190            }
191        } else {
192            $parsedTemplate = $this->parseTemplateSource($templateIdentifier, $templateSourceClosure);
193            try {
194                $compiler->store($templateIdentifier, $parsedTemplate);
195            } catch (StopCompilingException $stop) {
196                $this->renderingContext->getErrorHandler()->handleCompilerError($stop);
197                $parsedTemplate->setCompilable(false);
198                $compiler->store($templateIdentifier, $parsedTemplate);
199            }
200        }
201        return $parsedTemplate;
202    }
203
204    /**
205     * @param string $templateIdentifier
206     * @param \Closure $templateSourceClosure
207     * @return ParsedTemplateInterface
208     */
209    protected function parseTemplateSource($templateIdentifier, $templateSourceClosure)
210    {
211        $parsedTemplate = $this->parse(
212            $templateSourceClosure($this, $this->renderingContext->getTemplatePaths()),
213            $templateIdentifier
214        );
215        $parsedTemplate->setIdentifier($templateIdentifier);
216        $this->parsedTemplates[$templateIdentifier] = $parsedTemplate;
217        return $parsedTemplate;
218    }
219
220    /**
221     * Pre-process the template source, making all registered TemplateProcessors
222     * do what they need to do with the template source before it is parsed.
223     *
224     * @param string $templateSource
225     * @return string
226     */
227    protected function preProcessTemplateSource($templateSource)
228    {
229        foreach ($this->renderingContext->getTemplateProcessors() as $templateProcessor) {
230            $templateSource = $templateProcessor->preProcessSource($templateSource);
231        }
232        return $templateSource;
233    }
234
235    /**
236     * Resets the parser to its default values.
237     *
238     * @return void
239     */
240    protected function reset()
241    {
242        $this->escapingEnabled = true;
243        $this->pointerLineNumber = 1;
244        $this->pointerLineCharacter = 1;
245    }
246
247    /**
248     * Splits the template string on all dynamic tags found.
249     *
250     * @param string $templateString Template string to split.
251     * @return array Splitted template
252     */
253    protected function splitTemplateAtDynamicTags($templateString)
254    {
255        return preg_split(Patterns::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
256    }
257
258    /**
259     * Build object tree from the split template
260     *
261     * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a seperate array element.
262     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
263     * @return ParsingState
264     * @throws Exception
265     */
266    protected function buildObjectTree(array $splitTemplate, $context)
267    {
268        $state = $this->getParsingState();
269        $previousBlock = '';
270
271        foreach ($splitTemplate as $templateElement) {
272            if ($context === self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS) {
273                // Store a neat reference to the outermost chunk of Fluid template code.
274                // Don't store the reference if parsing ViewHelper arguments object tree;
275                // we want the reference code to contain *all* of the ViewHelper call.
276                $this->pointerTemplateCode = $templateElement;
277            }
278            $this->pointerLineNumber += substr_count($templateElement, PHP_EOL);
279            $this->pointerLineCharacter = strlen(substr($previousBlock, strrpos($previousBlock, PHP_EOL))) + 1;
280            $previousBlock = $templateElement;
281            $matchedVariables = [];
282
283            if (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
284                try {
285                    if ($this->openingViewHelperTagHandler(
286                        $state,
287                        $matchedVariables['NamespaceIdentifier'],
288                        $matchedVariables['MethodIdentifier'],
289                        $matchedVariables['Attributes'],
290                        ($matchedVariables['Selfclosing'] === '' ? false : true),
291                        $templateElement
292                    )) {
293                        continue;
294                    }
295                } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
296                    $this->textHandler(
297                        $state,
298                        $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
299                    );
300                } catch (Exception $error) {
301                    $this->textHandler(
302                        $state,
303                        $this->renderingContext->getErrorHandler()->handleParserError($error)
304                    );
305                }
306            } elseif (preg_match(Patterns::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG, $templateElement, $matchedVariables) > 0) {
307                if ($this->closingViewHelperTagHandler(
308                    $state,
309                    $matchedVariables['NamespaceIdentifier'],
310                    $matchedVariables['MethodIdentifier']
311                )) {
312                    continue;
313                }
314            }
315            $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
316        }
317
318        if ($state->countNodeStack() !== 1) {
319            throw new Exception(
320                'Not all tags were closed!',
321                1238169398
322            );
323        }
324        return $state;
325    }
326    /**
327     * Handles an opening or self-closing view helper tag.
328     *
329     * @param ParsingState $state Current parsing state
330     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
331     * @param string $methodIdentifier Method identifier
332     * @param string $arguments Arguments string, not yet parsed
333     * @param boolean $selfclosing true, if the tag is a self-closing tag.
334     * @param string $templateElement The template code containing the ViewHelper call
335     * @return NodeInterface|null
336     */
337    protected function openingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing, $templateElement)
338    {
339        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
340        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
341            return null;
342        }
343        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
344            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
345        }
346
347        $viewHelper = $viewHelperResolver->createViewHelperInstance($namespaceIdentifier, $methodIdentifier);
348        $argumentDefinitions = $viewHelper->prepareArguments();
349        $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
350            $state,
351            $namespaceIdentifier,
352            $methodIdentifier,
353            $this->parseArguments($arguments, $viewHelper)
354        );
355
356        if ($viewHelperNode) {
357            $viewHelperNode->setPointerTemplateCode($templateElement);
358            if ($selfclosing === true) {
359                $state->popNodeFromStack();
360                $this->callInterceptor($viewHelperNode, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
361                // This needs to be called here because closingViewHelperTagHandler() is not triggered for self-closing tags
362                $state->getNodeFromStack()->addChildNode($viewHelperNode);
363            }
364        }
365
366        return $viewHelperNode;
367    }
368
369    /**
370     * Initialize the given ViewHelper and adds it to the current node and to
371     * the stack.
372     *
373     * @param ParsingState $state Current parsing state
374     * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
375     * @param string $methodIdentifier Method identifier
376     * @param array $argumentsObjectTree Arguments object tree
377     * @return null|NodeInterface An instance of ViewHelperNode if identity was valid - NULL if the namespace/identity was not registered
378     * @throws Exception
379     */
380    protected function initializeViewHelperAndAddItToStack(ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
381    {
382        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
383        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
384            return null;
385        }
386        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
387            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
388        }
389        try {
390            $currentViewHelperNode = new ViewHelperNode(
391                $this->renderingContext,
392                $namespaceIdentifier,
393                $methodIdentifier,
394                $argumentsObjectTree,
395                $state
396            );
397
398            $this->callInterceptor($currentViewHelperNode, InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
399            $viewHelper = $currentViewHelperNode->getUninitializedViewHelper();
400            $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
401            $state->pushNodeToStack($currentViewHelperNode);
402            return $currentViewHelperNode;
403        } catch (\TYPO3Fluid\Fluid\Core\ViewHelper\Exception $error) {
404            $this->textHandler(
405                $state,
406                $this->renderingContext->getErrorHandler()->handleViewHelperError($error)
407            );
408        } catch (Exception $error) {
409            $this->textHandler(
410                $state,
411                $this->renderingContext->getErrorHandler()->handleParserError($error)
412            );
413        }
414        return null;
415    }
416
417    /**
418     * Handles a closing view helper tag
419     *
420     * @param ParsingState $state The current parsing state
421     * @param string $namespaceIdentifier Namespace identifier for the closing tag.
422     * @param string $methodIdentifier Method identifier.
423     * @return boolean whether the viewHelper was found and added to the stack or not
424     * @throws Exception
425     */
426    protected function closingViewHelperTagHandler(ParsingState $state, $namespaceIdentifier, $methodIdentifier)
427    {
428        $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
429        if ($viewHelperResolver->isNamespaceIgnored($namespaceIdentifier)) {
430            return false;
431        }
432        if (!$viewHelperResolver->isNamespaceValid($namespaceIdentifier)) {
433            throw new UnknownNamespaceException('Unknown Namespace: ' . $namespaceIdentifier);
434        }
435        $lastStackElement = $state->popNodeFromStack();
436        if (!($lastStackElement instanceof ViewHelperNode)) {
437            throw new Exception('You closed a templating tag which you never opened!', 1224485838);
438        }
439        $actualViewHelperClassName = $viewHelperResolver->resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier);
440        $expectedViewHelperClassName = $lastStackElement->getViewHelperClassName();
441        if ($actualViewHelperClassName !== $expectedViewHelperClassName) {
442            throw new Exception(
443                'Templating tags not properly nested. Expected: ' . $expectedViewHelperClassName . '; Actual: ' .
444                $actualViewHelperClassName,
445                1224485398
446            );
447        }
448        $this->callInterceptor($lastStackElement, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
449        $state->getNodeFromStack()->addChildNode($lastStackElement);
450
451        return true;
452    }
453
454    /**
455     * Handles the appearance of an object accessor (like {posts.author.email}).
456     * Creates a new instance of \TYPO3Fluid\Fluid\ObjectAccessorNode.
457     *
458     * Handles ViewHelpers as well which are in the shorthand syntax.
459     *
460     * @param ParsingState $state The current parsing state
461     * @param string $objectAccessorString String which identifies which objects to fetch
462     * @param string $delimiter
463     * @param string $viewHelperString
464     * @param string $additionalViewHelpersString
465     * @return void
466     */
467    protected function objectAccessorHandler(ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
468    {
469        $viewHelperString .= $additionalViewHelpersString;
470        $numberOfViewHelpers = 0;
471
472        // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
473        // Resolves bug #5107.
474        if (strlen($delimiter) === 0 && strlen($viewHelperString) > 0) {
475            $viewHelperString = $objectAccessorString . $viewHelperString;
476            $objectAccessorString = '';
477        }
478
479        // ViewHelpers
480        $matches = [];
481        if (strlen($viewHelperString) > 0 && preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
482            // The last ViewHelper has to be added first for correct chaining.
483            // Note that ignoring namespaces is NOT possible in inline syntax; any inline syntax that contains a namespace
484            // which is invalid will be reported as an error regardless of whether the namespace is marked as ignored.
485            $viewHelperResolver = $this->renderingContext->getViewHelperResolver();
486            foreach (array_reverse($matches) as $singleMatch) {
487                if (!$viewHelperResolver->isNamespaceValid($singleMatch['NamespaceIdentifier'])) {
488                    throw new UnknownNamespaceException('Unknown Namespace: ' . $singleMatch['NamespaceIdentifier']);
489                }
490                $viewHelper = $viewHelperResolver->createViewHelperInstance($singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier']);
491                if (strlen($singleMatch['ViewHelperArguments']) > 0) {
492                    $arguments = $this->recursiveArrayHandler($state, $singleMatch['ViewHelperArguments'], $viewHelper);
493                } else {
494                    $arguments = [];
495                }
496                $viewHelperNode = $this->initializeViewHelperAndAddItToStack(
497                    $state,
498                    $singleMatch['NamespaceIdentifier'],
499                    $singleMatch['MethodIdentifier'],
500                    $arguments
501                );
502                if ($viewHelperNode) {
503                    $numberOfViewHelpers++;
504                }
505            }
506        }
507
508        // Object Accessor
509        if (strlen($objectAccessorString) > 0) {
510            $node = new ObjectAccessorNode($objectAccessorString);
511            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
512            $state->getNodeFromStack()->addChildNode($node);
513        }
514
515        // Close ViewHelper Tags if needed.
516        for ($i = 0; $i < $numberOfViewHelpers; $i++) {
517            $node = $state->popNodeFromStack();
518            $this->callInterceptor($node, InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
519            $state->getNodeFromStack()->addChildNode($node);
520        }
521    }
522
523    /**
524     * Call all interceptors registered for a given interception point.
525     *
526     * @param NodeInterface $node The syntax tree node which can be modified by the interceptors.
527     * @param integer $interceptionPoint the interception point. One of the \TYPO3Fluid\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
528     * @param ParsingState $state the parsing state
529     * @return void
530     */
531    protected function callInterceptor(NodeInterface & $node, $interceptionPoint, ParsingState $state)
532    {
533        if ($this->configuration === null) {
534            return;
535        }
536        if ($this->escapingEnabled) {
537            /** @var $interceptor InterceptorInterface */
538            foreach ($this->configuration->getEscapingInterceptors($interceptionPoint) as $interceptor) {
539                $node = $interceptor->process($node, $interceptionPoint, $state);
540            }
541        }
542
543        /** @var $interceptor InterceptorInterface */
544        foreach ($this->configuration->getInterceptors($interceptionPoint) as $interceptor) {
545            $node = $interceptor->process($node, $interceptionPoint, $state);
546        }
547    }
548
549    /**
550     * Parse arguments of a given tag, and build up the Arguments Object Tree
551     * for each argument.
552     * Returns an associative array, where the key is the name of the argument,
553     * and the value is a single Argument Object Tree.
554     *
555     * @param string $argumentsString All arguments as string
556     * @param ViewHelperInterface $viewHelper
557     * @return array An associative array of objects, where the key is the argument name.
558     */
559    protected function parseArguments($argumentsString, ViewHelperInterface $viewHelper)
560    {
561        $argumentDefinitions = $this->renderingContext->getViewHelperResolver()->getArgumentDefinitionsForViewHelper($viewHelper);
562        $argumentsObjectTree = [];
563        $undeclaredArguments = [];
564        $matches = [];
565        if (preg_match_all(Patterns::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
566            foreach ($matches as $singleMatch) {
567                $argument = $singleMatch['Argument'];
568                $value = $this->unquoteString($singleMatch['ValueQuoted']);
569                $escapingEnabledBackup = $this->escapingEnabled;
570                if (isset($argumentDefinitions[$argument])) {
571                    $argumentDefinition = $argumentDefinitions[$argument];
572                    $this->escapingEnabled = $this->escapingEnabled && $this->isArgumentEscaped($viewHelper, $argumentDefinition);
573                    $isBoolean = $argumentDefinition->getType() === 'boolean' || $argumentDefinition->getType() === 'bool';
574                    $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
575                    if ($isBoolean) {
576                        $argumentsObjectTree[$argument] = new BooleanNode($argumentsObjectTree[$argument]);
577                    }
578                } else {
579                    $this->escapingEnabled = false;
580                    $undeclaredArguments[$argument] = $this->buildArgumentObjectTree($value);
581                }
582                $this->escapingEnabled = $escapingEnabledBackup;
583            }
584        }
585        $this->abortIfRequiredArgumentsAreMissing($argumentDefinitions, $argumentsObjectTree);
586        $viewHelper->validateAdditionalArguments($undeclaredArguments);
587        return $argumentsObjectTree + $undeclaredArguments;
588    }
589
590    protected function isArgumentEscaped(ViewHelperInterface $viewHelper, ArgumentDefinition $argumentDefinition = null)
591    {
592        $hasDefinition = $argumentDefinition instanceof ArgumentDefinition;
593        $isBoolean = $hasDefinition && ($argumentDefinition->getType() === 'boolean' || $argumentDefinition->getType() === 'bool');
594        $escapingEnabled = $this->configuration->isViewHelperArgumentEscapingEnabled();
595        $isArgumentEscaped = $hasDefinition && $argumentDefinition->getEscape() === true;
596        $isContentArgument = $hasDefinition && method_exists($viewHelper, 'resolveContentArgumentName') && $argumentDefinition->getName() === $viewHelper->resolveContentArgumentName();
597        if ($isContentArgument) {
598            return !$isBoolean && ($viewHelper->isChildrenEscapingEnabled() || $isArgumentEscaped);
599        }
600        return !$isBoolean && $escapingEnabled && $isArgumentEscaped;
601    }
602
603    /**
604     * Build up an argument object tree for the string in $argumentString.
605     * This builds up the tree for a single argument value.
606     *
607     * This method also does some performance optimizations, so in case
608     * no { or < is found, then we just return a TextNode.
609     *
610     * @param string $argumentString
611     * @return SyntaxTree\NodeInterface the corresponding argument object tree.
612     */
613    protected function buildArgumentObjectTree($argumentString)
614    {
615        if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
616            if (is_numeric($argumentString)) {
617                return new NumericNode($argumentString);
618            }
619            return new TextNode($argumentString);
620        }
621        $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
622        $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
623        return $rootNode;
624    }
625
626    /**
627     * Removes escapings from a given argument string and trims the outermost
628     * quotes.
629     *
630     * This method is meant as a helper for regular expression results.
631     *
632     * @param string $quotedValue Value to unquote
633     * @return string Unquoted value
634     */
635    public function unquoteString($quotedValue)
636    {
637        $value = $quotedValue;
638        if ($value === '') {
639            return $value;
640        }
641        if ($quotedValue[0] === '"') {
642            $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
643        } elseif ($quotedValue[0] === '\'') {
644            $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
645        }
646        return str_replace('\\\\', '\\', $value);
647    }
648
649    /**
650     * Handler for everything which is not a ViewHelperNode.
651     *
652     * This includes Text, array syntax, and object accessor syntax.
653     *
654     * @param ParsingState $state Current parsing state
655     * @param string $text Text to process
656     * @param integer $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
657     * @return void
658     */
659    protected function textAndShorthandSyntaxHandler(ParsingState $state, $text, $context)
660    {
661        $sections = preg_split(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX, $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
662        if ($sections === false) {
663            // String $text was not possible to split; we must return a text node with the full text instead.
664            $this->textHandler($state, $text);
665            return;
666        }
667        foreach ($sections as $section) {
668            $matchedVariables = [];
669            $expressionNode = null;
670            if (preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
671                $this->objectAccessorHandler(
672                    $state,
673                    $matchedVariables['Object'],
674                    $matchedVariables['Delimiter'],
675                    (isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : ''),
676                    (isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '')
677                );
678            } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS
679                && preg_match(Patterns::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0
680            ) {
681                // We only match arrays if we are INSIDE viewhelper arguments
682                $this->arrayHandler($state, $this->recursiveArrayHandler($state, $matchedVariables['Array']));
683            } else {
684                // We ask custom ExpressionNode instances from ViewHelperResolver
685                // if any match our expression:
686                foreach ($this->renderingContext->getExpressionNodeTypes() as $expressionNodeTypeClassName) {
687                    $detectionExpression = $expressionNodeTypeClassName::$detectionExpression;
688                    $matchedVariables = [];
689                    preg_match_all($detectionExpression, $section, $matchedVariables, PREG_SET_ORDER);
690                    if (is_array($matchedVariables) === true) {
691                        foreach ($matchedVariables as $matchedVariableSet) {
692                            $expressionStartPosition = strpos($section, $matchedVariableSet[0]);
693                            /** @var ExpressionNodeInterface $expressionNode */
694                            $expressionNode = new $expressionNodeTypeClassName($matchedVariableSet[0], $matchedVariableSet, $state);
695                            try {
696                                // Trigger initial parse-time evaluation to allow the node to manipulate the rendering context.
697                                if ($expressionNode instanceof ParseTimeEvaluatedExpressionNodeInterface) {
698                                    $expressionNode->evaluate($this->renderingContext);
699                                }
700
701                                if ($expressionStartPosition > 0) {
702                                    $state->getNodeFromStack()->addChildNode(new TextNode(substr($section, 0, $expressionStartPosition)));
703                                }
704
705                                $this->callInterceptor($expressionNode, InterceptorInterface::INTERCEPT_EXPRESSION, $state);
706                                $state->getNodeFromStack()->addChildNode($expressionNode);
707
708                                $expressionEndPosition = $expressionStartPosition + strlen($matchedVariableSet[0]);
709                                if ($expressionEndPosition < strlen($section)) {
710                                    $this->textAndShorthandSyntaxHandler($state, substr($section, $expressionEndPosition), $context);
711                                    break;
712                                }
713                            } catch (ExpressionException $error) {
714                                $this->textHandler(
715                                    $state,
716                                    $this->renderingContext->getErrorHandler()->handleExpressionError($error)
717                                );
718                            }
719                        }
720                    }
721                }
722
723                if (!$expressionNode) {
724                    // As fallback we simply render the expression back as template content.
725                    $this->textHandler($state, $section);
726                }
727            }
728        }
729    }
730
731    /**
732     * Handler for array syntax. This creates the array object recursively and
733     * adds it to the current node.
734     *
735     * @param ParsingState $state The current parsing state
736     * @param NodeInterface[] $arrayText The array as string.
737     * @return void
738     */
739    protected function arrayHandler(ParsingState $state, $arrayText)
740    {
741        $arrayNode = new ArrayNode($arrayText);
742        $state->getNodeFromStack()->addChildNode($arrayNode);
743    }
744
745    /**
746     * Recursive function which takes the string representation of an array and
747     * builds an object tree from it.
748     *
749     * Deals with the following value types:
750     * - Numbers (Integers and Floats)
751     * - Strings
752     * - Variables
753     * - sub-arrays
754     *
755     * @param ParsingState $state
756     * @param string $arrayText Array text
757     * @param ViewHelperInterface|null $viewHelper ViewHelper instance - passed only if the array is a collection of arguments for an inline ViewHelper
758     * @return NodeInterface[] the array node built up
759     * @throws Exception
760     */
761    protected function recursiveArrayHandler(ParsingState $state, $arrayText, ViewHelperInterface $viewHelper = null)
762    {
763        $undeclaredArguments = [];
764        $argumentDefinitions = [];
765        if ($viewHelper instanceof ViewHelperInterface) {
766            $argumentDefinitions = $this->renderingContext->getViewHelperResolver()->getArgumentDefinitionsForViewHelper($viewHelper);
767        }
768        $matches = [];
769        $arrayToBuild = [];
770        if (preg_match_all(Patterns::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER)) {
771            foreach ($matches as $singleMatch) {
772                $arrayKey = $this->unquoteString($singleMatch['Key']);
773                $assignInto = &$arrayToBuild;
774                $isBoolean = false;
775                $argumentDefinition = null;
776                if (isset($argumentDefinitions[$arrayKey])) {
777                    $argumentDefinition = $argumentDefinitions[$arrayKey];
778                    $isBoolean = $argumentDefinitions[$arrayKey]->getType() === 'boolean' || $argumentDefinitions[$arrayKey]->getType() === 'bool';
779                } else {
780                    $assignInto = &$undeclaredArguments;
781                }
782
783                $escapingEnabledBackup = $this->escapingEnabled;
784                $this->escapingEnabled = $this->escapingEnabled && $viewHelper instanceof ViewHelperInterface && $this->isArgumentEscaped($viewHelper, $argumentDefinition);
785
786                if (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
787                    $assignInto[$arrayKey] = new ArrayNode($this->recursiveArrayHandler($state, $singleMatch['Subarray']));
788                } elseif (!empty($singleMatch['VariableIdentifier'])) {
789                    $assignInto[$arrayKey] = new ObjectAccessorNode($singleMatch['VariableIdentifier']);
790                    if ($viewHelper instanceof ViewHelperInterface && !$isBoolean) {
791                        $this->callInterceptor($assignInto[$arrayKey], InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
792                    }
793                } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
794                    // Note: this method of casting picks "int" when value is a natural number and "float" if any decimals are found. See also NumericNode.
795                    $assignInto[$arrayKey] = $singleMatch['Number'] + 0;
796                } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
797                    $argumentString = $this->unquoteString($singleMatch['QuotedString']);
798                    $assignInto[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
799                }
800
801                if ($isBoolean) {
802                    $assignInto[$arrayKey] = new BooleanNode($assignInto[$arrayKey]);
803                }
804
805                $this->escapingEnabled = $escapingEnabledBackup;
806            }
807        }
808        if ($viewHelper instanceof ViewHelperInterface) {
809            $this->abortIfRequiredArgumentsAreMissing($argumentDefinitions, $arrayToBuild);
810            $viewHelper->validateAdditionalArguments($undeclaredArguments);
811        }
812        return $arrayToBuild + $undeclaredArguments;
813    }
814
815    /**
816     * Text node handler
817     *
818     * @param ParsingState $state
819     * @param string $text
820     * @return void
821     */
822    protected function textHandler(ParsingState $state, $text)
823    {
824        $node = new TextNode($text);
825        $this->callInterceptor($node, InterceptorInterface::INTERCEPT_TEXT, $state);
826        $state->getNodeFromStack()->addChildNode($node);
827    }
828
829    /**
830     * @return ParsingState
831     */
832    protected function getParsingState()
833    {
834        $rootNode = new RootNode();
835        $variableProvider = $this->renderingContext->getVariableProvider();
836        $state = new ParsingState();
837        $state->setRootNode($rootNode);
838        $state->pushNodeToStack($rootNode);
839        $state->setVariableProvider($variableProvider->getScopeCopy($variableProvider->getAll()));
840        return $state;
841    }
842
843    /**
844     * Throw an exception if required arguments are missing
845     *
846     * @param ArgumentDefinition[] $expectedArguments Array of all expected arguments
847     * @param NodeInterface[] $actualArguments Actual arguments
848     * @throws Exception
849     */
850    protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments)
851    {
852        $actualArgumentNames = array_keys($actualArguments);
853        foreach ($expectedArguments as $name => $expectedArgument) {
854            if ($expectedArgument->isRequired() && !in_array($name, $actualArgumentNames)) {
855                throw new Exception('Required argument "' . $name . '" was not supplied.', 1237823699);
856            }
857        }
858    }
859}
860