1<?php
2namespace TYPO3Fluid\Fluid\Core\ViewHelper;
3
4/*
5 * This file belongs to the package "TYPO3 Fluid".
6 * See LICENSE.txt that was shipped with this package.
7 */
8
9use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler;
10use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\NodeInterface;
11use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\TextNode;
12use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\ViewHelperNode;
13use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
14use TYPO3Fluid\Fluid\Core\Variables\VariableProviderInterface;
15
16/**
17 * The abstract base class for all view helpers.
18 *
19 * @api
20 */
21abstract class AbstractViewHelper implements ViewHelperInterface
22{
23
24    /**
25     * Stores all \TYPO3Fluid\Fluid\ArgumentDefinition instances
26     * @var ArgumentDefinition[]
27     */
28    protected $argumentDefinitions = [];
29
30    /**
31     * Cache of argument definitions; the key is the ViewHelper class name, and the
32     * value is the array of argument definitions.
33     *
34     * In our benchmarks, this cache leads to a 40% improvement when using a certain
35     * ViewHelper class many times throughout the rendering process.
36     * @var array
37     */
38    static private $argumentDefinitionCache = [];
39
40    /**
41     * Current view helper node
42     * @var ViewHelperNode
43     */
44    protected $viewHelperNode;
45
46    /**
47     * Arguments array.
48     * @var array
49     * @api
50     */
51    protected $arguments = [];
52
53    /**
54     * Arguments array.
55     * @var NodeInterface[] array
56     * @api
57     */
58    protected $childNodes = [];
59
60    /**
61     * Current variable container reference.
62     * @var VariableProviderInterface
63     * @api
64     */
65    protected $templateVariableContainer;
66
67    /**
68     * @var RenderingContextInterface
69     */
70    protected $renderingContext;
71
72    /**
73     * @var \Closure
74     */
75    protected $renderChildrenClosure = null;
76
77    /**
78     * ViewHelper Variable Container
79     * @var ViewHelperVariableContainer
80     * @api
81     */
82    protected $viewHelperVariableContainer;
83
84    /**
85     * Specifies whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
86     * @see isChildrenEscapingEnabled()
87     *
88     * Note: If this is NULL the value of $this->escapingInterceptorEnabled is considered for backwards compatibility
89     *
90     * @var boolean
91     * @api
92     */
93    protected $escapeChildren = null;
94
95    /**
96     * Specifies whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
97     * @see isOutputEscapingEnabled()
98     *
99     * @var boolean
100     * @api
101     */
102    protected $escapeOutput = null;
103
104    /**
105     * @param array $arguments
106     * @return void
107     */
108    public function setArguments(array $arguments)
109    {
110        $this->arguments = $arguments;
111    }
112
113    /**
114     * @param RenderingContextInterface $renderingContext
115     * @return void
116     */
117    public function setRenderingContext(RenderingContextInterface $renderingContext)
118    {
119        $this->renderingContext = $renderingContext;
120        $this->templateVariableContainer = $renderingContext->getVariableProvider();
121        $this->viewHelperVariableContainer = $renderingContext->getViewHelperVariableContainer();
122    }
123
124    /**
125     * Returns whether the escaping interceptors should be disabled or enabled for the result of renderChildren() calls within this ViewHelper
126     *
127     * Note: This method is no public API, use $this->escapeChildren instead!
128     *
129     * @return boolean
130     */
131    public function isChildrenEscapingEnabled()
132    {
133        if ($this->escapeChildren === null) {
134            // Disable children escaping automatically, if output escaping is on anyway.
135            return !$this->isOutputEscapingEnabled();
136        }
137        return $this->escapeChildren;
138    }
139
140    /**
141     * Returns whether the escaping interceptors should be disabled or enabled for the render-result of this ViewHelper
142     *
143     * Note: This method is no public API, use $this->escapeChildren instead!
144     *
145     * @return boolean
146     */
147    public function isOutputEscapingEnabled()
148    {
149        return $this->escapeOutput !== false;
150    }
151
152    /**
153     * Register a new argument. Call this method from your ViewHelper subclass
154     * inside the initializeArguments() method.
155     *
156     * @param string $name Name of the argument
157     * @param string $type Type of the argument
158     * @param string $description Description of the argument
159     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
160     * @param mixed $defaultValue Default value of argument
161     * @param bool|null $escape Can be toggled to TRUE to force escaping of variables and inline syntax passed as argument value.
162     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
163     * @throws Exception
164     * @api
165     */
166    protected function registerArgument($name, $type, $description, $required = false, $defaultValue = null, $escape = null)
167    {
168        if (array_key_exists($name, $this->argumentDefinitions)) {
169            throw new Exception(
170                'Argument "' . $name . '" has already been defined, thus it should not be defined again.',
171                1253036401
172            );
173        }
174        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue, $escape);
175        return $this;
176    }
177
178    /**
179     * Overrides a registered argument. Call this method from your ViewHelper subclass
180     * inside the initializeArguments() method if you want to override a previously registered argument.
181     * @see registerArgument()
182     *
183     * @param string $name Name of the argument
184     * @param string $type Type of the argument
185     * @param string $description Description of the argument
186     * @param boolean $required If TRUE, argument is required. Defaults to FALSE.
187     * @param mixed $defaultValue Default value of argument
188     * @param bool|null $escape Can be toggled to TRUE to force escaping of variables and inline syntax passed as argument value.
189     * @return \TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper $this, to allow chaining.
190     * @throws Exception
191     * @api
192     */
193    protected function overrideArgument($name, $type, $description, $required = false, $defaultValue = null, $escape = null)
194    {
195        if (!array_key_exists($name, $this->argumentDefinitions)) {
196            throw new Exception(
197                'Argument "' . $name . '" has not been defined, thus it can\'t be overridden.',
198                1279212461
199            );
200        }
201        $this->argumentDefinitions[$name] = new ArgumentDefinition($name, $type, $description, $required, $defaultValue, $escape);
202        return $this;
203    }
204
205    /**
206     * Sets all needed attributes needed for the rendering. Called by the
207     * framework. Populates $this->viewHelperNode.
208     * This is PURELY INTERNAL! Never override this method!!
209     *
210     * @param ViewHelperNode $node View Helper node to be set.
211     * @return void
212     */
213    public function setViewHelperNode(ViewHelperNode $node)
214    {
215        $this->viewHelperNode = $node;
216    }
217
218    /**
219     * Sets all needed attributes needed for the rendering. Called by the
220     * framework. Populates $this->viewHelperNode.
221     * This is PURELY INTERNAL! Never override this method!!
222     *
223     * @param NodeInterface[] $childNodes
224     * @return void
225     */
226    public function setChildNodes(array $childNodes)
227    {
228        $this->childNodes = $childNodes;
229    }
230
231    /**
232     * Called when being inside a cached template.
233     *
234     * @param \Closure $renderChildrenClosure
235     * @return void
236     */
237    public function setRenderChildrenClosure(\Closure $renderChildrenClosure)
238    {
239        $this->renderChildrenClosure = $renderChildrenClosure;
240    }
241
242    /**
243     * Initialize the arguments of the ViewHelper, and call the render() method of the ViewHelper.
244     *
245     * @return string the rendered ViewHelper.
246     */
247    public function initializeArgumentsAndRender()
248    {
249        $this->validateArguments();
250        $this->initialize();
251
252        return $this->callRenderMethod();
253    }
254
255    /**
256     * Call the render() method and handle errors.
257     *
258     * @return string the rendered ViewHelper
259     * @throws Exception
260     */
261    protected function callRenderMethod()
262    {
263        if (method_exists($this, 'render')) {
264            return call_user_func([$this, 'render']);
265        }
266        if ((new \ReflectionMethod($this, 'renderStatic'))->getDeclaringClass()->getName() !== AbstractViewHelper::class) {
267            // Method is safe to call - will not recurse through ViewHelperInvoker via the default
268            // implementation of renderStatic() on this class.
269            return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext);
270        }
271        throw new Exception(
272            sprintf(
273                'ViewHelper class "%s" does not declare a "render()" method and inherits the default "renderStatic". ' .
274                'Executing this ViewHelper would cause infinite recursion - please either implement "render()" or ' .
275                '"renderStatic()" on your ViewHelper class',
276                get_class($this)
277            )
278        );
279    }
280
281    /**
282     * Initializes the view helper before invoking the render method.
283     *
284     * Override this method to solve tasks before the view helper content is rendered.
285     *
286     * @return void
287     * @api
288     */
289    public function initialize()
290    {
291    }
292
293    /**
294     * Helper method which triggers the rendering of everything between the
295     * opening and the closing tag.
296     *
297     * @return mixed The finally rendered child nodes.
298     * @api
299     */
300    public function renderChildren()
301    {
302        if ($this->renderChildrenClosure !== null) {
303            $closure = $this->renderChildrenClosure;
304            return $closure();
305        }
306        return $this->viewHelperNode->evaluateChildNodes($this->renderingContext);
307    }
308
309    /**
310     * Helper which is mostly needed when calling renderStatic() from within
311     * render().
312     *
313     * No public API yet.
314     *
315     * @return \Closure
316     */
317    protected function buildRenderChildrenClosure()
318    {
319        $self = clone $this;
320        return function() use ($self) {
321            return $self->renderChildren();
322        };
323    }
324
325    /**
326     * Initialize all arguments and return them
327     *
328     * @return ArgumentDefinition[]
329     */
330    public function prepareArguments()
331    {
332        $thisClassName = get_class($this);
333        if (isset(self::$argumentDefinitionCache[$thisClassName])) {
334            $this->argumentDefinitions = self::$argumentDefinitionCache[$thisClassName];
335        } else {
336            $this->initializeArguments();
337            self::$argumentDefinitionCache[$thisClassName] = $this->argumentDefinitions;
338        }
339        return $this->argumentDefinitions;
340    }
341
342    /**
343     * Validate arguments, and throw exception if arguments do not validate.
344     *
345     * @return void
346     * @throws \InvalidArgumentException
347     */
348    public function validateArguments()
349    {
350        $argumentDefinitions = $this->prepareArguments();
351        foreach ($argumentDefinitions as $argumentName => $registeredArgument) {
352            if ($this->hasArgument($argumentName)) {
353                $value = $this->arguments[$argumentName];
354                $type = $registeredArgument->getType();
355                if ($value !== $registeredArgument->getDefaultValue() && $type !== 'mixed') {
356                    $givenType = is_object($value) ? get_class($value) : gettype($value);
357                    if (!$this->isValidType($type, $value)) {
358                        throw new \InvalidArgumentException(
359                            'The argument "' . $argumentName . '" was registered with type "' . $type . '", but is of type "' .
360                            $givenType . '" in view helper "' . get_class($this) . '".',
361                            1256475113
362                        );
363                    }
364                }
365            }
366        }
367    }
368
369    /**
370     * Check whether the defined type matches the value type
371     *
372     * @param string $type
373     * @param mixed $value
374     * @return boolean
375     */
376    protected function isValidType($type, $value)
377    {
378        if ($type === 'object') {
379            if (!is_object($value)) {
380                return false;
381            }
382        } elseif ($type === 'array' || substr($type, -2) === '[]') {
383            if (!is_array($value) && !$value instanceof \ArrayAccess && !$value instanceof \Traversable && !empty($value)) {
384                return false;
385            } elseif (substr($type, -2) === '[]') {
386                $firstElement = $this->getFirstElementOfNonEmpty($value);
387                if ($firstElement === null) {
388                    return true;
389                }
390                return $this->isValidType(substr($type, 0, -2), $firstElement);
391            }
392        } elseif ($type === 'string') {
393            if (is_object($value) && !method_exists($value, '__toString')) {
394                return false;
395            }
396        } elseif ($type === 'boolean' && !is_bool($value)) {
397            return false;
398        } elseif (class_exists($type) && $value !== null && !$value instanceof $type) {
399            return false;
400        } elseif (is_object($value) && !is_a($value, $type, true)) {
401            return false;
402        }
403        return true;
404    }
405
406    /**
407     * Return the first element of the given array, ArrayAccess or Traversable
408     * that is not empty
409     *
410     * @param mixed $value
411     * @return mixed
412     */
413    protected function getFirstElementOfNonEmpty($value)
414    {
415        if (is_array($value)) {
416            return reset($value);
417        } elseif ($value instanceof \Traversable) {
418            foreach ($value as $element) {
419                return $element;
420            }
421        }
422        return null;
423    }
424
425    /**
426     * Initialize all arguments. You need to override this method and call
427     * $this->registerArgument(...) inside this method, to register all your arguments.
428     *
429     * @return void
430     * @api
431     */
432    public function initializeArguments()
433    {
434    }
435
436    /**
437     * Tests if the given $argumentName is set, and not NULL.
438     * The isset() test used fills both those requirements.
439     *
440     * @param string $argumentName
441     * @return boolean TRUE if $argumentName is found, FALSE otherwise
442     * @api
443     */
444    protected function hasArgument($argumentName)
445    {
446        return isset($this->arguments[$argumentName]);
447    }
448
449    /**
450     * Default implementation of "handling" additional, undeclared arguments.
451     * In this implementation the behavior is to consistently throw an error
452     * about NOT supporting any additional arguments. This method MUST be
453     * overridden by any ViewHelper that desires this support and this inherited
454     * method must not be called, obviously.
455     *
456     * @throws Exception
457     * @param array $arguments
458     * @return void
459     */
460    public function handleAdditionalArguments(array $arguments)
461    {
462    }
463
464    /**
465     * Default implementation of validating additional, undeclared arguments.
466     * In this implementation the behavior is to consistently throw an error
467     * about NOT supporting any additional arguments. This method MUST be
468     * overridden by any ViewHelper that desires this support and this inherited
469     * method must not be called, obviously.
470     *
471     * @throws Exception
472     * @param array $arguments
473     * @return void
474     */
475    public function validateAdditionalArguments(array $arguments)
476    {
477        if (!empty($arguments)) {
478            throw new Exception(
479                sprintf(
480                    'Undeclared arguments passed to ViewHelper %s: %s. Valid arguments are: %s',
481                    get_class($this),
482                    implode(', ', array_keys($arguments)),
483                    implode(', ', array_keys($this->argumentDefinitions))
484                )
485            );
486        }
487    }
488
489    /**
490     * You only should override this method *when you absolutely know what you
491     * are doing*, and really want to influence the generated PHP code during
492     * template compilation directly.
493     *
494     * @param string $argumentsName
495     * @param string $closureName
496     * @param string $initializationPhpCode
497     * @param ViewHelperNode $node
498     * @param TemplateCompiler $compiler
499     * @return string
500     */
501    public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler)
502    {
503        return sprintf(
504            '%s::renderStatic(%s, %s, $renderingContext)',
505            get_class($this),
506            $argumentsName,
507            $closureName
508        );
509    }
510
511    /**
512     * Default implementation of static rendering; useful API method if your ViewHelper
513     * when compiled is able to render itself statically to increase performance. This
514     * default implementation will simply delegate to the ViewHelperInvoker.
515     *
516     * @param array $arguments
517     * @param \Closure $renderChildrenClosure
518     * @param RenderingContextInterface $renderingContext
519     * @return mixed
520     */
521    public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
522    {
523        $viewHelperClassName = get_called_class();
524        return $renderingContext->getViewHelperInvoker()->invoke($viewHelperClassName, $arguments, $renderingContext, $renderChildrenClosure);
525    }
526
527    /**
528     * Save the associated ViewHelper node in a static public class variable.
529     * called directly after the ViewHelper was built.
530     *
531     * @param ViewHelperNode $node
532     * @param TextNode[] $arguments
533     * @param VariableProviderInterface $variableContainer
534     * @return void
535     */
536    public static function postParseEvent(ViewHelperNode $node, array $arguments, VariableProviderInterface $variableContainer)
537    {
538    }
539
540    /**
541     * Resets the ViewHelper state.
542     *
543     * Overwrite this method if you need to get a clean state of your ViewHelper.
544     *
545     * @return void
546     */
547    public function resetState()
548    {
549    }
550}
551