1<?php
2namespace TYPO3\CMS\Frontend\ContentObject;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Core\TypoScript\TypoScriptService;
18use TYPO3\CMS\Core\Utility\GeneralUtility;
19use TYPO3\CMS\Core\Utility\StringUtility;
20use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
21use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
22use TYPO3\CMS\Extbase\Object\ObjectManager;
23use TYPO3\CMS\Fluid\View\StandaloneView;
24use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
25
26/**
27 * Contains FLUIDTEMPLATE class object
28 */
29class FluidTemplateContentObject extends AbstractContentObject
30{
31    /**
32     * @var StandaloneView
33     */
34    protected $view;
35
36    /**
37     * @var ContentDataProcessor
38     */
39    protected $contentDataProcessor;
40
41    /**
42     * @param ContentObjectRenderer $cObj
43     */
44    public function __construct(ContentObjectRenderer $cObj)
45    {
46        parent::__construct($cObj);
47        $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
48    }
49
50    /**
51     * @param ContentDataProcessor $contentDataProcessor
52     */
53    public function setContentDataProcessor($contentDataProcessor)
54    {
55        $this->contentDataProcessor = $contentDataProcessor;
56    }
57
58    /**
59     * Rendering the cObject, FLUIDTEMPLATE
60     *
61     * Configuration properties:
62     * - file string+stdWrap The FLUID template file
63     * - layoutRootPaths array of filepath+stdWrap Root paths to layouts (fallback)
64     * - partialRootPaths array of filepath+stdWrap Root paths to partials (fallback)
65     * - variable array of cObjects, the keys are the variable names in fluid
66     * - dataProcessing array of data processors which are classes to manipulate $data
67     * - extbase.pluginName
68     * - extbase.controllerExtensionName
69     * - extbase.controllerName
70     * - extbase.controllerActionName
71     *
72     * Example:
73     * 10 = FLUIDTEMPLATE
74     * 10.templateName = MyTemplate
75     * 10.templateRootPaths.10 = EXT:site_configuration/Resources/Private/Templates/
76     * 10.partialRootPaths.10 = EXT:site_configuration/Resources/Private/Patials/
77     * 10.layoutRootPaths.10 = EXT:site_configuration/Resources/Private/Layouts/
78     * 10.variables {
79     *   mylabel = TEXT
80     *   mylabel.value = Label from TypoScript coming
81     * }
82     *
83     * @param array $conf Array of TypoScript properties
84     * @return string The HTML output
85     */
86    public function render($conf = [])
87    {
88        $parentView = $this->view;
89        $this->initializeStandaloneViewInstance();
90
91        if (!is_array($conf)) {
92            $conf = [];
93        }
94
95        $this->setFormat($conf);
96        $this->setTemplate($conf);
97        $this->setLayoutRootPath($conf);
98        $this->setPartialRootPath($conf);
99        $this->setExtbaseVariables($conf);
100        $this->assignSettings($conf);
101        $variables = $this->getContentObjectVariables($conf);
102        $variables = $this->contentDataProcessor->process($this->cObj, $conf, $variables);
103
104        $this->view->assignMultiple($variables);
105
106        $this->renderFluidTemplateAssetsIntoPageRenderer();
107        $content = $this->renderFluidView();
108        $content = $this->applyStandardWrapToRenderedContent($content, $conf);
109
110        $this->view = $parentView;
111        return $content;
112    }
113
114    /**
115     * Attempts to render HeaderAssets and FooterAssets sections from the
116     * Fluid template, then adds each (if not empty) to either header or
117     * footer, as appropriate, using PageRenderer.
118     */
119    protected function renderFluidTemplateAssetsIntoPageRenderer()
120    {
121        $pageRenderer = $this->getPageRenderer();
122        $headerAssets = $this->view->renderSection('HeaderAssets', ['contentObject' => $this], true);
123        $footerAssets = $this->view->renderSection('FooterAssets', ['contentObject' => $this], true);
124        if (!empty(trim($headerAssets))) {
125            $pageRenderer->addHeaderData($headerAssets);
126        }
127        if (!empty(trim($footerAssets))) {
128            $pageRenderer->addFooterData($footerAssets);
129        }
130    }
131
132    /**
133     * Creating standalone view instance must not be done in construct() as
134     * it can lead to a nasty cache issue since content object instances
135     * are not always re-created by the content object rendered for every
136     * usage, but can be re-used. Thus, we need a fresh instance of
137     * StandaloneView every time render() is called.
138     */
139    protected function initializeStandaloneViewInstance()
140    {
141        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
142    }
143
144    /**
145     * Set template
146     *
147     * @param array $conf With possibly set file resource
148     * @throws \InvalidArgumentException
149     */
150    protected function setTemplate(array $conf)
151    {
152        // Fetch the Fluid template by templateName
153        if (
154            (!empty($conf['templateName']) || !empty($conf['templateName.']))
155            && !empty($conf['templateRootPaths.']) && is_array($conf['templateRootPaths.'])
156        ) {
157            $templateRootPaths = $this->applyStandardWrapToFluidPaths($conf['templateRootPaths.']);
158            $this->view->setTemplateRootPaths($templateRootPaths);
159            $templateName = isset($conf['templateName.'])
160                ? $this->cObj->stdWrap($conf['templateName'] ?? '', $conf['templateName.'])
161                : $conf['templateName'];
162            $this->view->setTemplate($templateName);
163        } elseif (!empty($conf['template']) && !empty($conf['template.'])) {
164            // Fetch the Fluid template by template cObject
165            $templateSource = $this->cObj->cObjGetSingle($conf['template'], $conf['template.'], 'template');
166            if ($templateSource === '') {
167                throw new ContentRenderingException(
168                    'Could not find template source for ' . $conf['template'],
169                    1437420865
170                );
171            }
172            $this->view->setTemplateSource($templateSource);
173        } else {
174            // Fetch the Fluid template by file stdWrap
175            $file = isset($conf['file.'])
176                ? $this->cObj->stdWrap($conf['file'] ?? '', $conf['file.'])
177                : ($conf['file'] ?? '');
178            // Get the absolute file name
179            $templatePathAndFilename = GeneralUtility::getFileAbsFileName($file);
180            $this->view->setTemplatePathAndFilename($templatePathAndFilename);
181        }
182    }
183
184    /**
185     * Set layout root path if given in configuration
186     *
187     * @param array $conf Configuration array
188     */
189    protected function setLayoutRootPath(array $conf)
190    {
191        // Override the default layout path via typoscript
192        $layoutPaths = [];
193        if (isset($conf['layoutRootPath']) || isset($conf['layoutRootPath.'])) {
194            $layoutRootPath = isset($conf['layoutRootPath.'])
195                ? $this->cObj->stdWrap($conf['layoutRootPath'], $conf['layoutRootPath.'])
196                : $conf['layoutRootPath'];
197            $layoutPaths[] = GeneralUtility::getFileAbsFileName($layoutRootPath);
198        }
199        if (isset($conf['layoutRootPaths.'])) {
200            $layoutPaths = array_replace($layoutPaths, $this->applyStandardWrapToFluidPaths($conf['layoutRootPaths.']));
201        }
202        if (!empty($layoutPaths)) {
203            $this->view->setLayoutRootPaths($layoutPaths);
204        }
205    }
206
207    /**
208     * Set partial root path if given in configuration
209     *
210     * @param array $conf Configuration array
211     */
212    protected function setPartialRootPath(array $conf)
213    {
214        $partialPaths = [];
215        if (isset($conf['partialRootPath']) || isset($conf['partialRootPath.'])) {
216            $partialRootPath = isset($conf['partialRootPath.'])
217                ? $this->cObj->stdWrap($conf['partialRootPath'], $conf['partialRootPath.'])
218                : $conf['partialRootPath'];
219            $partialPaths[] = GeneralUtility::getFileAbsFileName($partialRootPath);
220        }
221        if (isset($conf['partialRootPaths.'])) {
222            $partialPaths = array_replace($partialPaths, $this->applyStandardWrapToFluidPaths($conf['partialRootPaths.']));
223        }
224        if (!empty($partialPaths)) {
225            $this->view->setPartialRootPaths($partialPaths);
226        }
227    }
228
229    /**
230     * Set different format if given in configuration
231     *
232     * @param array $conf Configuration array
233     */
234    protected function setFormat(array $conf)
235    {
236        $format = isset($conf['format.'])
237            ? $this->cObj->stdWrap($conf['format'] ?? '', $conf['format.'])
238            : ($conf['format'] ?? '');
239        if ($format) {
240            $this->view->setFormat($format);
241        }
242    }
243
244    /**
245     * Set some extbase variables if given
246     *
247     * @param array $conf Configuration array
248     */
249    protected function setExtbaseVariables(array $conf)
250    {
251        /** @var \TYPO3\CMS\Extbase\Mvc\Request $request */
252        $requestPluginName = isset($conf['extbase.']['pluginName.'])
253            ? $this->cObj->stdWrap($conf['extbase.']['pluginName'] ?? '', $conf['extbase.']['pluginName.'])
254            : ($conf['extbase.']['pluginName'] ?? '');
255        if ($requestPluginName) {
256            $this->view->getRequest()->setPluginName($requestPluginName);
257        }
258        $requestControllerExtensionName = isset($conf['extbase.']['controllerExtensionName.'])
259            ? $this->cObj->stdWrap($conf['extbase.']['controllerExtensionName'] ?? '', $conf['extbase.']['controllerExtensionName.'])
260            : ($conf['extbase.']['controllerExtensionName'] ?? '');
261        if ($requestControllerExtensionName) {
262            $this->view->getRequest()->setControllerExtensionName($requestControllerExtensionName);
263        }
264        $requestControllerName = isset($conf['extbase.']['controllerName.'])
265            ? $this->cObj->stdWrap($conf['extbase.']['controllerName'] ?? '', $conf['extbase.']['controllerName.'])
266            : ($conf['extbase.']['controllerName'] ?? '');
267        if ($requestControllerName) {
268            $this->view->getRequest()->setControllerName($requestControllerName);
269        }
270        $requestControllerActionName = isset($conf['extbase.']['controllerActionName.'])
271            ? $this->cObj->stdWrap($conf['extbase.']['controllerActionName'] ?? '', $conf['extbase.']['controllerActionName.'])
272            : ($conf['extbase.']['controllerActionName'] ?? '');
273        if ($requestControllerActionName) {
274            $this->view->getRequest()->setControllerActionName($requestControllerActionName);
275        }
276
277        if (
278            $requestPluginName
279            && $requestControllerExtensionName
280            && $requestControllerName
281            && $requestControllerActionName
282        ) {
283            $configurationManager = GeneralUtility::makeInstance(ObjectManager::class)->get(ConfigurationManager::class);
284            $configurationManager->setConfiguration([
285                'extensionName' => $requestControllerExtensionName,
286                'pluginName' => $requestPluginName,
287            ]);
288
289            if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'])) {
290                $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'] = [
291                    $requestControllerName => [
292                        'actions' => [
293                            $requestControllerActionName,
294                        ],
295                    ],
296                ];
297            }
298
299            $requestBuilder = GeneralUtility::makeInstance(ObjectManager::class)->get(RequestBuilder::class);
300            $this->view->getRenderingContext()->getControllerContext()->setRequest($requestBuilder->build());
301        }
302    }
303
304    /**
305     * Compile rendered content objects in variables array ready to assign to the view
306     *
307     * @param array $conf Configuration array
308     * @return array the variables to be assigned
309     * @throws \InvalidArgumentException
310     */
311    protected function getContentObjectVariables(array $conf)
312    {
313        $variables = [];
314        $reservedVariables = ['data', 'current'];
315        // Accumulate the variables to be process and loop them through cObjGetSingle
316        $variablesToProcess = (array)($conf['variables.'] ?? []);
317        foreach ($variablesToProcess as $variableName => $cObjType) {
318            if (is_array($cObjType)) {
319                continue;
320            }
321            if (!in_array($variableName, $reservedVariables)) {
322                $variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $variablesToProcess[$variableName . '.'], 'variables.' . $variableName);
323            } else {
324                throw new \InvalidArgumentException(
325                    'Cannot use reserved name "' . $variableName . '" as variable name in FLUIDTEMPLATE.',
326                    1288095720
327                );
328            }
329        }
330        $variables['data'] = $this->cObj->data;
331        $variables['current'] = $this->cObj->data[$this->cObj->currentValKey ?? null] ?? null;
332        return $variables;
333    }
334
335    /**
336     * Set any TypoScript settings to the view. This is similar to a
337     * default MVC action controller in extbase.
338     *
339     * @param array $conf Configuration
340     */
341    protected function assignSettings(array $conf)
342    {
343        if (isset($conf['settings.'])) {
344            /** @var TypoScriptService $typoScriptService */
345            $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
346            $settings = $typoScriptService->convertTypoScriptArrayToPlainArray($conf['settings.']);
347            $this->view->assign('settings', $settings);
348        }
349    }
350
351    /**
352     * Render fluid standalone view
353     *
354     * @return string
355     */
356    protected function renderFluidView()
357    {
358        return $this->view->render();
359    }
360
361    /**
362     * Apply standard wrap to content
363     *
364     * @param string $content Rendered HTML content
365     * @param array $conf Configuration array
366     * @return string Standard wrapped content
367     */
368    protected function applyStandardWrapToRenderedContent($content, array $conf)
369    {
370        if (isset($conf['stdWrap.'])) {
371            $content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
372        }
373        return $content;
374    }
375
376    /**
377     * Applies stdWrap on Fluid path definitions
378     *
379     * @param array $paths
380     *
381     * @return array
382     */
383    protected function applyStandardWrapToFluidPaths(array $paths)
384    {
385        $finalPaths = [];
386        foreach ($paths as $key => $path) {
387            if (StringUtility::endsWith($key, '.')) {
388                if (isset($paths[substr($key, 0, -1)])) {
389                    continue;
390                }
391                $path = $this->cObj->stdWrap('', $path);
392            } elseif (isset($paths[$key . '.'])) {
393                $path = $this->cObj->stdWrap($path, $paths[$key . '.']);
394            }
395            $finalPaths[$key] = GeneralUtility::getFileAbsFileName($path);
396        }
397        return $finalPaths;
398    }
399}
400