Commit 528b90fe authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[TASK] Allow ServerRequestInterface in ext:fluid

This changes StandaloneView and RenderingContext
to accept instances of ServerRequestInterface or
no Request at all - in contrast to extbase Request
only.

This is possible with extbase Request implementing
ServerRequestInterface since v11.

The patch changes a couple of ViewHelpers like the
often used TranslateViewHelper: It can run without
triggering extbase magic, which especially avoids the
performance wise awful ConfigurationManager. Further
patches will refactor backend views to leverage this.

Internal method RenderingContext->getUriBuilder()
is obsoleted and removed along the way.

This is a powerful change since it drops the last hard
dependency to extbase in fluid and allows views without
extbase being involved at all.

Change-Id: I3b447b6f70e9ae6f94b981478cd8c4f43a86e9d4
Resolves: #96473
Related: #94428
Releases: main
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72758


Tested-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Stefan Bürk's avatarStefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 61de2f37
......@@ -94,10 +94,6 @@ parameters:
message: "#^Unsafe usage of new static\\(\\)\\.$#"
count: 1
path: typo3/sysext/workspaces/Classes/Domain/Record/WorkspaceRecord.php
-
message: "#^Call to an undefined static method TYPO3Fluid\\\\Fluid\\\\Core\\\\Rendering\\\\RenderingContext\\:\\:getParserConfiguration\\(\\)\\.$#"
count: 1
path: typo3/sysext/fluid/Classes/Core/Rendering/RenderingContext.php
# ignored errors for level 1
-
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -15,9 +17,9 @@
namespace TYPO3\CMS\Fluid\Core\Rendering;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Request;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Fluid\Core\ViewHelper\ViewHelperResolver;
use TYPO3\CMS\Fluid\View\TemplatePaths;
use TYPO3Fluid\Fluid\Core\Cache\FluidCacheInterface;
......@@ -29,15 +31,9 @@ use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperInvoker;
use TYPO3Fluid\Fluid\Core\ViewHelper\ViewHelperVariableContainer;
/**
* Class RenderingContext
*/
class RenderingContext extends \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext
{
/**
* @var Request
*/
protected $request;
protected ?ServerRequestInterface $request = null;
/**
* @var string
......@@ -58,130 +54,98 @@ class RenderingContext extends \TYPO3Fluid\Fluid\Core\Rendering\RenderingContext
array $templateProcessors,
array $expressionNodeTypes
) {
// Reproduced partial initialisation from parent::__construct; minus the custom implementations we attach below.
// Partially cloning parent::__construct() but with custom implementations.
$this->setTemplateParser(new TemplateParser());
$this->setTemplateCompiler(new TemplateCompiler());
$this->setViewHelperInvoker(new ViewHelperInvoker());
$this->setViewHelperVariableContainer(new ViewHelperVariableContainer());
$this->setVariableProvider(new StandardVariableProvider());
$this->setTemplateProcessors($templateProcessors);
$this->setExpressionNodeTypes($expressionNodeTypes);
$this->setTemplatePaths(GeneralUtility::makeInstance(TemplatePaths::class));
$this->setViewHelperResolver($viewHelperResolver);
$this->setCache($cache);
}
/**
* Alternative to buildParserConfiguration, called only in Fluid 3.0
*
* @return Configuration
*/
public function getParserConfiguration(): Configuration
{
$parserConfiguration = parent::getParserConfiguration();
$this->addInterceptorsToParserConfiguration($GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['interceptors'], $parserConfiguration);
return $parserConfiguration;
}
/**
* Build parser configuration
* Build parser configuration. Adds custom fluid interceptors from configuration.
*
* @return Configuration
* @throws \InvalidArgumentException if a class not implementing InterceptorInterface was registered
*/
public function buildParserConfiguration()
public function buildParserConfiguration(): Configuration
{
$parserConfiguration = parent::buildParserConfiguration();
$this->addInterceptorsToParserConfiguration($GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['interceptors'], $parserConfiguration);
return $parserConfiguration;
}
protected function addInterceptorsToParserConfiguration(iterable $interceptors, Configuration $parserConfiguration): void
{
foreach ($interceptors as $className) {
foreach ($GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['interceptors'] as $className) {
$interceptor = GeneralUtility::makeInstance($className);
if (!$interceptor instanceof InterceptorInterface) {
throw new \InvalidArgumentException('Interceptor "' . $className . '" needs to implement ' . InterceptorInterface::class . '.', 1462869795);
throw new \InvalidArgumentException(
'Interceptor "' . $className . '" needs to implement ' . InterceptorInterface::class . '.',
1462869795
);
}
$parserConfiguration->addInterceptor($interceptor);
}
return $parserConfiguration;
}
/**
* @param string $action
*/
public function setControllerAction($action)
public function setControllerAction($action): void
{
$dotPosition = strpos($action, '.');
if ($dotPosition !== false) {
$action = substr($action, 0, $dotPosition);
}
$this->controllerAction = $action;
if ($this->request) {
if ($this->request instanceof RequestInterface) {
// @todo: Avoid altogether?!
$this->request->setControllerActionName(lcfirst($action));
}
}
/**
* @param string $controllerName
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException
*/
public function setControllerName($controllerName)
public function setControllerName($controllerName): void
{
$this->controllerName = $controllerName;
if ($this->request instanceof Request) {
if ($this->request instanceof RequestInterface) {
// @todo: Avoid altogether?!
$this->request->setControllerName($controllerName);
}
}
/**
* @return string
*/
public function getControllerName()
public function getControllerName(): string
{
return $this->request instanceof Request ? $this->request->getControllerName() : $this->controllerName;
// @todo: Why fallback to request here? This is not consistent!
return $this->request instanceof RequestInterface ? $this->request->getControllerName() : $this->controllerName;
}
/**
* @return string
*/
public function getControllerAction()
public function getControllerAction(): string
{
return $this->request instanceof Request ? $this->request->getControllerActionName() : $this->controllerAction;
// @todo: Why fallback to request here? This is not consistent!
return $this->request instanceof RequestInterface ? $this->request->getControllerActionName() : $this->controllerAction;
}
/**
* @param Request $request
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidControllerNameException
* @internal this might change to use a PSR-7 compliant request
* It is currently allowed to setRequest(null) to unset a
* request object created by factories. Some tests use this
* to make sure no extbase request is set. This may change.
*/
public function setRequest(Request $request): void
public function setRequest(?ServerRequestInterface $request): void
{
$this->request = $request;
$this->setControllerAction($request->getControllerActionName());
$this->setControllerName($request->getControllerName());
if ($request instanceof RequestInterface) {
// Set magic if this is an extbase request
$this->setControllerAction($request->getControllerActionName());
$this->setControllerName($request->getControllerName());
}
}
/**
* @return Request
* @internal this might change to use a PSR-7 compliant request
*/
public function getRequest(): Request
public function getRequest(): ?ServerRequestInterface
{
return $this->request;
}
/**
* @return UriBuilder
* @internal this is subject to change
*/
public function getUriBuilder(): UriBuilder
{
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$uriBuilder->setRequest($this->request);
return $uriBuilder;
}
}
......@@ -29,7 +29,7 @@ use TYPO3Fluid\Fluid\View\Exception\InvalidTemplateResourceException;
abstract class AbstractTemplateView extends Typo3FluidAbstractTemplateView
{
/**
* @param RenderingContextInterface $context
* @param RenderingContextInterface|null $context
* @internal
*/
public function __construct(RenderingContextInterface $context = null)
......
......@@ -15,6 +15,7 @@
namespace TYPO3\CMS\Fluid\View;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Request;
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext;
......@@ -30,6 +31,10 @@ class StandaloneView extends AbstractTemplateView
public function __construct()
{
$renderingContext = GeneralUtility::makeInstance(RenderingContextFactory::class)->create();
// @todo: This is very unfortunate. This creates an extbase request by default. Standalone
// usage is typically *not* extbase context. Controllers that want to get rid of this
// have to ->setRequest($myServerRequestInterface), or even ->setRequest(null) after
// object construction to get rid of an extbase request again.
$renderingContext->setRequest(GeneralUtility::makeInstance(Request::class));
parent::__construct($renderingContext);
}
......@@ -55,6 +60,7 @@ class StandaloneView extends AbstractTemplateView
*
* @return string $format
* @throws \RuntimeException
* @todo: deprecate?!
*/
public function getFormat()
{
......@@ -64,14 +70,24 @@ class StandaloneView extends AbstractTemplateView
throw new \RuntimeException('The rendering context must be of type ' . RenderingContext::class, 1482251887);
}
/**
* @internal Currently used especially in functional tests. May change.
*/
public function setRequest(?ServerRequestInterface $request = null): void
{
if ($this->baseRenderingContext instanceof RenderingContext) {
$this->baseRenderingContext->setRequest($request);
}
}
/**
* Returns the current request object
*
* @return \TYPO3\CMS\Extbase\Mvc\Request
* @throws \RuntimeException
* @internal
* @todo: deprecate?!
*/
public function getRequest()
public function getRequest(): ?ServerRequestInterface
{
if ($this->baseRenderingContext instanceof RenderingContext) {
return $this->baseRenderingContext->getRequest();
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -29,10 +31,8 @@ abstract class AbstractBackendViewHelper extends AbstractViewHelper
/**
* Gets instance of template if exists or create a new one.
* Saves instance in viewHelperVariableContainer
*
* @return ModuleTemplate
*/
public function getModuleTemplate()
public function getModuleTemplate(): ModuleTemplate
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(self::class, 'ModuleTemplate')) {
......@@ -47,10 +47,8 @@ abstract class AbstractBackendViewHelper extends AbstractViewHelper
/**
* Gets instance of PageRenderer if exists or create a new one.
* Saves instance in viewHelperVariableContainer
*
* @return PageRenderer
*/
public function getPageRenderer()
public function getPageRenderer(): PageRenderer
{
$viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer();
if ($viewHelperVariableContainer->exists(self::class, 'PageRenderer')) {
......@@ -59,7 +57,6 @@ abstract class AbstractBackendViewHelper extends AbstractViewHelper
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$viewHelperVariableContainer->add(self::class, 'PageRenderer', $pageRenderer);
}
return $pageRenderer;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -16,6 +18,7 @@
namespace TYPO3\CMS\Fluid\ViewHelpers\Be\Buttons;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Fluid\ViewHelpers\Be\AbstractBackendViewHelper;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
......@@ -62,12 +65,7 @@ final class CshViewHelper extends AbstractBackendViewHelper
*/
protected $escapeOutput = false;
/**
* Initialize arguments.
*
* @throws \TYPO3Fluid\Fluid\Core\ViewHelper\Exception
*/
public function initializeArguments()
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('table', 'string', 'Table name (\'_MOD_\'+module name). If not set, the current module name will be used');
......@@ -75,35 +73,31 @@ final class CshViewHelper extends AbstractBackendViewHelper
$this->registerArgument('wrap', 'string', 'Markup to wrap around the CSH, split by "|"', false, '');
}
/**
* Render context sensitive help (CSH) for the given table
*
* @return string the rendered CSH icon
*/
public function render()
public function render(): string
{
return static::renderStatic(
$this->arguments,
$this->buildRenderChildrenClosure(),
$this->renderingContext
);
return self::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext);
}
/**
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
* @return string
* @throws \RuntimeException
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
$table = $arguments['table'];
$field = $arguments['field'];
$wrap = $arguments['wrap'];
if ($table === null) {
$currentRequest = $renderingContext->getRequest();
$moduleName = $currentRequest->getPluginName();
$request = $renderingContext->getRequest();
if (!$request instanceof RequestInterface) {
// Throw if not an extbase request
throw new \RuntimeException(
'ViewHelper f:be.buttons.csh needs an extbase Request object to resolve module name magically.'
. ' When not in extbase context, attribute "table" is required to be set to something like "_MOD_my_module_name"',
1639740545
);
}
$moduleName = $request->getPluginName();
$table = '_MOD_' . $moduleName;
}
$content = (string)$renderChildrenClosure();
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -17,6 +19,7 @@ namespace TYPO3\CMS\Fluid\ViewHelpers\Be\Labels;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Fluid\ViewHelpers\Be\AbstractBackendViewHelper;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
......@@ -55,21 +58,7 @@ final class CshViewHelper extends AbstractBackendViewHelper
*/
protected $escapeOutput = false;
/**
* Returns the Language Service
* @return LanguageService
*/
protected static function getLanguageService()
{
return $GLOBALS['LANG'];
}
/**
* Initialize arguments.
*
* @throws \TYPO3Fluid\Fluid\Core\ViewHelper\Exception
*/
public function initializeArguments()
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('table', 'string', 'Table name (\'_MOD_\'+module name). If not set, the current module name will be used');
......@@ -77,42 +66,39 @@ final class CshViewHelper extends AbstractBackendViewHelper
$this->registerArgument('label', 'string', 'Language label which is wrapped with the CSH', false, '');
}
/**
* Render context sensitive help (CSH) for the given table
*
* @return string the rendered CSH icon
*/
public function render()
public function render(): string
{
return static::renderStatic(
$this->arguments,
$this->buildRenderChildrenClosure(),
$this->renderingContext
);
return self::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext);
}
/**
* @param array $arguments
* @param \Closure $renderChildrenClosure
* @param RenderingContextInterface $renderingContext
* @return string
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
$table = $arguments['table'];
$field = $arguments['field'];
$label = $arguments['label'];
if ($table === null) {
$currentRequest = $renderingContext->getRequest();
$moduleName = $currentRequest->getPluginName();
$request = $renderingContext->getRequest();
if (!$request instanceof RequestInterface) {
// Throw if not an extbase request
throw new \RuntimeException(
'ViewHelper f:be.labels.csh needs an extbase Request object to resolve module name magically.'
. ' When not in extbase context, attribute "table" is required to be set to something like "_MOD_my_module_name"',
1639759760
);
}
$moduleName = $request->getPluginName();
$table = '_MOD_' . $moduleName;
}
if (strpos($label, 'LLL:') === 0) {
$label = self::getLanguageService()->sL($label);
}
// Double encode can be set to true, once the typo3fluid/fluid fix is released and required
$label = '<label>' . htmlspecialchars($label, ENT_QUOTES, '', false) . '</label>';
$label = '<label>' . htmlspecialchars($label, ENT_QUOTES, '', true) . '</label>';
return BackendUtility::wrapInHelp($table, $field, $label);
}
protected static function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
......@@ -17,12 +19,14 @@ namespace TYPO3\CMS\Fluid\ViewHelpers\Be\Menus;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Extbase\Mvc\Web\Routing\UriBuilder;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
/**
* ViewHelper which returns an option tag.
* This ViewHelper only works in conjunction with :php:`\TYPO3\CMS\Fluid\ViewHelpers\Be\Menus\ActionMenuViewHelper`.
* This ViewHelper is tailored to be used only in extbase context.
*
* .. note::
* This ViewHelper is experimental!
......@@ -38,7 +42,7 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
* <f:be.menus.actionMenuItem label="List Posts" controller="Post" action="index" arguments="{blog: blog}" />
* </f:be.menus.actionMenu>
*
* Selectbox with the options "Overview", "Create new Blog" and "List Posts".
* Select box with the options "Overview", "Create new Blog" and "List Posts".
*
* Localized::
*
......@@ -47,7 +51,7 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
* <f:be.menus.actionMenuItem label="{f:translate(key='create_blog')}" controller="Blog" action="new" />
* </f:be.menus.actionMenu>
*
* Localized selectbox.
* Localized select box.
*/
final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
{
......@@ -56,12 +60,7 @@ final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
*/
protected $tagName = 'option';
/**
* Initialize arguments.
*
* @throws \TYPO3Fluid\Fluid\Core\ViewHelper\Exception
*/
public function initializeArguments()
public function initializeArguments(): void
{
parent::initializeArguments();
$this->registerArgument('label', 'string', 'label of the option tag', true);
......@@ -70,13 +69,7 @@ final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
$this->registerArgument('arguments', 'array', 'additional controller arguments to be passed to the action when this ActionMenuItem is selected', false, []);
}
/**
* Renders an ActionMenu option tag
*
* @return string the rendered option tag
* @see \TYPO3\CMS\Fluid\ViewHelpers\Be\Menus\ActionMenuViewHelper
*/
public function render()
public function render(): string
{
$label = $this->arguments['label'];
$controller = $this->arguments['controller'];
......@@ -84,7 +77,15 @@ final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
$arguments = $this->arguments['arguments'];
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$uriBuilder->setRequest($this->renderingContext->getRequest());
$request = $this->renderingContext->getRequest();
if (!$request instanceof RequestInterface) {
// Throw if not an extbase request
throw new \RuntimeException(
'ViewHelper f:be.menus.actionMenuItem needs an extbase Request object to create URIs.',
1639741792
);
}
$uriBuilder->setRequest($request);
$uri = $uriBuilder->reset()->uriFor($action, $arguments, $controller);
$this->tag->addAttribute('value', $uri);
......@@ -93,21 +94,18 @@ final class ActionMenuItemViewHelper extends AbstractTagBasedViewHelper
$this->evaluateSelectItemState($controller, $action, $arguments);
}
$this->tag->setContent(
// Double encode can be set to true, once the typo3fluid/fluid fix is released and required
htmlspecialchars($label, ENT_QUOTES, '', false)
);
$this->tag->setContent(htmlspecialchars($label, ENT_QUOTES, '', true));
return $this->tag->render();
}
protected function evaluateSelectItemState(string $controller, string $action, array $arguments): void
{
$currentRequest = $this->renderingContext->getRequest();
$request = $this->renderingContext->getRequest();
$flatRequestArguments = ArrayUtility::flattenPlain(
array_merge([
'controller' => $currentRequest->getControllerName(),
'action' => $currentRequest->getControllerActionName(),
], $currentRequest->getArguments())
'controller' => $request->getControllerName(),
'action' => $request->getControllerActionName(),