Commit 3a25c94b authored by Benni Mack's avatar Benni Mack
Browse files

[!!!][FEATURE] Introduce PSR-14 events in TSFE page resolving

This change introduces three new PSR-14 Events in favor
of the following hooks:

* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing']

replaced by "BeforePageIsResolvedEvent"

* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing']
* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess']

replaced by "AfterPageWithRootLineIsResolvedEvent"

* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc']
* $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess']

replaced by "AfterPageAndLanguageIsResolvedEvent"

Due to the restructuring of TSFE in the past versions, it became
obvious that several hooks are called right after each other,
making them superfluous. For this reason, they have been unified.

The new PSR-14 Events are more powerful as they also contain the
current request.

In addition TSFE->determineId() can now intercept the process
by properly returning a Response object.

Resolves: #97737
Releases: main
Change-Id: I9f82ba79c89826208a1131663a1b8b5e2e2781c9
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/74516


Tested-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Oliver Bartsch's avatarOliver Bartsch <bo@cedev.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 3d1b0a08
.. include:: /Includes.rst.txt
.. _breaking-97737-1654595331
=====================================================
Breaking: #97737 - Page-related hooks in TSFE removed
=====================================================
See :issue:`97737`
Description
===========
The following hooks, which were executed during the process of resolving page
details of a frontend request have been removed:
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess']`
They have been replaced by improved PSR-14 events.
Impact
======
Extensions that hook into these places are not executing the PHP-code anymore.
Affected installations
======================
TYPO3 installations with extensions using one of the hooks.
Check the "Configuration" module to see if your TYPO3 installations is using
one of the hooks by browsing :php:`$TYPO3_CONF_VARS[SC_OPTIONS]` or using the
Extension Scanner.
Migration
=========
The hooks are removed without deprecation in order to allow extensions
to work with TYPO3 v11 (using the hook) and v12+ (using the new Event)
when implementing the Event as well without any further deprecations.
Use the :doc:`PSR-14 Events <../12.0/Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved>`
* :php:`BeforePageIsResolvedEvent`
* :php:`AfterPageWithRootLineIsResolvedEvent`
* :php:`AfterPageAndLanguageIsResolvedEvent`
as an improved replacement.
.. index:: Frontend, FullyScanned, ext:frontend
.. include:: /Includes.rst.txt
.. _feature-97737-1654595148
================================================================================
Feature: #97737 - New PSR-14 Events when Page + Rootline in Frontend is resolved
================================================================================
See :issue:`97737`
Description
===========
Three new PSR-14 events have been added in the process when the main class
:php:`TypoScriptFrontendController` is resolving a page and its rootline,
based on the incoming request.
* :php:`BeforePageIsResolvedEvent`
* :php:`AfterPageWithRootLineIsResolvedEvent`
* :php:`AfterPageAndLanguageIsResolvedEvent`
All events receive the incoming PSR-7 Request object, and the
:php:`TypoScriptFrontendController` object.
In addition, the latter two events allow event listeners to define a custom
PSR-7 Response for custom permission layers, and interrupting further processing
of a page.
These events serve as a replacement for the previously available hooks:
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc']`
* :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess']`
Impact
======
Please note that TypoScript hasn't been resolved at the time of firing the
events, as this is done in the next step of the Frontend request.
.. index:: Frontend, ext:frontend
......@@ -67,6 +67,9 @@ use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
use TYPO3\CMS\Frontend\Cache\CacheLifetimeCalculator;
use TYPO3\CMS\Frontend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent;
use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent;
use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent;
use TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent;
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;
use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
......@@ -155,11 +158,9 @@ class TypoScriptFrontendController implements LoggerAwareInterface
public $sys_page = '';
/**
* Is set to 1 if a pageNotFound handler could have been called.
* @var int
* @internal
* Is set to > 0 if the page cound not be resolved. This will then result in early returns when resolving the page.
*/
public $pageNotFound = 0;
protected int $pageNotFound = 0;
/**
* Array containing a history of why a requested page was not accessible.
......@@ -580,33 +581,15 @@ class TypoScriptFrontendController implements LoggerAwareInterface
* - originalMountPointPage
* - pageAccessFailureHistory['direct_access']
* - pageNotFound
*
* @todo:
*
* On the first impression the method does too much.
* The reasons are manifold.
*
* 1.) The workflow of the resolution could be elaborated to be less
* tangled. Maybe the check of the page id to be below the domain via the
* root line doesn't need to be done each time, but for the final result
* only.
*
* 2.) The root line does not need to be directly addressed by this class.
* A root line is always related to one page. The rootline could be handled
* indirectly by page objects. Page objects still don't exist.
*
* @param ServerRequestInterface $request
*/
public function determineId(ServerRequestInterface $request): void
public function determineId(ServerRequestInterface $request): ?ResponseInterface
{
// Call pre processing function for id determination
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PreProcessing'] ?? [] as $functionReference) {
$parameters = ['parentObject' => $this];
GeneralUtility::callUserFunction($functionReference, $parameters, $this);
}
$timeTracker = $this->getTimeTracker();
$this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$eventDispatcher->dispatch(new BeforePageIsResolvedEvent($this, $request));
$timeTracker = $this->getTimeTracker();
$timeTracker->push('determineId rootLine/');
try {
// Sets ->page and ->rootline information based on ->id. ->id may change during this operation.
......@@ -637,57 +620,65 @@ class TypoScriptFrontendController implements LoggerAwareInterface
$this->pageNotFound = 1;
}
$timeTracker->pull();
if ($this->pageNotFound) {
switch ($this->pageNotFound) {
case 1:
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
$request,
'ID was not an accessible page',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
);
break;
case 2:
$response = GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
$request,
'Subsection was found and not accessible',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
);
break;
case 3:
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'ID was outside the domain',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
);
break;
default:
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'Unspecified error',
$this->getPageAccessFailureReasons()
);
}
throw new PropagateResponseException($response, 1533931329);
}
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['fetchPageId-PostProcessing'] ?? [] as $functionReference) {
$parameters = ['parentObject' => $this];
GeneralUtility::callUserFunction($functionReference, $parameters, $this);
$event = new AfterPageWithRootLineIsResolvedEvent($this, $request);
$event = $eventDispatcher->dispatch($event);
if ($event->getResponse()) {
return $event->getResponse();
}
// Setting language and fetch translated page
$this->settingLanguage($request);
// Check the "content_from_pid" field of the resolved page
$this->contentPid = $this->resolveContentPid($request);
$response = null;
try {
$this->evaluatePageNotFound($this->pageNotFound, $request);
// Update SYS_LASTCHANGED at the time, when $this->page might be changed by settingLanguage() and the $this->page was finally resolved
$this->setRegisterValueForSysLastChanged($this->page);
// Setting language and fetch translated page
$this->settingLanguage($request);
// Check the "content_from_pid" field of the resolved page
$this->contentPid = $this->resolveContentPid($request);
// Call post processing function for id determination:
$_params = ['pObj' => &$this];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'] ?? [] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $this);
// Update SYS_LASTCHANGED at the very last, when $this->page might be changed
// by settingLanguage() and the $this->page was finally resolved
$this->setRegisterValueForSysLastChanged($this->page);
} catch (PropagateResponseException $e) {
$response = $e->getResponse();
}
$event = new AfterPageAndLanguageIsResolvedEvent($this, $request, $response);
$eventDispatcher->dispatch($event);
return $event->getResponse();
}
/**
* If $this->pageNotFound is set, then throw an exception to stop further page generation process
*/
protected function evaluatePageNotFound(int $pageNotFoundNumber, ServerRequestInterface $request): void
{
if (!$pageNotFoundNumber) {
return;
}
$response = match ($pageNotFoundNumber) {
1 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
$request,
'ID was not an accessible page',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED)
),
2 => GeneralUtility::makeInstance(ErrorController::class)->accessDeniedAction(
$request,
'Subsection was found and not accessible',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED)
),
3 => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'ID was outside the domain',
$this->getPageAccessFailureReasons(PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH)
),
default => GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'Unspecified error',
$this->getPageAccessFailureReasons()
),
};
throw new PropagateResponseException($response, 1533931329);
}
/**
......@@ -1410,12 +1401,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
*/
protected function settingLanguage(ServerRequestInterface $request)
{
$_params = [];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_preProcess'] ?? [] as $_funcRef) {
$ref = $this; // introduced for phpstan to not lose type information when passing $this into callUserFunction
GeneralUtility::callUserFunction($_funcRef, $_params, $ref);
}
// Get values from site language
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($this->language);
......@@ -1491,7 +1476,7 @@ class TypoScriptFrontendController implements LoggerAwareInterface
// Setting the site language if an overlay record was found (which it is only if a language is used)
// We'll do this every time since the language aspect might have changed now
// Doing this ensures that page properties like the page title are returned in the correct language
// Doing this ensures that page properties like the page title are resolved in the correct language
$this->page = $this->sys_page->getPageOverlay($this->page, $languageAspect->getContentId());
}
......@@ -1500,14 +1485,13 @@ class TypoScriptFrontendController implements LoggerAwareInterface
// Setting sys_language_uid inside sys-page by creating a new page repository
$this->sys_page = GeneralUtility::makeInstance(PageRepository::class, $this->context);
// If default language is not available:
// If default language is not available
if ((!$languageAspect->getContentId() || !$languageAspect->getId())
&& $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
) {
$message = 'Page is not available in default language.';
$response = GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
$message,
'Page is not available in default language.',
['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
);
throw new PropagateResponseException($response, 1533931423);
......@@ -1516,11 +1500,6 @@ class TypoScriptFrontendController implements LoggerAwareInterface
if ($languageAspect->getId() > 0) {
$this->updateRootLinesWithTranslations();
}
$_params = [];
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['settingLanguage_postProcess'] ?? [] as $_funcRef) {
GeneralUtility::callUserFunction($_funcRef, $_params, $this);
}
}
/**
......
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* A PSR-14 event fired in the frontend process after a given page has been resolved including
* its language.
*
* This event is intended to e.g. modify TYPO3's language resolving logic by custom additions.
* This event also allows to send a custom Response via Event Listeners (e.g. a custom 403 response)
*/
final class AfterPageAndLanguageIsResolvedEvent
{
public function __construct(
private TypoScriptFrontendController $controller,
private ServerRequestInterface $request,
private ?ResponseInterface $response
) {
}
public function getController(): TypoScriptFrontendController
{
return $this->controller;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
public function setResponse(?ResponseInterface $response): void
{
$this->response = $response;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* A PSR-14 event fired in the frontend process after a given page has been resolved with permissions, rootline etc.
* This is useful to modify the page + rootline (but before the language is resolved)
* to direct or load content from a different page, or modify the page response if additional
* permissions should be checked.
*/
final class AfterPageWithRootLineIsResolvedEvent
{
private ?ResponseInterface $response = null;
public function __construct(
private TypoScriptFrontendController $controller,
private ServerRequestInterface $request
) {
}
public function getController(): TypoScriptFrontendController
{
return $this->controller;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function setResponse(ResponseInterface $response): void
{
$this->response = $response;
}
public function getResponse(): ?ResponseInterface
{
return $this->response;
}
}
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Event;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* A PSR-14 event fired before the frontend process is trying to fully resolve a given page
* by its page ID and the request.
*
* Event Listeners can modify incoming parameters (such as $controller->id) or modify the context
* for resolving a page.
*/
final class BeforePageIsResolvedEvent
{
public function __construct(
private TypoScriptFrontendController $controller,
private ServerRequestInterface $request
) {
}
public function getController(): TypoScriptFrontendController
{
return $this->controller;
}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
}
......@@ -98,7 +98,10 @@ class TypoScriptFrontendInitialization implements MiddlewareInterface
if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
$controller->set_no_cache('Preview active', true);
}
$controller->determineId($request);
$directResponse = $controller->determineId($request);
if ($directResponse) {
return $directResponse;
}
// Check if backend user has read access to this page.
if ($this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)
&& $this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)
......
......@@ -725,4 +725,34 @@ return [
'Feature-97544-PSR-14EventsForModifyingPreviewURIs.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'tslib/class.tslib_fe.php\'][\'determineId-PreProcessing\']' => [
'restFiles' => [
'Breaking-97737-Page-relatedHooksInTSFERemoved.rst',
'Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'tslib/class.tslib_fe.php\'][\'fetchPageId-PostProcessing\']' => [
'restFiles' => [
'Breaking-97737-Page-relatedHooksInTSFERemoved.rst',
'Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'tslib/class.tslib_fe.php\'][\'settingLanguage_preProcess\']' => [
'restFiles' => [
'Breaking-97737-Page-relatedHooksInTSFERemoved.rst',
'Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'tslib/class.tslib_fe.php\'][\'determineId-PostProc\']' => [
'restFiles' => [
'Breaking-97737-Page-relatedHooksInTSFERemoved.rst',
'Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'tslib/class.tslib_fe.php\'][\'settingLanguage_postProcess\']' => [
'restFiles' => [
'Breaking-97737-Page-relatedHooksInTSFERemoved.rst',
'Feature-97737-PSR-14EventsWhenPageRootlineInFrontendIsResolved.rst',
],
],
];
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment