[CLEANUP] Replace strlen() with === for zero length check
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / Core / Parser / TemplateParser.php
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
14 /**
15 * Template parser building up an object syntax tree
16 */
17 class TemplateParser {
18
19 static public $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 static public $SCAN_PATTERN_XMLNSDECLARATION = '/\sxmlns:(?P<identifier>.*?)="(?P<xmlNamespace>.*?)"/m';
21
22 /**
23 * The following two constants are used for tracking whether we are currently
24 * parsing ViewHelper arguments or not. This is used to parse arrays only as
25 * ViewHelper argument.
26 */
27 const CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS = 1;
28 const CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS = 2;
29
30 /**
31 * This regular expression splits the input string at all dynamic tags, AND
32 * on all <![CDATA[...]]> sections.
33 */
34 static public $SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS = '/
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
54 /**
55 * This regular expression scans if the input string is a ViewHelper tag
56 */
57 static public $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG = '/
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
79 /**
80 * This regular expression scans if the input string is a closing ViewHelper
81 * tag.
82 */
83 static public $SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG = '/^<\\/(?P<NamespaceIdentifier>NAMESPACE):(?P<MethodIdentifier>[a-zA-Z0-9\\.]+)\\s*>$/';
84
85 /**
86 * This regular expression splits the tag arguments into its parts
87 */
88 static public $SPLIT_PATTERN_TAGARGUMENTS = '/
89 (?: #
90 \\s* #
91 (?P<Argument> # The attribute name
92 [a-zA-Z0-9:-]+ #
93 ) #
94 = # =
95 (?> # If we have found an argument, we will not back-track (That does the Atomic Bracket)
96 (?P<ValueQuoted> # either...
97 (?:"(?:\\\\"|[^"])*") # a double-quoted string
98 |(?:\'(?:\\\\\'|[^\'])*\') # or a single quoted string
99 )
100 )\\s*
101 )
102 /xs';
103
104 /**
105 * This pattern detects CDATA sections and outputs the text between opening
106 * and closing CDATA.
107 */
108 static public $SCAN_PATTERN_CDATA = '/^<!\\[CDATA\\[(.*?)\\]\\]>$/s';
109
110 /**
111 * Pattern which splits the shorthand syntax into different tokens. The
112 * "shorthand syntax" is everything like {...}
113 */
114 static public $SPLIT_PATTERN_SHORTHANDSYNTAX = '/
115 (
116 { # Start of shorthand syntax
117 (?: # Shorthand syntax is either composed of...
118 [a-zA-Z0-9\\->_:,.()] # Various characters
119 |"(?:\\\\"|[^"])*" # Double-quoted strings
120 |\'(?:\\\\\'|[^\'])*\' # Single-quoted strings
121 |(?R) # Other shorthand syntaxes inside, albeit not in a quoted string
122 |\\s+ # Spaces
123 )+
124 } # End of shorthand syntax
125 )/x';
126
127 /**
128 * Pattern which detects the object accessor syntax:
129 * {object.some.value}, additionally it detects ViewHelpers like
130 * {f:for(param1:bla)} and chaining like
131 * {object.some.value->f:bla.blubb()->f:bla.blubb2()}
132 *
133 * THIS IS ALMOST THE SAME AS IN $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
134 */
135 static public $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS = '/
136 ^{ # Start of shorthand syntax
137 # A shorthand syntax is either...
138 (?P<Object>[a-zA-Z0-9\\-_.]*) # ... an object accessor
139 \\s*(?P<Delimiter>(?:->)?)\\s*
140
141 (?P<ViewHelper> # ... a ViewHelper
142 [a-zA-Z0-9]+ # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
143 :
144 [a-zA-Z0-9\\.]+ # Method Identifier (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
145 \\( # Opening parameter brackets of ViewHelper
146 (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
147 (?:
148 \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
149 \\s*:\\s* # Key|Value delimiter :
150 (?: # Possible value options:
151 "(?:\\\\"|[^"])*" # Double qouoted string
152 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
153 |[a-zA-Z0-9\\-_.]+ # variable identifiers
154 |{(?P>ViewHelperArguments)} # Another sub-array
155 ) # END possible value options
156 \\s*,? # There might be a , to separate different parts of the array
157 )* # The above cycle is repeated for all array elements
158 ) # End ViewHelper Arguments submatch
159 \\) # Closing parameter brackets of ViewHelper
160 )?
161 (?P<AdditionalViewHelpers> # There can be more than one ViewHelper chained, by adding more -> and the ViewHelper (recursively)
162 (?:
163 \\s*->\\s*
164 (?P>ViewHelper)
165 )*
166 )
167 }$/x';
168
169 /**
170 * THIS IS ALMOST THE SAME AS $SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
171 */
172 static public $SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER = '/
173
174 (?P<NamespaceIdentifier>[a-zA-Z0-9]+) # Namespace prefix of ViewHelper (as in $SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG)
175 :
176 (?P<MethodIdentifier>[a-zA-Z0-9\\.]+)
177 \\( # Opening parameter brackets of ViewHelper
178 (?P<ViewHelperArguments> # Start submatch for ViewHelper arguments. This is taken from $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS
179 (?:
180 \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
181 \\s*:\\s* # Key|Value delimiter :
182 (?: # Possible value options:
183 "(?:\\\\"|[^"])*" # Double qouoted string
184 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
185 |[a-zA-Z0-9\\-_.]+ # variable identifiers
186 |{(?P>ViewHelperArguments)} # Another sub-array
187 ) # END possible value options
188 \\s*,? # There might be a , to separate different parts of the array
189 )* # The above cycle is repeated for all array elements
190 ) # End ViewHelper Arguments submatch
191 \\) # Closing parameter brackets of ViewHelper
192 /x';
193
194 /**
195 * Pattern which detects the array/object syntax like in JavaScript, so it
196 * detects strings like:
197 * {object: value, object2: {nested: array}, object3: "Some string"}
198 *
199 * THIS IS ALMOST THE SAME AS IN SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS
200 */
201 static public $SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS = '/^
202 (?P<Recursion> # Start the recursive part of the regular expression - describing the array syntax
203 { # Each array needs to start with {
204 (?P<Array> # Start submatch
205 (?:
206 \\s*[a-zA-Z0-9\\-_]+ # The keys of the array
207 \\s*:\\s* # Key|Value delimiter :
208 (?: # Possible value options:
209 "(?:\\\\"|[^"])*" # Double qouoted string
210 |\'(?:\\\\\'|[^\'])*\' # Single quoted string
211 |[a-zA-Z0-9\\-_.]+ # variable identifiers
212 |(?P>Recursion) # Another sub-array
213 ) # END possible value options
214 \\s*,? # There might be a , to separate different parts of the array
215 )* # The above cycle is repeated for all array elements
216 ) # End array submatch
217 } # Each array ends with }
218 )$/x';
219
220 /**
221 * This pattern splits an array into its parts. It is quite similar to the
222 * pattern above.
223 */
224 static public $SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS = '/
225 (?P<ArrayPart> # Start submatch
226 (?P<Key>[a-zA-Z0-9\\-_]+) # The keys of the array
227 \\s*:\\s* # Key|Value delimiter :
228 (?: # Possible value options:
229 (?P<QuotedString> # Quoted string
230 (?:"(?:\\\\"|[^"])*")
231 |(?:\'(?:\\\\\'|[^\'])*\')
232 )
233 |(?P<VariableIdentifier>[a-zA-Z][a-zA-Z0-9\\-_.]*) # variable identifiers have to start with a letter
234 |(?P<Number>[0-9.]+) # Number
235 |{\\s*(?P<Subarray>(?:(?P>ArrayPart)\\s*,?\\s*)+)\\s*} # Another sub-array
236 ) # END possible value options
237 ) # End array part submatch
238 /x';
239
240 /**
241 * This pattern detects the default xml namespace
242 *
243 */
244 static public $SCAN_PATTERN_DEFAULT_XML_NAMESPACE = '/^http\:\/\/typo3\.org\/ns\/(?P<PhpNamespace>.+)$/s';
245
246 /**
247 * Namespace identifiers and their component name prefix (Associative array).
248 * @var array
249 */
250 protected $namespaces = array(
251 'f' => 'TYPO3\\CMS\\Fluid\\ViewHelpers'
252 );
253
254 /**
255 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
256 * @inject
257 */
258 protected $objectManager;
259
260 /**
261 * @var \TYPO3\CMS\Fluid\Core\Parser\Configuration
262 */
263 protected $configuration;
264
265 /**
266 * @var array
267 */
268 protected $settings;
269
270 /**
271 * @var array
272 */
273 protected $viewHelperNameToImplementationClassNameRuntimeCache = array();
274
275 /**
276 * Constructor. Preprocesses the $SCAN_PATTERN_NAMESPACEDECLARATION by
277 * inserting the correct namespace separator.
278 */
279 public function __construct() {
280 self::$SCAN_PATTERN_NAMESPACEDECLARATION = str_replace(
281 array(
282 'LEGACY_NAMESPACE_SEPARATOR',
283 'FLUID_NAMESPACE_SEPARATOR'
284 ),
285 array(
286 preg_quote(\TYPO3\CMS\Fluid\Fluid::LEGACY_NAMESPACE_SEPARATOR),
287 preg_quote(\TYPO3\CMS\Fluid\Fluid::NAMESPACE_SEPARATOR)
288 ),
289 self::$SCAN_PATTERN_NAMESPACEDECLARATION
290 );
291 }
292
293 /**
294 * Injects Fluid settings
295 *
296 * @param array $settings
297 */
298 public function injectSettings(array $settings) {
299 $this->settings = $settings;
300 }
301
302 /**
303 * Set the configuration for the parser.
304 *
305 * @param \TYPO3\CMS\Fluid\Core\Parser\Configuration $configuration
306 * @return void
307 */
308 public function setConfiguration(\TYPO3\CMS\Fluid\Core\Parser\Configuration $configuration = NULL) {
309 $this->configuration = $configuration;
310 }
311
312 /**
313 * Parses a given template string and returns a parsed template object.
314 *
315 * The resulting ParsedTemplate can then be rendered by calling evaluate() on it.
316 *
317 * Normally, you should use a subclass of AbstractTemplateView instead of calling the
318 * TemplateParser directly.
319 *
320 * @param string $templateString The template to parse as a string
321 * @return \TYPO3\CMS\Fluid\Core\Parser\ParsedTemplateInterface Parsed template
322 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
323 */
324 public function parse($templateString) {
325 if (!is_string($templateString)) {
326 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Parse requires a template string as argument, ' . gettype($templateString) . ' given.', 1224237899);
327 }
328 $this->reset();
329
330 $templateString = $this->extractNamespaceDefinitions($templateString);
331 $splitTemplate = $this->splitTemplateAtDynamicTags($templateString);
332
333 $parsingState = $this->buildObjectTree($splitTemplate, self::CONTEXT_OUTSIDE_VIEWHELPER_ARGUMENTS);
334
335 $variableContainer = $parsingState->getVariableContainer();
336 if ($variableContainer !== NULL && $variableContainer->exists('layoutName')) {
337 $parsingState->setLayoutNameNode($variableContainer->get('layoutName'));
338 }
339
340 return $parsingState;
341 }
342
343 /**
344 * Gets the namespace definitions found.
345 *
346 * @return array Namespace identifiers and their component name prefix
347 */
348 public function getNamespaces() {
349 return $this->namespaces;
350 }
351
352 /**
353 * Resets the parser to its default values.
354 *
355 * @return void
356 */
357 protected function reset() {
358 $this->namespaces = array(
359 'f' => 'TYPO3\\CMS\\Fluid\\ViewHelpers'
360 );
361 }
362
363 /**
364 * Extracts namespace definitions out of the given template string and sets
365 * $this->namespaces.
366 *
367 * @param string $templateString Template string to extract the namespaces from
368 * @return string The updated template string without namespace declarations inside
369 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception if a namespace can't be resolved or has been declared already
370 */
371 protected function extractNamespaceDefinitions($templateString) {
372 $matches = array();
373 preg_match_all(self::$SCAN_PATTERN_XMLNSDECLARATION, $templateString, $matches, PREG_SET_ORDER);
374 foreach ($matches as $match) {
375 // skip reserved "f" namespace identifier
376 if ($match['identifier'] === 'f') {
377 continue;
378 }
379 if (array_key_exists($match['identifier'], $this->namespaces)) {
380 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1331135889);
381 }
382 if (isset($this->settings['namespaces'][$match['xmlNamespace']])) {
383 $phpNamespace = $this->settings['namespaces'][$match['xmlNamespace']];
384 } else {
385 $matchedPhpNamespace = array();
386 if (preg_match(self::$SCAN_PATTERN_DEFAULT_XML_NAMESPACE, $match['xmlNamespace'], $matchedPhpNamespace) === 0) {
387 continue;
388 }
389 $phpNamespace = str_replace('/', '\\', $matchedPhpNamespace['PhpNamespace']);
390 }
391 $this->namespaces[$match['identifier']] = $phpNamespace;
392 }
393 $matches = array();
394 preg_match_all(self::$SCAN_PATTERN_NAMESPACEDECLARATION, $templateString, $matches, PREG_SET_ORDER);
395 foreach ($matches as $match) {
396 if (array_key_exists($match['identifier'], $this->namespaces)) {
397 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception(sprintf('Namespace identifier "%s" is already registered. Do not re-declare namespaces!', $match['identifier']), 1224241246);
398 }
399 $this->namespaces[$match['identifier']] = $match['phpNamespace'];
400 }
401 if ($matches !== array()) {
402 $templateString = preg_replace(self::$SCAN_PATTERN_NAMESPACEDECLARATION, '', $templateString);
403 }
404
405 return $templateString;
406 }
407
408 /**
409 * Splits the template string on all dynamic tags found.
410 *
411 * @param string $templateString Template string to split.
412 * @return array Splitted template
413 */
414 protected function splitTemplateAtDynamicTags($templateString) {
415 $regularExpression = $this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_TEMPLATE_DYNAMICTAGS);
416 return preg_split($regularExpression, $templateString, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
417 }
418
419 /**
420 * Build object tree from the split template
421 *
422 * @param array $splitTemplate The split template, so that every tag with a namespace declaration is already a separate array element.
423 * @param int $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
424 * @return \TYPO3\CMS\Fluid\Core\Parser\ParsingState
425 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
426 */
427 protected function buildObjectTree($splitTemplate, $context) {
428 $regularExpression_openingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_VIEWHELPERTAG);
429 $regularExpression_closingViewHelperTag = $this->prepareTemplateRegularExpression(self::$SCAN_PATTERN_TEMPLATE_CLOSINGVIEWHELPERTAG);
430
431 $state = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\ParsingState::class);
432 $rootNode = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\RootNode::class);
433 $state->setRootNode($rootNode);
434 $state->pushNodeToStack($rootNode);
435
436 foreach ($splitTemplate as $templateElement) {
437 $matchedVariables = array();
438 if (preg_match(self::$SCAN_PATTERN_CDATA, $templateElement, $matchedVariables) > 0) {
439 $this->textHandler($state, $matchedVariables[1]);
440 } elseif (preg_match($regularExpression_openingViewHelperTag, $templateElement, $matchedVariables) > 0) {
441 $this->openingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier'], $matchedVariables['Attributes'], ($matchedVariables['Selfclosing'] === '' ? FALSE : TRUE));
442 } elseif (preg_match($regularExpression_closingViewHelperTag, $templateElement, $matchedVariables) > 0) {
443 $this->closingViewHelperTagHandler($state, $matchedVariables['NamespaceIdentifier'], $matchedVariables['MethodIdentifier']);
444 } else {
445 $this->textAndShorthandSyntaxHandler($state, $templateElement, $context);
446 }
447 }
448
449 if ($state->countNodeStack() !== 1) {
450 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Not all tags were closed!', 1238169398);
451 }
452 return $state;
453 }
454
455 /**
456 * Handles an opening or self-closing view helper tag.
457 *
458 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state Current parsing state
459 * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
460 * @param string $methodIdentifier Method identifier
461 * @param string $arguments Arguments string, not yet parsed
462 * @param bool $selfclosing true, if the tag is a self-closing tag.
463 * @return void
464 */
465 protected function openingViewHelperTagHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $arguments, $selfclosing) {
466 $argumentsObjectTree = $this->parseArguments($arguments);
467 $this->initializeViewHelperAndAddItToStack($state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree);
468
469 if ($selfclosing) {
470 $node = $state->popNodeFromStack();
471 $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
472 }
473 }
474
475 /**
476 * Initialize the given ViewHelper and adds it to the current node and to
477 * the stack.
478 *
479 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state Current parsing state
480 * @param string $namespaceIdentifier Namespace identifier - being looked up in $this->namespaces
481 * @param string $methodIdentifier Method identifier
482 * @param array $argumentsObjectTree Arguments object tree
483 * @return void
484 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
485 */
486 protected function initializeViewHelperAndAddItToStack(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier, $argumentsObjectTree) {
487 if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
488 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224254792);
489 }
490 $viewHelper = $this->objectManager->get($this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier));
491 $this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier] = get_class($viewHelper);
492
493 // The following three checks are only done *in an uncached template*, and not needed anymore in the cached version
494 $expectedViewHelperArguments = $viewHelper->prepareArguments();
495 $this->abortIfUnregisteredArgumentsExist($expectedViewHelperArguments, $argumentsObjectTree);
496 $this->abortIfRequiredArgumentsAreMissing($expectedViewHelperArguments, $argumentsObjectTree);
497 $this->rewriteBooleanNodesInArgumentsObjectTree($expectedViewHelperArguments, $argumentsObjectTree);
498
499 $currentViewHelperNode = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode::class, $viewHelper, $argumentsObjectTree);
500
501 $state->getNodeFromStack()->addChildNode($currentViewHelperNode);
502
503 if ($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\ChildNodeAccessInterface && !($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\CompilableInterface)) {
504 $state->setCompilable(FALSE);
505 }
506
507 // PostParse Facet
508 if ($viewHelper instanceof \TYPO3\CMS\Fluid\Core\ViewHelper\Facets\PostParseInterface) {
509 // Don't just use $viewHelper::postParseEvent(...),
510 // as this will break with PHP < 5.3.
511 call_user_func(array($viewHelper, 'postParseEvent'), $currentViewHelperNode, $argumentsObjectTree, $state->getVariableContainer());
512 }
513
514 $this->callInterceptor($currentViewHelperNode, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OPENING_VIEWHELPER, $state);
515
516 $state->pushNodeToStack($currentViewHelperNode);
517 }
518
519 /**
520 * Throw an exception if there are arguments which were not registered
521 * before.
522 *
523 * @param array $expectedArguments Array of \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition of all expected arguments
524 * @param array $actualArguments Actual arguments
525 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
526 */
527 protected function abortIfUnregisteredArgumentsExist($expectedArguments, $actualArguments) {
528 $expectedArgumentNames = array();
529 foreach ($expectedArguments as $expectedArgument) {
530 $expectedArgumentNames[] = $expectedArgument->getName();
531 }
532
533 foreach ($actualArguments as $argumentName => $_) {
534 if (!in_array($argumentName, $expectedArgumentNames)) {
535 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Argument "' . $argumentName . '" was not registered.', 1237823695);
536 }
537 }
538 }
539
540 /**
541 * Throw an exception if required arguments are missing
542 *
543 * @param array $expectedArguments Array of \TYPO3\CMS\Fluid\Core\ViewHelper\ArgumentDefinition of all expected arguments
544 * @param array $actualArguments Actual arguments
545 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
546 */
547 protected function abortIfRequiredArgumentsAreMissing($expectedArguments, $actualArguments) {
548 $actualArgumentNames = array_keys($actualArguments);
549 foreach ($expectedArguments as $expectedArgument) {
550 if ($expectedArgument->isRequired() && !in_array($expectedArgument->getName(), $actualArgumentNames)) {
551 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Required argument "' . $expectedArgument->getName() . '" was not supplied.', 1237823699);
552 }
553 }
554 }
555
556 /**
557 * Wraps the argument tree, if a node is boolean, into a Boolean syntax tree node
558 *
559 * @param array $argumentDefinitions the argument definitions, key is the argument name, value is the ArgumentDefinition object
560 * @param array $argumentsObjectTree the arguments syntax tree, key is the argument name, value is an AbstractNode
561 * @return void
562 */
563 protected function rewriteBooleanNodesInArgumentsObjectTree($argumentDefinitions, &$argumentsObjectTree) {
564 foreach ($argumentDefinitions as $argumentName => $argumentDefinition) {
565 if ($argumentDefinition->getType() === 'boolean' && isset($argumentsObjectTree[$argumentName])) {
566 $argumentsObjectTree[$argumentName] = new \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\BooleanNode($argumentsObjectTree[$argumentName]);
567 }
568 }
569 }
570
571 /**
572 * Resolve a viewhelper name.
573 *
574 * @param string $namespaceIdentifier Namespace identifier for the view helper.
575 * @param string $methodIdentifier Method identifier, might be hierarchical like "link.url"
576 * @return string The fully qualified class name of the viewhelper
577 */
578 protected function resolveViewHelperName($namespaceIdentifier, $methodIdentifier) {
579 if (isset($this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier])) {
580 $name = $this->viewHelperNameToImplementationClassNameRuntimeCache[$namespaceIdentifier][$methodIdentifier];
581 } else {
582 $explodedViewHelperName = explode('.', $methodIdentifier);
583 $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;
584 if (count($explodedViewHelperName) > 1) {
585 $className = implode($namespaceSeparator, array_map('ucfirst', $explodedViewHelperName));
586 } else {
587 $className = ucfirst($explodedViewHelperName[0]);
588 }
589 $className .= 'ViewHelper';
590 $name = $this->namespaces[$namespaceIdentifier] . $namespaceSeparator . $className;
591 $name = \TYPO3\CMS\Core\Core\ClassLoader::getClassNameForAlias($name);
592 // The name isn't cached in viewHelperNameToImplementationClassNameRuntimeCache here because the
593 // class could be overloaded by extbase object manager. Thus the cache is filled in
594 // initializeViewHelperAndAddItToStack after getting the real object from the object manager.
595 }
596 return $name;
597 }
598
599 /**
600 * Handles a closing view helper tag
601 *
602 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state The current parsing state
603 * @param string $namespaceIdentifier Namespace identifier for the closing tag.
604 * @param string $methodIdentifier Method identifier.
605 * @return void
606 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
607 */
608 protected function closingViewHelperTagHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $namespaceIdentifier, $methodIdentifier) {
609 if (!array_key_exists($namespaceIdentifier, $this->namespaces)) {
610 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Namespace could not be resolved. This exception should never be thrown!', 1224256186);
611 }
612 $lastStackElement = $state->popNodeFromStack();
613 if (!($lastStackElement instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ViewHelperNode)) {
614 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('You closed a templating tag which you never opened!', 1224485838);
615 }
616 if ($lastStackElement->getViewHelperClassName() != $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier)) {
617 throw new \TYPO3\CMS\Fluid\Core\Parser\Exception('Templating tags not properly nested. Expected: ' . $lastStackElement->getViewHelperClassName() . '; Actual: ' . $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier), 1224485398);
618 }
619 $this->callInterceptor($lastStackElement, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
620 }
621
622 /**
623 * Handles the appearance of an object accessor (like {posts.author.email}).
624 * Creates a new instance of \TYPO3\CMS\Fluid\ObjectAccessorNode.
625 *
626 * Handles ViewHelpers as well which are in the shorthand syntax.
627 *
628 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state The current parsing state
629 * @param string $objectAccessorString String which identifies which objects to fetch
630 * @param string $delimiter
631 * @param string $viewHelperString
632 * @param string $additionalViewHelpersString
633 * @return void
634 */
635 protected function objectAccessorHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $objectAccessorString, $delimiter, $viewHelperString, $additionalViewHelpersString) {
636 $viewHelperString .= $additionalViewHelpersString;
637 $numberOfViewHelpers = 0;
638
639 // The following post-processing handles a case when there is only a ViewHelper, and no Object Accessor.
640 // Resolves bug #5107.
641 if ($delimiter === '' && $viewHelperString !== '') {
642 $viewHelperString = $objectAccessorString . $viewHelperString;
643 $objectAccessorString = '';
644 }
645
646 // ViewHelpers
647 $matches = array();
648 if ($viewHelperString !== '' && preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_VIEWHELPER, $viewHelperString, $matches, PREG_SET_ORDER) > 0) {
649 // The last ViewHelper has to be added first for correct chaining.
650 foreach (array_reverse($matches) as $singleMatch) {
651 if ($singleMatch['ViewHelperArguments'] !== '') {
652 $arguments = $this->postProcessArgumentsForObjectAccessor(
653 $this->recursiveArrayHandler($singleMatch['ViewHelperArguments'])
654 );
655 } else {
656 $arguments = array();
657 }
658 $this->initializeViewHelperAndAddItToStack($state, $singleMatch['NamespaceIdentifier'], $singleMatch['MethodIdentifier'], $arguments);
659 $numberOfViewHelpers++;
660 }
661 }
662
663 // Object Accessor
664 if ($objectAccessorString !== '') {
665
666 $node = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::class, $objectAccessorString);
667 $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_OBJECTACCESSOR, $state);
668
669 $state->getNodeFromStack()->addChildNode($node);
670 }
671
672 // Close ViewHelper Tags if needed.
673 for ($i=0; $i<$numberOfViewHelpers; $i++) {
674 $node = $state->popNodeFromStack();
675 $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_CLOSING_VIEWHELPER, $state);
676 }
677 }
678
679 /**
680 * Call all interceptors registered for a given interception point.
681 *
682 * @param \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NodeInterface $node The syntax tree node which can be modified by the interceptors.
683 * @param int $interceptionPoint the interception point. One of the \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_* constants.
684 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state the parsing state
685 * @return void
686 */
687 protected function callInterceptor(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\NodeInterface &$node, $interceptionPoint, \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state) {
688 if ($this->configuration !== NULL) {
689 // $this->configuration is UNSET inside the arguments of a ViewHelper.
690 // That's why the interceptors are only called if the object accessor is not inside a ViewHelper Argument
691 // This could be a problem if We have a ViewHelper as an argument to another ViewHelper, and an ObjectAccessor nested inside there.
692 // @todo Clean up this.
693 $interceptors = $this->configuration->getInterceptors($interceptionPoint);
694 if (count($interceptors) > 0) {
695 foreach ($interceptors as $interceptor) {
696 $node = $interceptor->process($node, $interceptionPoint, $state);
697 }
698 }
699 }
700 }
701
702 /**
703 * Post process the arguments for the ViewHelpers in the object accessor
704 * syntax. We need to convert an array into an array of (only) nodes
705 *
706 * @param array $arguments The arguments to be processed
707 * @return array the processed array
708 * @todo This method should become superflous once the rest has been refactored, so that this code is not needed.
709 */
710 protected function postProcessArgumentsForObjectAccessor(array $arguments) {
711 foreach ($arguments as $argumentName => $argumentValue) {
712 if (!($argumentValue instanceof \TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\AbstractNode)) {
713 $arguments[$argumentName] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, (string)$argumentValue);
714 }
715 }
716 return $arguments;
717 }
718
719 /**
720 * Parse arguments of a given tag, and build up the Arguments Object Tree
721 * for each argument.
722 * Returns an associative array, where the key is the name of the argument,
723 * and the value is a single Argument Object Tree.
724 *
725 * @param string $argumentsString All arguments as string
726 * @return array An associative array of objects, where the key is the argument name.
727 */
728 protected function parseArguments($argumentsString) {
729 $argumentsObjectTree = array();
730 $matches = array();
731 if (preg_match_all(self::$SPLIT_PATTERN_TAGARGUMENTS, $argumentsString, $matches, PREG_SET_ORDER) > 0) {
732 $configurationBackup = $this->configuration;
733 $this->configuration = NULL;
734 foreach ($matches as $singleMatch) {
735 $argument = $singleMatch['Argument'];
736 $value = $this->unquoteString($singleMatch['ValueQuoted']);
737 $argumentsObjectTree[$argument] = $this->buildArgumentObjectTree($value);
738 }
739 $this->configuration = $configurationBackup;
740 }
741 return $argumentsObjectTree;
742 }
743
744 /**
745 * Build up an argument object tree for the string in $argumentString.
746 * This builds up the tree for a single argument value.
747 *
748 * This method also does some performance optimizations, so in case
749 * no { or < is found, then we just return a TextNode.
750 *
751 * @param string $argumentString
752 * @return SyntaxTree\AbstractNode the corresponding argument object tree.
753 */
754 protected function buildArgumentObjectTree($argumentString) {
755 if (strpos($argumentString, '{') === FALSE && strpos($argumentString, '<') === FALSE) {
756 return $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, $argumentString);
757 }
758 $splitArgument = $this->splitTemplateAtDynamicTags($argumentString);
759 $rootNode = $this->buildObjectTree($splitArgument, self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS)->getRootNode();
760 return $rootNode;
761 }
762
763 /**
764 * Removes escapings from a given argument string and trims the outermost
765 * quotes.
766 *
767 * This method is meant as a helper for regular expression results.
768 *
769 * @param string $quotedValue Value to unquote
770 * @return string Unquoted value
771 */
772 protected function unquoteString($quotedValue) {
773 switch ($quotedValue[0]) {
774 case '"':
775 $value = str_replace('\\"', '"', preg_replace('/(^"|"$)/', '', $quotedValue));
776 break;
777 case "'":
778 $value = str_replace("\\'", "'", preg_replace('/(^\'|\'$)/', '', $quotedValue));
779 break;
780 default:
781 $value = $quotedValue;
782 }
783 return str_replace('\\\\', '\\', $value);
784 }
785
786 /**
787 * Takes a regular expression template and replaces "NAMESPACE" with the
788 * currently registered namespace identifiers. Returns a regular expression
789 * which is ready to use.
790 *
791 * @param string $regularExpression Regular expression template
792 * @return string Regular expression ready to be used
793 */
794 protected function prepareTemplateRegularExpression($regularExpression) {
795 return str_replace('NAMESPACE', implode('|', array_keys($this->namespaces)), $regularExpression);
796 }
797
798 /**
799 * Handler for everything which is not a ViewHelperNode.
800 *
801 * This includes Text, array syntax, and object accessor syntax.
802 *
803 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state Current parsing state
804 * @param string $text Text to process
805 * @param int $context one of the CONTEXT_* constants, defining whether we are inside or outside of ViewHelper arguments currently.
806 * @return void
807 */
808 protected function textAndShorthandSyntaxHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $text, $context) {
809 $sections = preg_split($this->prepareTemplateRegularExpression(self::$SPLIT_PATTERN_SHORTHANDSYNTAX), $text, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
810
811 foreach ($sections as $section) {
812 $matchedVariables = array();
813 if (preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_OBJECTACCESSORS, $section, $matchedVariables) > 0) {
814 $this->objectAccessorHandler($state, $matchedVariables['Object'], $matchedVariables['Delimiter'], isset($matchedVariables['ViewHelper']) ? $matchedVariables['ViewHelper'] : '', isset($matchedVariables['AdditionalViewHelpers']) ? $matchedVariables['AdditionalViewHelpers'] : '');
815 } elseif ($context === self::CONTEXT_INSIDE_VIEWHELPER_ARGUMENTS && preg_match(self::$SCAN_PATTERN_SHORTHANDSYNTAX_ARRAYS, $section, $matchedVariables) > 0) {
816 // We only match arrays if we are INSIDE viewhelper arguments
817 $this->arrayHandler($state, $matchedVariables['Array']);
818 } else {
819 $this->textHandler($state, $section);
820 }
821 }
822 }
823
824 /**
825 * Handler for array syntax. This creates the array object recursively and
826 * adds it to the current node.
827 *
828 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state The current parsing state
829 * @param string $arrayText The array as string.
830 * @return void
831 */
832 protected function arrayHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $arrayText) {
833 $state->getNodeFromStack()->addChildNode(
834 $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode::class, $this->recursiveArrayHandler($arrayText))
835 );
836 }
837
838 /**
839 * Recursive function which takes the string representation of an array and
840 * builds an object tree from it.
841 *
842 * Deals with the following value types:
843 * - Numbers (Integers and Floats)
844 * - Strings
845 * - Variables
846 * - sub-arrays
847 *
848 * @param string $arrayText Array text
849 * @return SyntaxTree\ArrayNode the array node built up
850 * @throws \TYPO3\CMS\Fluid\Core\Parser\Exception
851 */
852 protected function recursiveArrayHandler($arrayText) {
853 $matches = array();
854 if (preg_match_all(self::$SPLIT_PATTERN_SHORTHANDSYNTAX_ARRAY_PARTS, $arrayText, $matches, PREG_SET_ORDER) > 0) {
855 $arrayToBuild = array();
856 foreach ($matches as $singleMatch) {
857 $arrayKey = $singleMatch['Key'];
858 if (!empty($singleMatch['VariableIdentifier'])) {
859 $arrayToBuild[$arrayKey] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ObjectAccessorNode::class, $singleMatch['VariableIdentifier']);
860 } elseif (array_key_exists('Number', $singleMatch) && (!empty($singleMatch['Number']) || $singleMatch['Number'] === '0')) {
861 $arrayToBuild[$arrayKey] = floatval($singleMatch['Number']);
862 } elseif ((array_key_exists('QuotedString', $singleMatch) && !empty($singleMatch['QuotedString']))) {
863 $argumentString = $this->unquoteString($singleMatch['QuotedString']);
864 $arrayToBuild[$arrayKey] = $this->buildArgumentObjectTree($argumentString);
865 } elseif (array_key_exists('Subarray', $singleMatch) && !empty($singleMatch['Subarray'])) {
866 $arrayToBuild[$arrayKey] = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\ArrayNode::class, $this->recursiveArrayHandler($singleMatch['Subarray']));
867 } else {
868 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);
869 }
870 }
871 return $arrayToBuild;
872 } else {
873 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);
874 }
875 }
876
877 /**
878 * Text node handler
879 *
880 * @param \TYPO3\CMS\Fluid\Core\Parser\ParsingState $state
881 * @param string $text
882 * @return void
883 */
884 protected function textHandler(\TYPO3\CMS\Fluid\Core\Parser\ParsingState $state, $text) {
885 $node = $this->objectManager->get(\TYPO3\CMS\Fluid\Core\Parser\SyntaxTree\TextNode::class, $text);
886 $this->callInterceptor($node, \TYPO3\CMS\Fluid\Core\Parser\InterceptorInterface::INTERCEPT_TEXT, $state);
887
888 $state->getNodeFromStack()->addChildNode($node);
889 }
890
891 }