Skip to content

Commit

Permalink
[FEATURE] Make PSR-7 Request available in ContentObjectRenderer
Browse files Browse the repository at this point in the history
This allows to add the current request into ALL userFunctions, including
Extbase, where we can then convert the request into an Extbase Request.

Plus, it is injected in all ContentObjects, making it available for subsequent
renderings.

Resolves: #92984
Releases: master
Change-Id: I7ac6872db6ea0ed8838a0d63c18b5fa53407ebed
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/66080
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Tested-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
bmack committed Dec 5, 2020
1 parent 785ff68 commit 8cec795
Show file tree
Hide file tree
Showing 24 changed files with 156 additions and 28 deletions.
@@ -0,0 +1,55 @@
.. include:: ../../Includes.txt

====================================================================
Feature: #92984 - PSR-7 Request available in Frontend ContentObjects
====================================================================

See :issue:`92984`

Description
===========

The main Request object of a web-based PHP process is now handed into all
ContentObjects and ContentObjectRenderer classes.

In addition, any kind of "userFunc" methods initiated from ContentObjectRenderer,
basically all custom Frontend PHP code, now receive the request object that was
handed in as third method argument.

The ContentObjectRenderer API now has a :php: `getRequest()` method.

Example:

.. code-block:: typoscript
page.10 = USER
page.10.userFunc = MyVendor\MyPackage\Frontend\MyClass->myMethod
.. code-block:: php
<?php
namespace MyVendor\MyPackage\Frontend;
class MyClass
{
public function myMethod(string $content, array $configuration, ServerRequestInterface $request)
{
$myValue = $request->getQueryParams()['myGetParameter'];
$normalizedParams = $request->getAttribute('normalizedParams');
}
}
This functionality should be used in PHP code related to Frontend code instead of
the superglobal variables like `$_GET` / `$_POST` / `$_SERVER`, or TYPO3's
API methods `GeneralUtility::_GP()` and `GeneralUtility::getIndpEnv()`.

Impact
======

Any kind of custom Content Object in PHP code can now access the PSR-7 Request
object to fetch information about the current request, making TYPO3 Frontend
aware of PSR-7 standardized request objects.

.. index:: Frontend, PHP-API, ext:frontend
7 changes: 4 additions & 3 deletions typo3/sysext/extbase/Classes/Core/Bootstrap.php
Expand Up @@ -174,16 +174,17 @@ private function initializeRequestHandlersConfiguration(): void
* @param array $configuration The TS configuration array
* @return string $content The processed content
*/
public function run(string $content, array $configuration): string
public function run(string $content, array $configuration, ?ServerRequestInterface $request = null): string
{
$request = $request ?? $GLOBALS['TYPO3_REQUEST'];
$this->initialize($configuration);
return $this->handleRequest();
return $this->handleRequest($request);
}

/**
* @return string
*/
protected function handleRequest(): string
protected function handleRequest(ServerRequestInterface $request): string
{
$extbaseRequest = $this->extbaseRequestBuilder->build();
$requestHandler = $this->requestHandlerResolver->resolveRequestHandler($extbaseRequest);
Expand Down
Expand Up @@ -15,6 +15,7 @@

namespace TYPO3\CMS\Frontend\ContentObject;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;

Expand All @@ -33,6 +34,8 @@ abstract class AbstractContentObject
*/
protected $pageRenderer;

protected ?ServerRequestInterface $request = null;

/**
* Default constructor.
*
Expand Down Expand Up @@ -61,6 +64,11 @@ public function getContentObjectRenderer()
return $this->cObj;
}

public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}

/**
* @return PageRenderer
*/
Expand Down
Expand Up @@ -88,7 +88,7 @@ public function render($conf = [])
$cObj->parentRecordNumber = $this->cObj->currentRecordNumber;
$frontendController->currentRecord = $conf['table'] . ':' . $row['uid'];
$this->cObj->lastChanged($row['tstamp']);
$cObj->start($row, $conf['table']);
$cObj->start($row, $conf['table'], $this->request);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$cobjValue .= $tmpValue;
}
Expand Down
Expand Up @@ -18,6 +18,7 @@
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Driver\Statement;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
Expand Down Expand Up @@ -427,6 +428,13 @@ class ContentObjectRenderer implements LoggerAwareInterface
*/
protected $typoScriptFrontendController;

/**
* Request pointer, if injected. Use getRequest() instead of reading this property directly.
*
* @var ServerRequestInterface|null
*/
private ?ServerRequestInterface $request = null;

/**
* Indicates that object type is USER.
*
Expand All @@ -451,6 +459,11 @@ public function __construct(TypoScriptFrontendController $typoScriptFrontendCont
$this->container = $container;
}

public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}

/**
* Prevent several objects from being serialized.
* If currentFile is set, it is either a File or a FileReference object. As the object itself can't be serialized,
Expand All @@ -461,7 +474,7 @@ public function __construct(TypoScriptFrontendController $typoScriptFrontendCont
public function __sleep()
{
$vars = get_object_vars($this);
unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container']);
unset($vars['typoScriptFrontendController'], $vars['logger'], $vars['container'], $vars['request']);
if ($this->currentFile instanceof FileReference) {
$this->currentFile = 'FileReference:' . $this->currentFile->getUid();
} elseif ($this->currentFile instanceof File) {
Expand Down Expand Up @@ -496,6 +509,10 @@ public function __wakeup()
}
$this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$this->container = GeneralUtility::getContainer();

// We do not derive $this->request from globals here. The request is expected to be injected
// using setRequest() after deserialization or with start().
// (A fallback to $GLOBALS['TYPO3_REQUEST'] is available in getRequest() for BC)
}

/**
Expand Down Expand Up @@ -534,9 +551,11 @@ public function registerContentObjectClass($className, $contentObjectName)
*
* @param array $data The record data that is rendered.
* @param string $table The table that the data record is from.
* @param ServerRequestInterface|null $request
*/
public function start($data, $table = '')
public function start($data, $table = '', ?ServerRequestInterface $request = null)
{
$this->request = $request ?? $this->request;
$this->data = $data;
$this->table = $table;
$this->currentRecord = $table !== ''
Expand Down Expand Up @@ -728,6 +747,7 @@ public function getContentObject($name)
if (!($contentObject instanceof AbstractContentObject)) {
throw new ContentRenderingException(sprintf('Registered content object class name "%s" must be an instance of AbstractContentObject, but is not!', $fullyQualifiedClassName), 1422564295);
}
$contentObject->setRequest($this->getRequest());
return $contentObject;
}

Expand Down Expand Up @@ -5176,7 +5196,8 @@ public function callUserFunction($funcName, $conf, $content)
$classObj->cObj = $this;
$content = call_user_func_array($callable, [
$content,
$conf
$conf,
$this->getRequest()
]);
} else {
$this->getTimeTracker()->setTSlogMessage('Method "' . $parts[1] . '" did not exist in class "' . $parts[0] . '"', 3);
Expand Down Expand Up @@ -6647,4 +6668,17 @@ protected function getContentLengthOfCurrentTag(string $theValue, int $pointer,

return $endingOffset;
}

public function getRequest(): ServerRequestInterface
{
if ($this->request instanceof ServerRequestInterface) {
return $this->request;
}

if (isset($GLOBALS['TYPO3_REQUEST']) && $GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
return $GLOBALS['TYPO3_REQUEST'];
}

throw new ContentRenderingException('PSR-7 request is missing in ContentObjectRenderer. Inject with start(), setRequest() or provide via $GLOBALS[\'TYPO3_REQUEST\'].', 1607172972);
}
}
Expand Up @@ -46,7 +46,7 @@ public function render($conf = [])
$GLOBALS['TSFE']->register['count_HMENU_MENUOBJ'] = 0;
$GLOBALS['TSFE']->register['count_MENUOBJ'] = 0;
$menu->parent_cObj = $this->cObj;
$menu->start($GLOBALS['TSFE']->tmpl, $GLOBALS['TSFE']->sys_page, '', $conf, 1);
$menu->start($GLOBALS['TSFE']->tmpl, $GLOBALS['TSFE']->sys_page, '', $conf, 1, '', $this->request);
$menu->makeMenu();
$theValue .= $menu->writeMenu();
} catch (NoSuchMenuTypeException $e) {
Expand Down
Expand Up @@ -15,6 +15,7 @@

namespace TYPO3\CMS\Frontend\ContentObject\Menu;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspect;
Expand Down Expand Up @@ -175,6 +176,8 @@ abstract class AbstractMenuContentObject
*/
protected $WMcObj;

protected ?ServerRequestInterface $request = null;

/**
* Can be set to contain menu item arrays for sub-levels.
*
Expand Down Expand Up @@ -219,15 +222,17 @@ abstract class AbstractMenuContentObject
* @param array $conf The TypoScript configuration for the HMENU cObject
* @param int $menuNumber Menu number; 1,2,3. Should probably be 1
* @param string $objSuffix Submenu Object suffix. This offers submenus a way to use alternative configuration for specific positions in the menu; By default "1 = TMENU" would use "1." for the TMENU configuration, but if this string is set to eg. "a" then "1a." would be used for configuration instead (while "1 = " is still used for the overall object definition of "TMENU")
* @param ServerRequestInterface|null $request
* @return bool Returns TRUE on success
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::HMENU()
*/
public function start($tmpl, $sys_page, $id, $conf, $menuNumber, $objSuffix = '')
public function start($tmpl, $sys_page, $id, $conf, $menuNumber, $objSuffix = '', ?ServerRequestInterface $request = null)
{
$tsfe = $this->getTypoScriptFrontendController();
$this->conf = $conf;
$this->menuNumber = $menuNumber;
$this->mconf = $conf[$this->menuNumber . $objSuffix . '.'];
$this->request = $request;
$this->WMcObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
// Sets the internal vars. $tmpl MUST be the template-object. $sys_page MUST be the PageRepository object
if ($this->conf[$this->menuNumber . $objSuffix] && is_object($tmpl) && is_object($sys_page)) {
Expand Down
Expand Up @@ -66,7 +66,7 @@ public function writeMenu()
$GLOBALS['TSFE']->register['count_HMENU_MENUOBJ']++;
$GLOBALS['TSFE']->register['count_MENUOBJ']++;
// Initialize the cObj with the page record of the menu item
$this->WMcObj->start($this->menuArr[$key], 'pages');
$this->WMcObj->start($this->menuArr[$key], 'pages', $this->request);
$this->I = [];
$this->I['key'] = $key;
$this->I['val'] = $val;
Expand Down
Expand Up @@ -115,7 +115,7 @@ public function render($conf = [])
$cObj->parentRecordNumber = $this->cObj->currentRecordNumber;
$GLOBALS['TSFE']->currentRecord = $val['table'] . ':' . $val['id'];
$this->cObj->lastChanged($row['tstamp']);
$cObj->start($row, $val['table']);
$cObj->start($row, $val['table'], $this->request);
$tmpValue = $cObj->cObjGetSingle($renderObjName, $renderObjConf, $renderObjKey);
$theValue .= $tmpValue;
}
Expand Down
Expand Up @@ -2489,7 +2489,7 @@ public function preparePageContentGeneration(ServerRequestInterface $request)
}

// Global content object
$this->newCObj();
$this->newCObj($request);
$this->getTimeTracker()->pull();
}

Expand Down Expand Up @@ -2647,9 +2647,12 @@ protected function getWebsiteTitle(): string

/**
* Processes the INTinclude-scripts
*
* @param ServerRequestInterface|null $request
*/
public function INTincScript()
public function INTincScript(ServerRequestInterface $request = null)
{
$request = $request ?? $GLOBALS['TYPO3_REQUEST'];
$this->additionalHeaderData = $this->config['INTincScript_ext']['additionalHeaderData'] ?? [];
$this->additionalFooterData = $this->config['INTincScript_ext']['additionalFooterData'] ?? [];
if (empty($this->config['INTincScript_ext']['pageRenderer'])) {
Expand All @@ -2665,7 +2668,7 @@ public function INTincScript()
GeneralUtility::makeInstance(AssetCollector::class)->updateState($assetCollector->getState());
}

$this->recursivelyReplaceIntPlaceholdersInContent();
$this->recursivelyReplaceIntPlaceholdersInContent($request);
$this->getTimeTracker()->push('Substitute header section');
$this->INTincScript_loadJSCode();
$this->generatePageTitle();
Expand All @@ -2682,7 +2685,7 @@ public function INTincScript()
$this->pageRenderer->renderJavaScriptAndCssForProcessingOfUncachedContentObjects($this->content, $this->config['INTincScript_ext']['divKey'])
);
// Replace again, because header and footer data and page renderer replacements may introduce additional placeholders (see #44825)
$this->recursivelyReplaceIntPlaceholdersInContent();
$this->recursivelyReplaceIntPlaceholdersInContent($request);
$this->setAbsRefPrefix();
$this->getTimeTracker()->pull();
}
Expand All @@ -2692,11 +2695,11 @@ public function INTincScript()
* In case the replacement adds additional placeholders, it loops
* until no new placeholders are found any more.
*/
protected function recursivelyReplaceIntPlaceholdersInContent()
protected function recursivelyReplaceIntPlaceholdersInContent(ServerRequestInterface $request)
{
do {
$nonCacheableData = $this->config['INTincScript'];
$this->processNonCacheableContentPartsAndSubstituteContentMarkers($nonCacheableData);
$this->processNonCacheableContentPartsAndSubstituteContentMarkers($nonCacheableData, $request);
// Check if there were new items added to INTincScript during the previous execution:
// array_diff_assoc throws notices if values are arrays but not strings. We suppress this here.
$nonCacheableData = @array_diff_assoc($this->config['INTincScript'], $nonCacheableData);
Expand All @@ -2713,7 +2716,7 @@ protected function recursivelyReplaceIntPlaceholdersInContent()
* @param array $nonCacheableData $GLOBALS['TSFE']->config['INTincScript'] or part of it
* @see INTincScript()
*/
protected function processNonCacheableContentPartsAndSubstituteContentMarkers(array $nonCacheableData)
protected function processNonCacheableContentPartsAndSubstituteContentMarkers(array $nonCacheableData, ServerRequestInterface $request)
{
$timeTracker = $this->getTimeTracker();
$timeTracker->push('Split content');
Expand All @@ -2732,6 +2735,7 @@ protected function processNonCacheableContentPartsAndSubstituteContentMarkers(ar
$nonCacheableContent = '';
$contentObjectRendererForNonCacheable = unserialize($nonCacheableData[$nonCacheableKey]['cObj']);
/* @var ContentObjectRenderer $contentObjectRendererForNonCacheable */
$contentObjectRendererForNonCacheable->setRequest($request);
switch ($nonCacheableData[$nonCacheableKey]['type']) {
case 'COA':
$nonCacheableContent = $contentObjectRendererForNonCacheable->cObjGetSingle('COA', $nonCacheableData[$nonCacheableKey]['conf']);
Expand Down Expand Up @@ -2903,12 +2907,12 @@ public function isStaticCacheble()
* Creates an instance of ContentObjectRenderer in $this->cObj
* This instance is used to start the rendering of the TypoScript template structure
*
* @see RequestHandler
* @param ServerRequestInterface|null $request
*/
public function newCObj()
public function newCObj(ServerRequestInterface $request = null)
{
$this->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class, $this);
$this->cObj->start($this->page, 'pages');
$this->cObj->start($this->page, 'pages', $request);
}

/**
Expand Down
Expand Up @@ -92,11 +92,12 @@ public function process(ContentObjectRenderer $cObj, array $contentObjectConfigu

// Execute a SQL statement to fetch the records
$records = $cObj->getRecords($tableName, $processorConfiguration);
$request = $cObj->getRequest();
$processedRecordVariables = [];
foreach ($records as $key => $record) {
/** @var ContentObjectRenderer $recordContentObjectRenderer */
$recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$recordContentObjectRenderer->start($record, $tableName);
$recordContentObjectRenderer->start($record, $tableName, $request);
$processedRecordVariables[$key] = ['data' => $record];
$processedRecordVariables[$key] = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $processedRecordVariables[$key]);
}
Expand Down
Expand Up @@ -472,9 +472,10 @@ protected function processAdditionalDataProcessors($page, $processorConfiguratio
$page['children'][$key] = $this->processAdditionalDataProcessors($item, $processorConfiguration);
}
}
$request = $this->cObj->getRequest();
/** @var ContentObjectRenderer $recordContentObjectRenderer */
$recordContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$recordContentObjectRenderer->start($page['data'], 'pages');
$recordContentObjectRenderer->start($page['data'], 'pages', $request);
$processedPage = $this->contentDataProcessor->process($recordContentObjectRenderer, $processorConfiguration, $page);
return $processedPage;
}
Expand Down

0 comments on commit 8cec795

Please sign in to comment.