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