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