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