TYPO3  7.6
TemplateParser.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Fluid\Core\Parser;
3 
4 /* *
5  * This script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
6  * *
7  * It is free software; you can redistribute it and/or modify it under *
8  * the terms of the GNU Lesser General Public License, either version 3 *
9  * of the License, or (at your option) any later version. *
10  * *
11  * The TYPO3 project - inspiring people to share! *
12  * */
13 
18 {
19  public static $SCAN_PATTERN_NAMESPACEDECLARATION = '/(?<!\\\\){namespace\\s*(?P<identifier>[a-zA-Z]+[a-zA-Z0-9]*)\\s*=\\s*(?P<phpNamespace>(?:[A-Za-z0-9\.]+|Tx)(?:LEGACY_NAMESPACE_SEPARATOR\\w+|FLUID_NAMESPACE_SEPARATOR\\w+)+)\\s*}/m';
20  public static $SCAN_PATTERN_XMLNSDECLARATION = '/\sxmlns:(?P<identifier>.*?)="(?P<xmlNamespace>.*?)"/m';
21 
29 
35  (
36  (?: <\\/? # Start dynamic tags
37  (?:(?:NAMESPACE):[a-zA-Z0-9\\.]+) # A tag consists of the namespace prefix and word characters
38  (?: # Begin tag arguments
39  \\s*[a-zA-Z0-9:-]+ # Argument Keys
40  = # =
41  (?> # either... If we have found an argument, we will not back-track (That does the Atomic Bracket)
42  "(?:\\\\"|[^"])*" # a double-quoted string
43  |\'(?:\\\\\'|[^\'])*\' # or a single quoted string
44  )\\s* #
45  )* # Tag arguments can be replaced many times.
46  \\s*
47  \\/?> # Closing tag
48  )
49  |(?: # Start match CDATA section
50  <!\\[CDATA\\[.*?\\]\\]>
51  )
52  )/xs';
53 
58  ^< # A Tag begins with <
59  (?P<NamespaceIdentifier>NAMESPACE): # Then comes the Namespace prefix followed by a :
60  (?P<MethodIdentifier> # Now comes the Name of the ViewHelper
61  [a-zA-Z0-9\\.]+
62  )
63  (?P<Attributes> # Begin Tag Attributes
64  (?: # A tag might have multiple attributes
65  \\s*
66  [a-zA-Z0-9:-]+ # The attribute name
67  = # =
68  (?> # either... # If we have found an argument, we will not back-track (That does the Atomic Bracket)
69  "(?:\\\\"|[^"])*" # a double-quoted string
70  |\'(?:\\\\\'|[^\'])*\' # or a single quoted string
71  ) #
72  \\s*
73  )* # A tag might have multiple attributes
74  ) # End Tag Attributes
75  \\s*
76  (?P<Selfclosing>\\/?) # A tag might be selfclosing
77  >$/x';
78 
83  public static $SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG = '/^<\\/(?P<NamespaceIdentifier>NAMESPACE):(?P<MethodIdentifier>[a-zA-Z0-9\\.]+)\\s*>$/';
84 
91  public static $SCAN_PATTERN_NAMESPACE_FLUID_HTML_TAG = '/<html\\s++[^>]*data-namespace-typo3-fluid="true"[^>]*>/m';
92 
103  public static $SCAN_PATTERN_REMOVE_VIEWHELPERS_XMLNSDECLARATIONS = '/(?:\\s*+xmlns:(?:%1$s)="[^"]*"\\s*+)++/m';
104 
108  public static $SPLIT_PATTERN_TAGARGUMENTS = '/
109  (?: #
110  \\s* #
111  (?P<Argument> # The attribute name
112  [a-zA-Z0-9:-]+ #
113  ) #
114  = # =
115  (?> # If we have found an argument, we will not back-track (That does the Atomic Bracket)
116  (?P<ValueQuoted> # either...
117  (?:"(?:\\\\"|[^"])*") # a double-quoted string
118  |(?:\'(?:\\\\\'|[^\'])*\') # or a single quoted string
119  )
120  )\\s*
121  )
122  /xs';
123 
128  public static $SCAN_PATTERN_CDATA = '/^<!\\[CDATA\\[(.*?)\\]\\]>$/s';
129 
135  (
136  { # Start of shorthand syntax
137  (?: # Shorthand syntax is either composed of...
138  [a-zA-Z0-9\\->_:,.()] # Various characters
139  |"(?:\\\\"|[^"])*" # Double-quoted strings
140  |\'(?:\\\\\'|[^\'])*\' # Single-quoted strings
141  |(?R) # Other shorthand syntaxes inside, albeit not in a quoted string
142  |\\s+ # Spaces
143  )+
144  } # End of shorthand syntax
145  )/x';
146 
156  ^{ # Start of shorthand syntax
157  # A shorthand syntax is either...
158  (?P<Object>[a-zA-Z0-9\\-_.]*) # ... an object accessor
159  \\s*(?P<Delimiter>(?:->)?)\\s*
160 
161  (?P<ViewHelper> # ... a ViewHelper
162  [a-zA-Z0-9]+ # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
163  :
164  [a-zA-Z0-9\\.]+ # Method Identifier (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
165  \\( # Opening parameter brackets of ViewHelper
166  (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
167  (?:
168  \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
169  \\s*:\\s* # Key|Value delimiter :
170  (?: # Possible value options:
171  "(?:\\\\"|[^"])*" # Double qouoted string
172  |\'(?:\\\\\'|[^\'])*\' # Single quoted string
173  |[a-zA-Z0-9\\-_.]+ # variable identifiers
174  |{(?P>ViewHelperArguments)} # Another sub-array
175  ) # END possible value options
176  \\s*,? # There might be a , to separate different parts of the array
177  )* # The above cycle is repeated for all array elements
178  ) # End ViewHelper Arguments submatch
179  \\) # Closing parameter brackets of ViewHelper
180  )?
181  (?P<AdditionalViewHelpers> # There can be more than one ViewHelper chained, by adding more -> and the ViewHelper (recursively)
182  (?:
183  \\s*->\\s*
184  (?P>ViewHelper)
185  )*
186  )
187  }$/x';
188 
193 
194  (?P<NamespaceIdentifier>[a-zA-Z0-9]+) # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
195  :
196  (?P<MethodIdentifier>[a-zA-Z0-9\\.]+)
197  \\( # Opening parameter brackets of ViewHelper
198  (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
199  (?:
200  \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
201  \\s*:\\s* # Key|Value delimiter :
202  (?: # Possible value options:
203  "(?:\\\\"|[^"])*" # Double qouoted string
204  |\'(?:\\\\\'|[^\'])*\' # Single quoted string
205  |[a-zA-Z0-9\\-_.]+ # variable identifiers
206  |{(?P>ViewHelperArguments)} # Another sub-array
207  ) # END possible value options
208  \\s*,? # There might be a , to separate different parts of the array
209  )* # The above cycle is repeated for all array elements
210  ) # End ViewHelper Arguments submatch
211  \\) # Closing parameter brackets of ViewHelper
212  /x';
213 
222  (?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
223  { # Each array needs to start with {
224  (?P<Array> # Start submatch
225  (?:
226  \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
227  \\s*:\\s* # Key|Value delimiter :
228  (?: # Possible value options:
229  "(?:\\\\"|[^"])*" # Double qouoted string
230  |\'(?:\\\\\'|[^\'])*\' # Single quoted string
231  |[a-zA-Z0-9\\-_.]+ # variable identifiers
232  |(?P>Recursion) # Another sub-array
233  ) # END possible value options
234  \\s*,? # There might be a , to separate different parts of the array
235  )* # The above cycle is repeated for all array elements
236  ) # End array submatch
237  } # Each array ends with }
238  )$/x';
239 
245  (?P<ArrayPart> # Start submatch
246  (?P<Key>[a-zA-Z0-9\\-_]+) # The keys of the array
247  \\s*:\\s* # Key|Value delimiter :
248  (?: # Possible value options:
249  (?P<QuotedString> # Quoted string
250  (?:"(?:\\\\"|[^"])*")
251  |(?:\'(?:\\\\\'|[^\'])*\')
252  )
253  |(?P<VariableIdentifier>[a-zA-Z][a-zA-Z0-9\\-_.]*) # variable identifiers have to start with a letter
254  |(?P<Number>[0-9.]+) # Number
255  |{\\s*(?P<Subarray>(?:(?P>ArrayPart)\\s*,?\\s*)+)\\s*} # Another sub-array
256  ) # END possible value options
257  ) # End array part submatch
258  /x';
259 
264  public static $SCAN_PATTERN_DEFAULT_XML_NAMESPACE = '/^http\:\/\/typo3\.org\/ns\/(?P<PhpNamespace>.+)$/s';
265 
270  protected $namespaces = array(
271  'f' => 'TYPO3\\CMS\\Fluid\\ViewHelpers'
272  );
273 
278  protected $objectManager;
279 
283  protected $configuration;
284 
288  protected $settings;
289 
294 
299  public function __construct()
300  {
301  self::$SCAN_PATTERN_NAMESPACEDECLARATION = str_replace(
302  array(
303  'LEGACY_NAMESPACE_SEPARATOR',
304  'FLUID_NAMESPACE_SEPARATOR'
305  ),
306  array(
307  preg_quote(\TYPO3\CMS\Fluid\Fluid::LEGACY_NAMESPACE_SEPARATOR),
308  preg_quote(\TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR)
309  ),
310  self::$SCAN_PATTERN_NAMESPACEDECLARATION
311  );
312  }
313 
319  public function injectSettings(array $settings)
320  {
321  $this->settings = $settings;
322  }
323 
330  public function setConfiguration(\TYPO3\CMS\Fluid\Core\Parser\Configuration $configuration = null)
331  {
332  $this->configuration = $configuration;
333  }
334 
347  public function parse($templateString)
348  {
349  if (!is_string($templateString)) {
350  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
351  }
352  $this->reset();
353 
354  $templateString = $this->extractNamespaceDefinitions($templateString);
355  $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
356 
357  $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
358 
359  $variableContainer = $parsingState->getVariableContainer();
360  if ($variableContainer !== null && $variableContainer->exists('layoutName')) {
361  $parsingState->setLayoutNameNode($variableContainer->get('layoutName'));
362  }
363 
364  return $parsingState;
365  }
366 
372  public function getNamespaces()
373  {
374  return $this->namespaces;
375  }
376 
382  protected function reset()
383  {
384  $this->namespaces = array(
385  'f' => 'TYPO3\\CMS\\Fluid\\ViewHelpers'
386  );
387  }
388 
397  protected function extractNamespaceDefinitions($templateString)
398  {
399  $matches = array();
400  $foundIdentifiers = array();
401  preg_match_all(self::$SCAN_PATTERN_XMLNSDECLARATION, $templateString, $matches, PREG_SET_ORDER);
402  foreach ($matches as $match) {
403  // skip reserved "f" namespace identifier
404  if ($match['identifier'] === 'f') {
405  $foundIdentifiers[] = 'f';
406  continue;
407  }
408  if (array_key_exists($match['identifier'], $this->namespaces)) {
409  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1331135889);
410  }
411  if (isset($this->settings['namespaces'][$match['xmlNamespace']])) {
412  $phpNamespace = $this->settings['namespaces'][$match['xmlNamespace']];
413  } else {
414  $matchedPhpNamespace = array();
415  if (preg_match(self::$SCAN_PATTERN_DEFAULT_XML_NAMESPACE, $match['xmlNamespace'], $matchedPhpNamespace) === 0) {
416  continue;
417  }
418  $phpNamespace = str_replace('/', '\\', $matchedPhpNamespace['PhpNamespace']);
419  }
420  $foundIdentifiers[] = $match['identifier'];
421  $this->namespaces[$match['identifier']] = $phpNamespace;
422  }
423 
424  $templateString = $this->removeXmlnsViewHelperNamespaceDeclarations($templateString, $foundIdentifiers);
425 
426  $matches = array();
427  preg_match_all(self::$SCAN_PATTERN_NAMESPACEDECLARATION, $templateString, $matches, PREG_SET_ORDER);
428  foreach ($matches as $match) {
429  if (array_key_exists($match['identifier'], $this->namespaces)) {
430  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1224241246);
431  }
432  $this->namespaces[$match['identifier']] = $match['phpNamespace'];
433  }
434  if ($matches !== array()) {
435  $templateString = preg_replace(self::$SCAN_PATTERN_NAMESPACEDECLARATION, '', $templateString);
436  }
437 
438  return $templateString;
439  }
440 
449  protected function removeXmlnsViewHelperNamespaceDeclarations($templateString, array $foundIdentifiers)
450  {
451  $foundHtmlTags = 0;
452  $templateString = preg_replace(self::$SCAN_PATTERN_NAMESPACE_FLUID_HTML_TAG, '', $templateString, 1, $foundHtmlTags);
453  if ($foundHtmlTags > 0) {
454  $templateString = str_replace('</html>', '', $templateString);
455  }
456 
457  if (!empty($foundIdentifiers)) {
458  $foundIdentifiers = array_map(function ($foundIdentifier) {
459  return preg_quote($foundIdentifier, '/');
460  }, $foundIdentifiers);
461  $foundIdentifiers = implode('|', $foundIdentifiers);
462 
463  // replaces the pattern with space because the pattern includes trailing spaces and consecutive xmlns ViewHelper defintions
464  $templateString = preg_replace(
465  sprintf(self::$SCAN_PATTERN_REMOVE_VIEWHELPERS_XMLNSDECLARATIONS, $foundIdentifiers),
466  ' ',
467  $templateString
468  );
469  }
470 
471  return $templateString;
472  }
473 
480  protected function splitTemplateAtDynamicTags($templateString)
481  {
482  $regularExpression = $this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS);
483  return preg_split($regularExpression, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
484  }
485 
494  protected function buildObjectTree($splitTemplate, $context)
495  {
496  $regularExpression_openingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG);
497  $regularExpression_closingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG);
498 
499  $state = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\ParsingState::class);
500  $rootNode = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode::class);
501  $state->setRootNode($rootNode);
502  $state->pushNodeToStack($rootNode);
503 
504  foreach ($splitTemplate as $templateElement) {
505  $matchedVariables = array();
506  if (preg_match(self::$SCAN_PATTERN_CDATA, $templateElement, $matchedVariables) > 0) {
507  $this->textHandler($state, $matchedVariables[1]);
508  } elseif (preg_match($regularExpression_openingViewHelperTag, $templateElement, $matchedVariables) > 0) {
509  $this->openingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier'], $matchedVariables['Attributes'], ($matchedVariables['Selfclosing'] !== ''));
510  } elseif (preg_match($regularExpression_closingViewHelperTag, $templateElement, $matchedVariables) > 0) {
511  $this->closingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier']);
512  } else {
513  $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
514  }
515  }
516 
517  if ($state->countNodeStack() !== 1) {
518  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Not all tags were closed!', 1238169398);
519  }
520  return $state;
521  }
522 
533  protected function openingViewHelperTagHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing)
534  {
535  $argumentsObjectTree = $this->parseArguments($arguments);
536  $this->initializeViewHelperAndAddItToStack($state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree);
537 
538  if ($selfclosing) {
539  $node = $state->popNodeFromStack();
540  $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
541  }
542  }
543 
555  protected function initializeViewHelperAndAddItToStack(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree)
556  {
557  if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
558  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224254792);
559  }
560  $viewHelper = $this->objectManager->get($this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier));
561  $this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier] = get_class($viewHelper);
562 
563  // The following three checks are only done *in an uncached template*, and not needed anymore in the cached version
564  $expectedViewHelperArguments = $viewHelper->prepareArguments();
565  $this->abortIfUnregisteredArgumentsExist($expectedViewHelperArguments, $argumentsObjectTree);
566  $this->abortIfRequiredArgumentsAreMissing($expectedViewHelperArguments, $argumentsObjectTree);
567  $this->rewriteBooleanNodesInArgumentsObjectTree($expectedViewHelperArguments, $argumentsObjectTree);
568 
569  $currentViewHelperNode = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode::class, $viewHelper, $argumentsObjectTree);
570 
571  $state->getNodeFromStack()->addChildNode($currentViewHelperNode);
572 
573  if ($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\ChildNodeAccessInterface && !($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface)) {
574  $state->setCompilable(false);
575  }
576 
577  // PostParse Facet
578  if ($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\PostParseInterface) {
579  $viewHelper::postParseEvent($currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
580  }
581 
582  $this->callInterceptor($currentViewHelperNode, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
583 
584  $state->pushNodeToStack($currentViewHelperNode);
585  }
586 
595  protected function abortIfUnregisteredArgumentsExist($expectedArguments, $actualArguments)
596  {
597  $expectedArgumentNames = array();
598  foreach ($expectedArguments as $expectedArgument) {
599  $expectedArgumentNames[] = $expectedArgument->getName();
600  }
601 
602  foreach ($actualArguments as $argumentName => $_) {
603  if (!in_array($argumentName, $expectedArgumentNames)) {
604  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Argument "' . $argumentName . '" was not registered.', 1237823695);
605  }
606  }
607  }
608 
616  protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments)
617  {
618  $actualArgumentNames = array_keys($actualArguments);
619  foreach ($expectedArguments as $expectedArgument) {
620  if ($expectedArgument->isRequired() && !in_array($expectedArgument->getName(), $actualArgumentNames)) {
621  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Required argument "' . $expectedArgument->getName() . '" was not supplied.', 1237823699);
622  }
623  }
624  }
625 
633  protected function rewriteBooleanNodesInArgumentsObjectTree($argumentDefinitions, &$argumentsObjectTree)
634  {
635  foreach ($argumentDefinitions as $argumentName => $argumentDefinition) {
636  if ($argumentDefinition->getType() === 'boolean' && isset($argumentsObjectTree[$argumentName])) {
637  $argumentsObjectTree[$argumentName] = new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode($argumentsObjectTree[$argumentName]);
638  }
639  }
640  }
641 
649  protected function resolveViewHelperName($namespaceIdentifier, $methodIdentifier)
650  {
651  if (isset($this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier])) {
652  $name = $this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier];
653  } else {
654  $explodedViewHelperName = explode('.', $methodIdentifier);
655  $namespaceSeparator = strpos($this->namespaces[$namespaceIdentifier], \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR) !== false ? \TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR : \TYPO3\CMS\Fluid\Fluid::LEGACY_NAMESPACE_SEPARATOR;
656  if (count($explodedViewHelperName) > 1) {
657  $className = implode($namespaceSeparator, array_map('ucfirst', $explodedViewHelperName));
658  } else {
659  $className = ucfirst($explodedViewHelperName[0]);
660  }
661  $className .= 'ViewHelper';
662  $name = $this->namespaces[$namespaceIdentifier] . $namespaceSeparator . $className;
663  $name = \TYPO3\CMS\Core\Core\ClassLoadingInformation::getClassNameForAlias($name);
664  // The name isn't cached in viewHelperNameToImplementationClassNameRuntimeCache here because the
665  // class could be overloaded by extbase object manager. Thus the cache is filled in
666  // initializeViewHelperAndAddItToStack after getting the real object from the object manager.
667  }
668  return $name;
669  }
670 
680  protected function closingViewHelperTagHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier)
681  {
682  if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
683  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224256186);
684  }
685  $lastStackElement = $state->popNodeFromStack();
686  if (!($lastStackElement instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode)) {
687  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('You closed a templating tag which you never opened!', 1224485838);
688  }
689  if ($lastStackElement->getViewHelperClassName() != $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier)) {
690  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Templating tags not properly nested. Expected: ' . $lastStackElement->getViewHelperClassName() . '; Actual: ' . $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier), 1224485398);
691  }
692  $this->callInterceptor($lastStackElement, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
693  }
694 
708  protected function objectAccessorHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString)
709  {
710  $viewHelperString .= $additionalViewHelpersString;
711  $numberOfViewHelpers = 0;
712 
713  // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
714  // Resolves bug #5107.
715  if ($delimiter === '' && $viewHelperString !== '') {
716  $viewHelperString = $objectAccessorString . $viewHelperString;
717  $objectAccessorString = '';
718  }
719 
720  // ViewHelpers
721  $matches = array();
722  if ($viewHelperString !== '' && preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
723  // The last ViewHelper has to be added first for correct chaining.
724  foreach (array_reverse($matches) as $singleMatch) {
725  if ($singleMatch['ViewHelperArguments'] !== '') {
726  $arguments = $this->postProcessArgumentsForObjectAccessor(
727  $this->recursiveArrayHandler($singleMatch['ViewHelperArguments'])
728  );
729  } else {
730  $arguments = array();
731  }
732  $this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
733  $numberOfViewHelpers++;
734  }
735  }
736 
737  // Object Accessor
738  if ($objectAccessorString !== '') {
739  $node = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::class, $objectAccessorString);
740  $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
741 
742  $state->getNodeFromStack()->addChildNode($node);
743  }
744 
745  // Close ViewHelper Tags if needed.
746  for ($i=0; $i<$numberOfViewHelpers; $i++) {
747  $node = $state->popNodeFromStack();
748  $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
749  }
750  }
751 
760  protected function callInterceptor(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NodeInterface &$node, $interceptionPoint, \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state)
761  {
762  if ($this->configuration !== null) {
763  // $this->configuration is UNSET inside the arguments of a ViewHelper.
764  // That's why the interceptors are only called if the object accessor is not inside a ViewHelper Argument
765  // This could be a problem if We have a ViewHelper as an argument to another ViewHelper, and an ObjectAccessor nested inside there.
766  // @todo Clean up this.
767  $interceptors = $this->configuration->getInterceptors($interceptionPoint);
768  if (count($interceptors) > 0) {
769  foreach ($interceptors as $interceptor) {
770  $node = $interceptor->process($node, $interceptionPoint, $state);
771  }
772  }
773  }
774  }
775 
784  protected function postProcessArgumentsForObjectAccessor(array $arguments)
785  {
786  foreach ($arguments as $argumentName => $argumentValue) {
787  if (!($argumentValue instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode)) {
788  $arguments[$argumentName] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, (string)$argumentValue);
789  }
790  }
791  return $arguments;
792  }
793 
803  protected function parseArguments($argumentsString)
804  {
805  $argumentsObjectTree = array();
806  $matches = array();
807  if (preg_match_all(self::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
808  $configurationBackup = $this->configuration;
809  $this->configuration = null;
810  foreach ($matches as $singleMatch) {
811  $argument = $singleMatch['Argument'];
812  $value = $this->unquoteString($singleMatch['ValueQuoted']);
813  $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
814  }
815  $this->configuration = $configurationBackup;
816  }
817  return $argumentsObjectTree;
818  }
819 
830  protected function buildArgumentObjectTree($argumentString)
831  {
832  if (strpos($argumentString, '{') === false && strpos($argumentString, '<') === false) {
833  return $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, $argumentString);
834  }
835  $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
836  $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
837  return $rootNode;
838  }
839 
849  protected function unquoteString($quotedValue)
850  {
851  switch ($quotedValue[0]) {
852  case '"':
853  $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
854  break;
855  case "'":
856  $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
857  break;
858  default:
859  $value = $quotedValue;
860  }
861  return str_replace('\\\\', '\\', $value);
862  }
863 
872  protected function prepareTemplateRegularExpression($regularExpression)
873  {
874  return str_replace('NAMESPACE', implode('|', array_keys($this->namespaces)), $regularExpression);
875  }
876 
887  protected function textAndShorthandSyntaxHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $text, $context)
888  {
889  $sections = preg_split($this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_SHORTHANDSYNTAX), $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
890 
891  foreach ($sections as $section) {
892  $matchedVariables = array();
893  if (preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
894  $this->objectAccessorHandler($state, $matchedVariables['Object'], $matchedVariables['Delimiter'], isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : '', isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '');
895  } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS && preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0) {
896  // We only match arrays if we are INSIDE viewhelper arguments
897  $this->arrayHandler($state, $matchedVariables['Array']);
898  } else {
899  $this->textHandler($state, $section);
900  }
901  }
902  }
903 
912  protected function arrayHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $arrayText)
913  {
914  $state->getNodeFromStack()->addChildNode(
915  $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode::class, $this->recursiveArrayHandler($arrayText))
916  );
917  }
918 
933  protected function recursiveArrayHandler($arrayText)
934  {
935  $matches = array();
936  if (preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER) > 0) {
937  $arrayToBuild = array();
938  foreach ($matches as $singleMatch) {
939  $arrayKey = $singleMatch['Key'];
940  if (!empty($singleMatch['VariableIdentifier'])) {
941  $arrayToBuild[$arrayKey] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::class, $singleMatch['VariableIdentifier']);
942  } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
943  $arrayToBuild[$arrayKey] = floatval($singleMatch['Number']);
944  } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
945  $argumentString = $this->unquoteString($singleMatch['QuotedString']);
946  $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
947  } elseif (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
948  $arrayToBuild[$arrayKey] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode::class, $this->recursiveArrayHandler($singleMatch['Subarray']));
949  } else {
950  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('This exception should never be thrown, as the array value has to be of some type (Value given: "' . var_export($singleMatch, true) . '"). Please post your template to the bugtracker at forge.typo3.org.', 1225136013);
951  }
952  }
953  return $arrayToBuild;
954  } else {
955  throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('This exception should never be thrown, there is most likely some error in the regular expressions. Please post your template to the bugtracker at forge.typo3.org.', 1225136014);
956  }
957  }
958 
966  protected function textHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $text)
967  {
968  $node = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, $text);
969  $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_TEXT, $state);
970 
971  $state->getNodeFromStack()->addChildNode($node);
972  }
973 }