[BUGFIX] Better error messages for FLUIDTEMPLATE object
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / ContentObject / FluidTemplateContentObject.php
1 <?php
2 namespace 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
17 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\StringUtility;
20 use TYPO3\CMS\Extbase\Configuration\ConfigurationManager;
21 use TYPO3\CMS\Extbase\Mvc\Web\RequestBuilder;
22 use TYPO3\CMS\Extbase\Object\ObjectManager;
23 use TYPO3\CMS\Fluid\View\StandaloneView;
24 use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
25
26 /**
27 * Contains FLUIDTEMPLATE class object
28 */
29 class FluidTemplateContentObject extends AbstractContentObject
30 {
31 /**
32 * @var StandaloneView
33 */
34 protected $view = null;
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(isset($conf['templateName']) ? $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.']);
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.']) ? $this->cObj->stdWrap($conf['file'], $conf['file.']) : $conf['file'];
176 /** @var $templateService \TYPO3\CMS\Core\TypoScript\TemplateService */
177 $templateService = $GLOBALS['TSFE']->tmpl;
178 $templatePathAndFilename = $templateService->getFileName($file);
179 $this->view->setTemplatePathAndFilename(PATH_site . $templatePathAndFilename);
180 }
181 }
182
183 /**
184 * Set layout root path if given in configuration
185 *
186 * @param array $conf Configuration array
187 */
188 protected function setLayoutRootPath(array $conf)
189 {
190 // Override the default layout path via typoscript
191 $layoutPaths = [];
192 if (isset($conf['layoutRootPath']) || isset($conf['layoutRootPath.'])) {
193 $layoutRootPath = isset($conf['layoutRootPath.'])
194 ? $this->cObj->stdWrap($conf['layoutRootPath'], $conf['layoutRootPath.'])
195 : $conf['layoutRootPath'];
196 $layoutPaths[] = GeneralUtility::getFileAbsFileName($layoutRootPath);
197 }
198 if (isset($conf['layoutRootPaths.'])) {
199 $layoutPaths = array_replace($layoutPaths, $this->applyStandardWrapToFluidPaths($conf['layoutRootPaths.']));
200 }
201 if (!empty($layoutPaths)) {
202 $this->view->setLayoutRootPaths($layoutPaths);
203 }
204 }
205
206 /**
207 * Set partial root path if given in configuration
208 *
209 * @param array $conf Configuration array
210 */
211 protected function setPartialRootPath(array $conf)
212 {
213 $partialPaths = [];
214 if (isset($conf['partialRootPath']) || isset($conf['partialRootPath.'])) {
215 $partialRootPath = isset($conf['partialRootPath.'])
216 ? $this->cObj->stdWrap($conf['partialRootPath'], $conf['partialRootPath.'])
217 : $conf['partialRootPath'];
218 $partialPaths[] = GeneralUtility::getFileAbsFileName($partialRootPath);
219 }
220 if (isset($conf['partialRootPaths.'])) {
221 $partialPaths = array_replace($partialPaths, $this->applyStandardWrapToFluidPaths($conf['partialRootPaths.']));
222 }
223 if (!empty($partialPaths)) {
224 $this->view->setPartialRootPaths($partialPaths);
225 }
226 }
227
228 /**
229 * Set different format if given in configuration
230 *
231 * @param array $conf Configuration array
232 */
233 protected function setFormat(array $conf)
234 {
235 $format = isset($conf['format.']) ? $this->cObj->stdWrap($conf['format'], $conf['format.']) : $conf['format'];
236 if ($format) {
237 $this->view->setFormat($format);
238 }
239 }
240
241 /**
242 * Set some extbase variables if given
243 *
244 * @param array $conf Configuration array
245 */
246 protected function setExtbaseVariables(array $conf)
247 {
248 /** @var $request \TYPO3\CMS\Extbase\Mvc\Request */
249 $requestPluginName = isset($conf['extbase.']['pluginName.']) ? $this->cObj->stdWrap($conf['extbase.']['pluginName'], $conf['extbase.']['pluginName.']) : $conf['extbase.']['pluginName'];
250 if ($requestPluginName) {
251 $this->view->getRequest()->setPluginName($requestPluginName);
252 }
253 $requestControllerExtensionName = isset($conf['extbase.']['controllerExtensionName.']) ? $this->cObj->stdWrap($conf['extbase.']['controllerExtensionName'], $conf['extbase.']['controllerExtensionName.']) : $conf['extbase.']['controllerExtensionName'];
254 if ($requestControllerExtensionName) {
255 $this->view->getRequest()->setControllerExtensionName($requestControllerExtensionName);
256 }
257 $requestControllerName = isset($conf['extbase.']['controllerName.']) ? $this->cObj->stdWrap($conf['extbase.']['controllerName'], $conf['extbase.']['controllerName.']) : $conf['extbase.']['controllerName'];
258 if ($requestControllerName) {
259 $this->view->getRequest()->setControllerName($requestControllerName);
260 }
261 $requestControllerActionName = isset($conf['extbase.']['controllerActionName.']) ? $this->cObj->stdWrap($conf['extbase.']['controllerActionName'], $conf['extbase.']['controllerActionName.']) : $conf['extbase.']['controllerActionName'];
262 if ($requestControllerActionName) {
263 $this->view->getRequest()->setControllerActionName($requestControllerActionName);
264 }
265
266 if (
267 $requestPluginName
268 && $requestControllerExtensionName
269 && $requestControllerName
270 && $requestControllerActionName
271 ) {
272 $configurationManager = GeneralUtility::makeInstance(ObjectManager::class)->get(ConfigurationManager::class);
273 $configurationManager->setConfiguration([
274 'extensionName' => $requestControllerExtensionName,
275 'pluginName' => $requestPluginName,
276 ]);
277
278 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'])) {
279 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$requestControllerExtensionName]['plugins'][$requestPluginName]['controllers'] = [
280 $requestControllerName => [
281 'actions' => [
282 $requestControllerActionName,
283 ],
284 ],
285 ];
286 }
287
288 $requestBuilder = GeneralUtility::makeInstance(ObjectManager::class)->get(RequestBuilder::class);
289 $this->view->getRenderingContext()->getControllerContext()->setRequest($requestBuilder->build());
290 }
291 }
292
293 /**
294 * Compile rendered content objects in variables array ready to assign to the view
295 *
296 * @param array $conf Configuration array
297 * @return array the variables to be assigned
298 * @throws \InvalidArgumentException
299 */
300 protected function getContentObjectVariables(array $conf)
301 {
302 $variables = [];
303 $reservedVariables = ['data', 'current'];
304 // Accumulate the variables to be process and loop them through cObjGetSingle
305 $variablesToProcess = (array)$conf['variables.'];
306 foreach ($variablesToProcess as $variableName => $cObjType) {
307 if (is_array($cObjType)) {
308 continue;
309 }
310 if (!in_array($variableName, $reservedVariables)) {
311 $variables[$variableName] = $this->cObj->cObjGetSingle($cObjType, $variablesToProcess[$variableName . '.']);
312 } else {
313 throw new \InvalidArgumentException(
314 'Cannot use reserved name "' . $variableName . '" as variable name in FLUIDTEMPLATE.',
315 1288095720
316 );
317 }
318 }
319 $variables['data'] = $this->cObj->data;
320 $variables['current'] = $this->cObj->data[$this->cObj->currentValKey];
321 return $variables;
322 }
323
324 /**
325 * Set any TypoScript settings to the view. This is similar to a
326 * default MVC action controller in extbase.
327 *
328 * @param array $conf Configuration
329 */
330 protected function assignSettings(array $conf)
331 {
332 if (isset($conf['settings.'])) {
333 /** @var $typoScriptService TypoScriptService */
334 $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
335 $settings = $typoScriptService->convertTypoScriptArrayToPlainArray($conf['settings.']);
336 $this->view->assign('settings', $settings);
337 }
338 }
339
340 /**
341 * Render fluid standalone view
342 *
343 * @return string
344 */
345 protected function renderFluidView()
346 {
347 return $this->view->render();
348 }
349
350 /**
351 * Apply standard wrap to content
352 *
353 * @param string $content Rendered HTML content
354 * @param array $conf Configuration array
355 * @return string Standard wrapped content
356 */
357 protected function applyStandardWrapToRenderedContent($content, array $conf)
358 {
359 if (isset($conf['stdWrap.'])) {
360 $content = $this->cObj->stdWrap($content, $conf['stdWrap.']);
361 }
362 return $content;
363 }
364
365 /**
366 * Applies stdWrap on Fluid path definitions
367 *
368 * @param array $paths
369 *
370 * @return array
371 */
372 protected function applyStandardWrapToFluidPaths(array $paths)
373 {
374 $finalPaths = [];
375 foreach ($paths as $key => $path) {
376 if (StringUtility::endsWith($key, '.')) {
377 if (isset($paths[substr($key, 0, -1)])) {
378 continue;
379 }
380 $path = $this->cObj->stdWrap('', $path);
381 } elseif (isset($paths[$key . '.'])) {
382 $path = $this->cObj->stdWrap($path, $paths[$key . '.']);
383 }
384 $finalPaths[$key] = GeneralUtility::getFileAbsFileName($path);
385 }
386 return $finalPaths;
387 }
388 }