TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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\Page;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use TYPO3\CMS\Core\Crypto\HashAlgo;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Logic for cHash calculation
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
class CacheHashCalculator
|
||||
{
|
||||
/**
|
||||
* Initialise class properties by using the relevant TYPO3 configuration
|
||||
*/
|
||||
public function __construct(
|
||||
protected CacheHashConfiguration $configuration,
|
||||
protected HashService $hashService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Calculates the cHash based on the provided parameters
|
||||
*
|
||||
* @param array $params Array of cHash key-value pairs
|
||||
* @return string Hash of all the values
|
||||
*/
|
||||
public function calculateCacheHash(array $params): string
|
||||
{
|
||||
if ($params === []) {
|
||||
return '';
|
||||
}
|
||||
unset($params['encryptionKey']);
|
||||
ksort($params);
|
||||
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||
return !empty($params) ? $hashService->hmac(serialize($params), self::class, HashAlgo::SHA3_256) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cHash based on provided query parameters and added values from internal call
|
||||
*
|
||||
* @param string $queryString Query-parameters: "&xxx=yyy&zzz=uuu
|
||||
* @return string Hash of all the values
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function generateForParameters($queryString)
|
||||
{
|
||||
$cacheHashParams = $this->getRelevantParameters($queryString);
|
||||
return $this->calculateCacheHash($cacheHashParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a parameter of the given $queryString requires cHash calculation
|
||||
*
|
||||
* @param string $queryString
|
||||
* @return bool
|
||||
*/
|
||||
public function doParametersRequireCacheHash($queryString)
|
||||
{
|
||||
if (!$this->configuration->hasData(CacheHashConfiguration::ASPECT_REQUIRED_CACHE_HASH_PRESENCE_PARAMETERS)) {
|
||||
return false;
|
||||
}
|
||||
$parameterNames = array_keys($this->splitQueryStringToArray($queryString));
|
||||
foreach ($parameterNames as $parameterName) {
|
||||
$hasRequiredParameter = $this->configuration->applies(
|
||||
CacheHashConfiguration::ASPECT_REQUIRED_CACHE_HASH_PRESENCE_PARAMETERS,
|
||||
$parameterName
|
||||
);
|
||||
if ($hasRequiredParameter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the input query-parameters into an array with certain parameters filtered out.
|
||||
* Used to create the cHash value
|
||||
*
|
||||
* @param string $queryString Query-parameters: "&xxx=yyy&zzz=uuu
|
||||
* @return array Array with key/value pairs of query-parameters WITHOUT a certain list of
|
||||
* @throws \RuntimeException
|
||||
* @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::typoLink()
|
||||
* @internal
|
||||
*/
|
||||
public function getRelevantParameters($queryString)
|
||||
{
|
||||
$parameters = $this->splitQueryStringToArray($queryString);
|
||||
$relevantParameters = [];
|
||||
foreach ($parameters as $parameterName => $parameterValue) {
|
||||
if ($this->isAdminPanelParameter($parameterName) || $this->isExcludedParameter($parameterName) || $this->isCoreParameter($parameterName)) {
|
||||
continue;
|
||||
}
|
||||
if ($this->hasCachedParametersWhiteList() && !$this->isInCachedParametersWhiteList($parameterName)) {
|
||||
continue;
|
||||
}
|
||||
if (($parameterValue === null || $parameterValue === '') && $this->isAllowedWithEmptyValue($parameterName)) {
|
||||
continue;
|
||||
}
|
||||
$relevantParameters[$parameterName] = $parameterValue;
|
||||
}
|
||||
if (!empty($relevantParameters)) {
|
||||
if (empty($parameters['id'])) {
|
||||
throw new \RuntimeException('ID parameter needs to be passed for the cHash calculation!', 1467983513);
|
||||
}
|
||||
$relevantParameters['id'] = $parameters['id'];
|
||||
// Finish and sort parameters array by keys:
|
||||
ksort($relevantParameters);
|
||||
}
|
||||
return $relevantParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the query string and converts it to an array.
|
||||
* Unlike parse_str it only creates an array with one level.
|
||||
*
|
||||
* e.g. foo[bar]=baz will be array('foo[bar]' => 'baz')
|
||||
*
|
||||
* @param string $queryString
|
||||
* @return array
|
||||
*/
|
||||
protected function splitQueryStringToArray($queryString)
|
||||
{
|
||||
$parameters = array_filter(explode('&', ltrim($queryString, '?')));
|
||||
$parameterArray = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
// should not remove empty values with trimExplode, otherwise cases like &=value, value is used as parameterName.
|
||||
$parts = GeneralUtility::trimExplode('=', $parameter, false);
|
||||
$parameterName = $parts[0];
|
||||
$parameterValue = $parts[1] ?? '';
|
||||
if (trim($parameterName) === '') {
|
||||
// This parameter cannot appear in $_GET in PHP even if its value is not empty, so it should be ignored!
|
||||
continue;
|
||||
}
|
||||
$parameterArray[rawurldecode($parameterName)] = rawurldecode($parameterValue);
|
||||
}
|
||||
return $parameterArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter is out of a known data-set starting
|
||||
* with ADMCMD.
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAdminPanelParameter($key)
|
||||
{
|
||||
return $key === 'ADMCMD_simUser' || $key === 'ADMCMD_simTime' || $key === 'ADMCMD_prev';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter is a core parameter
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCoreParameter($key)
|
||||
{
|
||||
return $key === 'id' || $key === 'type' || $key === 'no_cache' || $key === 'cHash' || $key === 'MP' || $key === 'logintype';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter should be excluded from cHash calculation
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function isExcludedParameter($key)
|
||||
{
|
||||
return $this->configuration->applies(
|
||||
CacheHashConfiguration::ASPECT_EXCLUDED_PARAMETERS,
|
||||
$key
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given parameter is an exclusive parameter for cHash calculation
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function isInCachedParametersWhiteList($key)
|
||||
{
|
||||
return $this->configuration->applies(
|
||||
CacheHashConfiguration::ASPECT_CACHED_PARAMETERS_WHITELIST,
|
||||
$key
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether cachedParametersWhiteList parameters are configured
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasCachedParametersWhiteList()
|
||||
{
|
||||
return $this->configuration->hasData(
|
||||
CacheHashConfiguration::ASPECT_CACHED_PARAMETERS_WHITELIST
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given parameter may be used even with an empty value
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAllowedWithEmptyValue($key)
|
||||
{
|
||||
return $this->configuration->shallExcludeAllEmptyParameters()
|
||||
|| $this->configuration->applies(
|
||||
CacheHashConfiguration::ASPECT_EXCLUDED_PARAMETERS_IF_EMPTY,
|
||||
$key
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
<?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\Page;
|
||||
|
||||
/**
|
||||
* Model for configuration properties in $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash'].
|
||||
*
|
||||
* URL parameter names are prefixed with the following indicators:
|
||||
* + = (equals): exact match, default behavior if not given
|
||||
* + + ^ (startsWith): matching the beginning of a parameter name
|
||||
* + ~ (contains): matching any inline occurrence in a parameter name
|
||||
*
|
||||
* Example:
|
||||
* $configuration = new CacheHashConfiguration([
|
||||
* 'excludedParameters' => ['utm_source', '^tx_my_plugin[aspects]', '~[temporary]'],
|
||||
* ...
|
||||
* ]);
|
||||
*/
|
||||
class CacheHashConfiguration
|
||||
{
|
||||
public const ASPECT_CACHED_PARAMETERS_WHITELIST = 'cachedParametersWhiteList';
|
||||
public const ASPECT_EXCLUDED_PARAMETERS = 'excludedParameters';
|
||||
public const ASPECT_EXCLUDED_PARAMETERS_IF_EMPTY = 'excludedParametersIfEmpty';
|
||||
public const ASPECT_REQUIRED_CACHE_HASH_PRESENCE_PARAMETERS = 'requireCacheHashPresenceParameters';
|
||||
|
||||
protected const PROPERTY_EXCLUDE_ALL_EMPTY_PARAMETERS = 'excludeAllEmptyParameters';
|
||||
protected const INDICATOR_STARTS_WITH = '^';
|
||||
protected const INDICATOR_CONTAINS = '~';
|
||||
protected const INDICATOR_EQUALS = '=';
|
||||
|
||||
protected const ALLOWED_INDICATORS = [
|
||||
self::INDICATOR_STARTS_WITH,
|
||||
self::INDICATOR_CONTAINS,
|
||||
self::INDICATOR_EQUALS,
|
||||
];
|
||||
|
||||
protected const ALLOWED_PROPERTY_NAMES = [
|
||||
self::PROPERTY_EXCLUDE_ALL_EMPTY_PARAMETERS,
|
||||
self::ASPECT_CACHED_PARAMETERS_WHITELIST,
|
||||
self::ASPECT_EXCLUDED_PARAMETERS,
|
||||
self::ASPECT_EXCLUDED_PARAMETERS_IF_EMPTY,
|
||||
self::ASPECT_REQUIRED_CACHE_HASH_PRESENCE_PARAMETERS,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array $configuration The raw configuration of `$GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash']`
|
||||
*/
|
||||
protected array $configuration;
|
||||
|
||||
/**
|
||||
* @var array $data The resolved aspects to be applied, based on the configuration
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(?array $configuration = null)
|
||||
{
|
||||
$configuration = $configuration ?? $GLOBALS['TYPO3_CONF_VARS']['FE']['cacheHash'] ?? [];
|
||||
$this->configuration = array_filter($configuration, [$this, 'isAllowedProperty'], ARRAY_FILTER_USE_KEY);
|
||||
$this->processConfiguration();
|
||||
}
|
||||
|
||||
public function shallExcludeAllEmptyParameters(): bool
|
||||
{
|
||||
return !empty($this->configuration[self::PROPERTY_EXCLUDE_ALL_EMPTY_PARAMETERS]);
|
||||
}
|
||||
|
||||
public function applies(string $aspect, string $value): bool
|
||||
{
|
||||
return $this->equals($aspect, $value)
|
||||
|| $this->contains($aspect, $value)
|
||||
|| $this->startsWith($aspect, $value);
|
||||
}
|
||||
|
||||
public function equals(string $aspect, string $value): bool
|
||||
{
|
||||
$data = $this->getData($aspect, self::INDICATOR_EQUALS);
|
||||
return !empty($data) && in_array($value, $data, true);
|
||||
}
|
||||
|
||||
public function startsWith(string $aspect, string $value): bool
|
||||
{
|
||||
$data = $this->getData($aspect, self::INDICATOR_STARTS_WITH);
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($data as $item) {
|
||||
if (str_starts_with($value, $item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function contains(string $aspect, string $value): bool
|
||||
{
|
||||
$data = $this->getData($aspect, self::INDICATOR_CONTAINS);
|
||||
if (empty($data)) {
|
||||
return false;
|
||||
}
|
||||
foreach ($data as $item) {
|
||||
if (str_contains($value, $item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function hasData(string $aspect): bool
|
||||
{
|
||||
return !empty($this->data[$aspect]);
|
||||
}
|
||||
|
||||
protected function getData(string $aspect, string $indicator): ?array
|
||||
{
|
||||
return $this->data[$aspect][$indicator] ?? null;
|
||||
}
|
||||
|
||||
protected function defineData(string $aspect): void
|
||||
{
|
||||
if (empty($this->configuration[$aspect])) {
|
||||
return;
|
||||
}
|
||||
if (!is_array($this->configuration[$aspect])) {
|
||||
throw new \LogicException(
|
||||
sprintf('Expected array value, got %s', gettype($this->configuration[$aspect])),
|
||||
1580225311
|
||||
);
|
||||
}
|
||||
$data = [];
|
||||
foreach ($this->configuration[$aspect] as $value) {
|
||||
if (!is_scalar($value)) {
|
||||
throw new \LogicException(
|
||||
sprintf('Expected scalar value, got %s', gettype($value)),
|
||||
1580225312
|
||||
);
|
||||
}
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$indicator = $value[0] ?? null;
|
||||
// normalize value to be indicated
|
||||
if (!in_array($indicator, self::ALLOWED_INDICATORS, true)) {
|
||||
$indicator = self::INDICATOR_EQUALS;
|
||||
$value = self::INDICATOR_EQUALS . $value;
|
||||
}
|
||||
if (strlen((string)$value) === 1) {
|
||||
throw new \LogicException(
|
||||
sprintf('Empty value after %s indicator', $indicator),
|
||||
1580225313
|
||||
);
|
||||
}
|
||||
$data[$indicator][] = substr((string)$value, 1);
|
||||
}
|
||||
if (!empty($data)) {
|
||||
$this->data[$aspect] = $data;
|
||||
}
|
||||
}
|
||||
|
||||
protected function processConfiguration(): void
|
||||
{
|
||||
$this->data = [];
|
||||
$this->defineData(self::ASPECT_CACHED_PARAMETERS_WHITELIST);
|
||||
$this->defineData(self::ASPECT_EXCLUDED_PARAMETERS);
|
||||
$this->defineData(self::ASPECT_EXCLUDED_PARAMETERS_IF_EMPTY);
|
||||
$this->defineData(self::ASPECT_REQUIRED_CACHE_HASH_PRESENCE_PARAMETERS);
|
||||
}
|
||||
|
||||
protected function isAllowedProperty(string $propertyName): bool
|
||||
{
|
||||
return in_array($propertyName, self::ALLOWED_PROPERTY_NAMES, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Page;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* A small utility layer to determine "absRefPrefix" from TypoScript,
|
||||
* the string that is prepended to all relative URLs in the frontend,
|
||||
* such as links and images.
|
||||
*
|
||||
* @internal It is highly possible that this class will not be needed anymore, so use it with care, if really needed
|
||||
*/
|
||||
final readonly class FrontendUrlPrefix
|
||||
{
|
||||
public function getUrlPrefix(ServerRequestInterface $request): string
|
||||
{
|
||||
$typoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
// TypoScript config.forceAbsoluteUrls overrides config.absRefPrefix
|
||||
if ($typoScriptConfigArray['forceAbsoluteUrls'] ?? false) {
|
||||
return $normalizedParams->getSiteUrl();
|
||||
}
|
||||
$absRefPrefix = trim($typoScriptConfigArray['absRefPrefix'] ?? '');
|
||||
return $absRefPrefix === 'auto' ? $normalizedParams->getSitePath() : $absRefPrefix;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?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\Page;
|
||||
|
||||
/**
|
||||
* Contains a list of all reasons that TYPO3 internally uses when a page cannot be found/rendered etc.
|
||||
*/
|
||||
final class PageAccessFailureReasons
|
||||
{
|
||||
// Page resolving issues
|
||||
public const string NO_PAGES_FOUND = 'page.database.empty';
|
||||
public const string PAGE_NOT_FOUND = 'page';
|
||||
public const string ROOTLINE_BROKEN = 'page.rootline';
|
||||
public const string INVALID_LINK_PAGE = 'page.invalid_external_url';
|
||||
|
||||
// Site configuration issues
|
||||
public const string INVALID_SITE_SETS = 'site.sets.invalid';
|
||||
|
||||
// Page configuration issues
|
||||
public const string RENDERING_INSTRUCTIONS_NOT_FOUND = 'rendering_instructions';
|
||||
public const string RENDERING_INSTRUCTIONS_NOT_CONFIGURED = 'rendering_instructions.type';
|
||||
|
||||
// Validation errors
|
||||
public const string INVALID_PAGE_ARGUMENTS = 'page.invalid_arguments';
|
||||
public const string CACHEHASH_COMPARISON_FAILED = 'cache_hash.comparison';
|
||||
public const string CACHEHASH_EMPTY = 'cache_hash.empty';
|
||||
|
||||
// Language-related issues
|
||||
public const string LANGUAGE_NOT_AVAILABLE = 'language';
|
||||
public const string LANGUAGE_NOT_AVAILABLE_STRICT_MODE = 'language.strict';
|
||||
public const string LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE = 'language.fallbacks';
|
||||
public const string LANGUAGE_DEFAULT_NOT_AVAILABLE = 'language.default';
|
||||
|
||||
// Access restrictions
|
||||
public const string ACCESS_DENIED_GENERAL = 'access';
|
||||
public const string ACCESS_DENIED_PAGE_NOT_RESOLVED = 'access.page';
|
||||
public const string ACCESS_DENIED_SUBSECTION_NOT_RESOLVED = 'access.subsection';
|
||||
public const string ACCESS_DENIED_HOST_PAGE_MISMATCH = 'access.host_mismatch';
|
||||
public const string ACCESS_DENIED_INVALID_PAGETYPE = 'access.pagetype';
|
||||
|
||||
// System errors
|
||||
public const string DATABASE_CONNECTION_FAILED = 'system.database';
|
||||
|
||||
/**
|
||||
* Labels for the status codes
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $messages = [
|
||||
self::NO_PAGES_FOUND => 'No page on rootlevel found',
|
||||
self::PAGE_NOT_FOUND => 'The requested page does not exist',
|
||||
self::INVALID_LINK_PAGE => 'Page of type "Link" could not be resolved properly',
|
||||
|
||||
self::RENDERING_INSTRUCTIONS_NOT_FOUND => 'No TypoScript record found',
|
||||
self::RENDERING_INSTRUCTIONS_NOT_CONFIGURED => 'The page is not configured',
|
||||
|
||||
self::INVALID_PAGE_ARGUMENTS => 'Page Arguments could not be resolved',
|
||||
self::CACHEHASH_COMPARISON_FAILED => 'Request parameters could not be validated (&cHash comparison failed)',
|
||||
self::CACHEHASH_EMPTY => 'Request parameters could not be validated (&cHash empty)',
|
||||
|
||||
self::LANGUAGE_NOT_AVAILABLE => 'Page is not available in the requested language',
|
||||
self::LANGUAGE_NOT_AVAILABLE_STRICT_MODE => 'Page is not available in the requested language (strict)',
|
||||
self::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE => 'Page is not available in the requested language (fallbacks did not apply)',
|
||||
self::LANGUAGE_DEFAULT_NOT_AVAILABLE => 'Page is not available in default language',
|
||||
|
||||
self::ACCESS_DENIED_GENERAL => 'The requested page was not accessible',
|
||||
self::ACCESS_DENIED_PAGE_NOT_RESOLVED => 'ID was not an accessible page',
|
||||
self::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED => 'Subsection was found and not accessible',
|
||||
self::ACCESS_DENIED_HOST_PAGE_MISMATCH => 'ID was outside the domain',
|
||||
self::ACCESS_DENIED_INVALID_PAGETYPE => 'The requested page type cannot be rendered',
|
||||
|
||||
self::DATABASE_CONNECTION_FAILED => 'Database Connection failed',
|
||||
self::ROOTLINE_BROKEN => 'The requested page did not have a proper connection to the tree-root',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $reasonCode a valid reason code (see above)
|
||||
*/
|
||||
public function getMessageForReason(string $reasonCode): string
|
||||
{
|
||||
if (!isset($this->messages[$reasonCode])) {
|
||||
throw new \InvalidArgumentException('No message for page access reason code "' . $reasonCode . '" found.', 1529299833);
|
||||
}
|
||||
return $this->messages[$reasonCode];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
<?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\Page;
|
||||
|
||||
use TYPO3\CMS\Core\Page\PageLayout;
|
||||
|
||||
/**
|
||||
* This DTO carries various Frontend rendering related page information. It is
|
||||
* set up by a Frontend middleware and attached to as 'frontend.page.information'
|
||||
* Request attribute.
|
||||
*/
|
||||
final class PageInformation
|
||||
{
|
||||
private int $id;
|
||||
private array $pageRecord;
|
||||
private string $mountPoint = '';
|
||||
private int $contentFromPid;
|
||||
|
||||
/**
|
||||
* Gets set when we are processing a page of type shortcut in the early stages
|
||||
* of the request, used later in a middleware to resolve the shortcut and redirect again.
|
||||
*/
|
||||
private ?array $originalShortcutPageRecord = null;
|
||||
|
||||
/**
|
||||
* Gets set when we are processing a page of type mountpoint with enabled overlay in getPageAndRootline()
|
||||
* Used later in a middleware to determine the final target URL where the user should be redirected to.
|
||||
*/
|
||||
private ?array $originalMountPointPageRecord = null;
|
||||
|
||||
/**
|
||||
* Rootline of page records all the way to the root.
|
||||
*
|
||||
* Both language and version overlays are applied to these page records:
|
||||
* All "data" fields are set to language / version overlay values, *except* uid and
|
||||
* pid, which are the default-language and live-version ids.
|
||||
*
|
||||
* First array row with the highest key is the deepest page (the requested page),
|
||||
* then parent pages with descending keys until (but not including) the
|
||||
* project root pseudo page 0.
|
||||
*
|
||||
* When page uid 5 is called in this example:
|
||||
* [0] Project name
|
||||
* |- [2] An organizational page, probably with is_siteroot=1 and a site config
|
||||
* |- [3] Site root with a sys_template having "root" flag set
|
||||
* |- [5] Here you are
|
||||
*
|
||||
* This $absoluteRootLine is:
|
||||
* [3] => [uid = 5, pid = 3, title = Here you are, ...]
|
||||
* [2] => [uid = 3, pid = 2, title = Site root with a sys_template having "root" flag set, ...]
|
||||
* [1] => [uid = 2, pid = 0, title = An organizational page, probably with is_siteroot=1 and a site config, ...]
|
||||
*
|
||||
* Read-only! Extensions may read but never write this property!
|
||||
*
|
||||
* @var array<int, array<string, mixed>>
|
||||
*/
|
||||
private array $rootLine;
|
||||
|
||||
/**
|
||||
* This is the "local" rootline of a deep page that stops at the first parent
|
||||
* sys_template record that has "root" flag set, in natural parent-child order.
|
||||
*
|
||||
* Both language and version overlays are applied to these page records:
|
||||
* All "data" fields are set to language / version overlay values, *except* uid and
|
||||
* pid, which are the default-language and live-version ids.
|
||||
*
|
||||
* When page uid 5 is called in this example:
|
||||
* [0] Project name
|
||||
* |- [2] An organizational page, probably with is_siteroot=1 and a site config
|
||||
* |- [3] Site root with a sys_template having "root" flag set
|
||||
* |- [5] Here you are
|
||||
*
|
||||
* This rootLine is:
|
||||
* [0] => [uid = 3, pid = 2, title = Site root with a sys_template having "root" flag set, ...]
|
||||
* [1] => [uid = 5, pid = 3, title = Here you are, ...]
|
||||
*
|
||||
* @var array<int, array<string, mixed>>
|
||||
*/
|
||||
private array $localRootLine;
|
||||
|
||||
/**
|
||||
* List of all sys_template rows attached to rootLine pages.
|
||||
*
|
||||
* @var array<int, array<string, mixed>>
|
||||
*/
|
||||
private array $sysTemplateRows;
|
||||
|
||||
/**
|
||||
* The resolved PageLayout of the page (selected backend layout)
|
||||
*
|
||||
* @var PageLayout|null
|
||||
*/
|
||||
private ?PageLayout $pageLayout = null;
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setId(int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setPageRecord(array $pageRecord): void
|
||||
{
|
||||
$this->pageRecord = $pageRecord;
|
||||
}
|
||||
|
||||
public function getPageRecord(): array
|
||||
{
|
||||
return $this->pageRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setMountPoint(string $mountPoint): void
|
||||
{
|
||||
$this->mountPoint = $mountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be read by core
|
||||
*/
|
||||
public function getMountPoint(): string
|
||||
{
|
||||
return $this->mountPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setRootLine(array $rootLine): void
|
||||
{
|
||||
$this->rootLine = $rootLine;
|
||||
}
|
||||
|
||||
public function getRootLine(): array
|
||||
{
|
||||
return $this->rootLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setLocalRootLine(array $localRootLine): void
|
||||
{
|
||||
$this->localRootLine = $localRootLine;
|
||||
}
|
||||
|
||||
public function getLocalRootLine(): array
|
||||
{
|
||||
return $this->localRootLine;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setSysTemplateRows(array $sysTemplateRows): void
|
||||
{
|
||||
$this->sysTemplateRows = $sysTemplateRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be read by core
|
||||
*/
|
||||
public function getSysTemplateRows(): array
|
||||
{
|
||||
return $this->sysTemplateRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setOriginalShortcutPageRecord(array $originalShortcutPageRecord): void
|
||||
{
|
||||
$this->originalShortcutPageRecord = $originalShortcutPageRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be read by core
|
||||
*/
|
||||
public function getOriginalShortcutPageRecord(): ?array
|
||||
{
|
||||
return $this->originalShortcutPageRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setOriginalMountPointPageRecord(array $originalMountPointPageRecord): void
|
||||
{
|
||||
$this->originalMountPointPageRecord = $originalMountPointPageRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be read by core
|
||||
*/
|
||||
public function getOriginalMountPointPageRecord(): ?array
|
||||
{
|
||||
return $this->originalMountPointPageRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setContentFromPid(int $contentFromPid): void
|
||||
{
|
||||
$this->contentFromPid = $contentFromPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be read by core
|
||||
*/
|
||||
public function getContentFromPid(): int
|
||||
{
|
||||
return $this->contentFromPid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal Only to be set by core
|
||||
*/
|
||||
public function setPageLayout(PageLayout $pageLayout): void
|
||||
{
|
||||
$this->pageLayout = $pageLayout;
|
||||
}
|
||||
|
||||
public function getPageLayout(): ?PageLayout
|
||||
{
|
||||
return $this->pageLayout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Page;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Exception created in PageInformationFactory that contains an "early" Response
|
||||
* when further calculation is stopped and the final PageInformation object can
|
||||
* not be created. It is typically caught and handled in the Frontend
|
||||
* middleware that triggers PageInformation creation.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class PageInformationCreationFailedException extends \Exception
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ResponseInterface $response,
|
||||
int $code
|
||||
) {
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
<?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\Page;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspect;
|
||||
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
|
||||
use TYPO3\CMS\Core\DataHandling\PageDoktypeRegistry;
|
||||
use TYPO3\CMS\Core\Domain\Access\RecordAccessVoter;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Error\Http\LinkedPageNotResolvableException;
|
||||
use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException;
|
||||
use TYPO3\CMS\Core\Error\Http\StatusException;
|
||||
use TYPO3\CMS\Core\Exception\Page\CircularPageReferenceChainException;
|
||||
use TYPO3\CMS\Core\Exception\Page\PageReferenceResolvingReachedIterationLimitException;
|
||||
use TYPO3\CMS\Core\Exception\Page\RootLineException;
|
||||
use TYPO3\CMS\Core\LinkHandling\PageTypeLinkResolver;
|
||||
use TYPO3\CMS\Core\Page\PageLayoutResolver;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Site\Entity\NullSite;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\TypoScript\IncludeTree\SysTemplateRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
||||
use TYPO3\CMS\Frontend\Authentication\FrontendBackendUserAuthentication;
|
||||
use TYPO3\CMS\Frontend\Controller\ErrorController;
|
||||
use TYPO3\CMS\Frontend\Event\AfterPageAndLanguageIsResolvedEvent;
|
||||
use TYPO3\CMS\Frontend\Event\AfterPageWithRootLineIsResolvedEvent;
|
||||
use TYPO3\CMS\Frontend\Event\BeforePageIsResolvedEvent;
|
||||
|
||||
/**
|
||||
* Create the PageInformation object. This is typically fired by a
|
||||
* middleware. It does all the heavy lifting, page access checks,
|
||||
* resolves shortcuts, workspaces, languages and similar.
|
||||
*
|
||||
* Possible results:
|
||||
* - The fully set up PageInformation object is returned.
|
||||
* - A PageInformationCreationFailedException is thrown that contains
|
||||
* an early Response from the ErrorController
|
||||
* - A StatusException is thrown when ErrorController itself failed
|
||||
*
|
||||
* @todo: This class also sets / resets the Context language aspect
|
||||
* as not directly obvious side effect. This can't be refactored
|
||||
* currently due to the dependency of stateful PageRepository to
|
||||
* the stateful singleton Context.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final readonly class PageInformationFactory
|
||||
{
|
||||
public function __construct(
|
||||
private Context $context,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private LoggerInterface $logger,
|
||||
private RecordAccessVoter $accessVoter,
|
||||
private ErrorController $errorController,
|
||||
private SysTemplateRepository $sysTemplateRepository,
|
||||
private PageLayoutResolver $pageLayoutResolver,
|
||||
private TcaSchemaFactory $tcaSchemaFactory,
|
||||
private PageTypeLinkResolver $pageTypeLinkResolver,
|
||||
private PageDoktypeRegistry $pageDoktypeRegistry,
|
||||
private PageRepository $pageRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Set up proper PageInformation object later available as
|
||||
* 'frontend.page.information' Request attribute.
|
||||
*
|
||||
* At this point, the Context object already contains relevant preview
|
||||
* settings, for instance if a backend user is logged in.
|
||||
*
|
||||
* As a not obvious side effect, this class also *sets* the
|
||||
*
|
||||
* @internal Extensions should not call themselves, use events.
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
public function create(ServerRequestInterface $request): PageInformation
|
||||
{
|
||||
// Set Initial, not yet validated values from routing.
|
||||
$pageInformation = new PageInformation();
|
||||
$pageInformation->setId($request->getAttribute('routing')->getPageId());
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids'] ?? false) {
|
||||
$mountPoint = (string)($request->getAttribute('routing')->getArguments()['MP'] ?? '');
|
||||
// Ensure no additional arguments are given via the &MP=123-345,908-172 (e.g. "/")
|
||||
$pageInformation->setMountPoint(preg_replace('/[^0-9,-]/', '', $mountPoint));
|
||||
}
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new BeforePageIsResolvedEvent($request, $pageInformation));
|
||||
$pageInformation = $event->getPageInformation();
|
||||
|
||||
$pageInformation = $this->setPageAndRootline($request, $pageInformation);
|
||||
$this->checkCrossDomainWithDirectId($request, $pageInformation);
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new AfterPageWithRootLineIsResolvedEvent($request, $pageInformation));
|
||||
if ($event->getResponse()) {
|
||||
throw new PageInformationCreationFailedException($event->getResponse(), 1705419743);
|
||||
}
|
||||
$pageInformation = $event->getPageInformation();
|
||||
|
||||
$pageInformation = $this->settingLanguage($request, $pageInformation);
|
||||
$pageInformation = $this->setContentFromPid($request, $pageInformation);
|
||||
$pageInformation = $this->setPageLayout($pageInformation);
|
||||
$this->checkBackendUserAccess($request, $pageInformation);
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(new AfterPageAndLanguageIsResolvedEvent($request, $pageInformation));
|
||||
if ($event->getResponse()) {
|
||||
throw new PageInformationCreationFailedException($event->getResponse(), 1705420010);
|
||||
}
|
||||
$pageInformation = $event->getPageInformation();
|
||||
|
||||
$pageInformation = $this->setSysTemplateRows($request, $pageInformation);
|
||||
$pageInformation = $this->setLocalRootLine($request, $pageInformation);
|
||||
|
||||
$this->verifySiteOrSysTemplateRowExists($request, $pageInformation);
|
||||
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main lifting. Final page and the matching root line are determined and loaded.
|
||||
*
|
||||
* Note this methods may be called a second time in case of 'content_from_pid'.
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function setPageAndRootline(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$id = $pageInformation->getId();
|
||||
$mountPoint = $pageInformation->getMountPoint();
|
||||
$pageRecord = $this->pageRepository->getPage($id);
|
||||
|
||||
if (empty($pageRecord)) {
|
||||
// @todo: This logic could be streamlined is general. The idea of PageRepository->getPage() is
|
||||
// to have *one* query that does all the access checks already, to save queries. Only
|
||||
// if this goes wrong more queries are fired to find out details.
|
||||
// This is ugly. It would be better to query the page without checks and return it, probably
|
||||
// only doing the deleted=1 check. If that does not resolve: 404. Then apply the hidden
|
||||
// and groups checks in PHP on the page record without further queries, to error out
|
||||
// whenever a single step goes wrong. Or have a single method that follows this strategy
|
||||
// and throws dedicated exceptions like a PageHiddenException or similar to catch it here
|
||||
// and act accordingly.
|
||||
// This strategy would turn the guesswork logic below around and would be easier to
|
||||
// maintain and follow in general. It would also clean up the various methods with their
|
||||
// subtle differences: getPage($id), getPage($id, true) and getPage_noCheck($id).
|
||||
// PageRepository->getPage() did not return a page. This can have
|
||||
// different reasons. We want to error out with different status codes.
|
||||
$schema = $this->tcaSchemaFactory->get('pages');
|
||||
$includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages') || $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
|
||||
if ($schema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) && !$includeHiddenPages) {
|
||||
// Page is hidden, user has no access. 404. This is deliberately done in default language
|
||||
// since language overlays should not be rendered when default language is hidden.
|
||||
$rawPageRecord = $this->pageRepository->getPage_noCheck($id);
|
||||
$hiddenField = $schema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName();
|
||||
if ($rawPageRecord === [] || $rawPageRecord[$hiddenField]) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist!',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1674144383);
|
||||
}
|
||||
}
|
||||
$requestedPageRowWithoutGroupCheck = $this->pageRepository->getPage($id, true);
|
||||
if (!empty($requestedPageRowWithoutGroupCheck)) {
|
||||
// We know now the page could not be received, but the reason is *not* that the
|
||||
// page is hidden and the user has no hidden access. So group access failed? 403.
|
||||
$response = $this->errorController->accessDeniedAction(
|
||||
$request,
|
||||
'ID was not an accessible page',
|
||||
[
|
||||
'code' => PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED,
|
||||
'direct_access' => [
|
||||
0 => $requestedPageRowWithoutGroupCheck,
|
||||
],
|
||||
],
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705325336);
|
||||
}
|
||||
// Else 404 for 'record not exists' or similar.
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist!',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931330);
|
||||
}
|
||||
|
||||
$pageInformation->setPageRecord($pageRecord);
|
||||
$pageDoktype = (int)($pageRecord['doktype']);
|
||||
|
||||
// Spacer and sysfolders are not accessible in frontend
|
||||
if (!$this->pageDoktypeRegistry->isPageTypeViewable($pageDoktype)) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist!',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_INVALID_PAGETYPE]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931343);
|
||||
}
|
||||
|
||||
if ($pageDoktype === PageRepository::DOKTYPE_LINK || $pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
|
||||
// Resolve shortcut page target.
|
||||
// Clear mount point if page is a shortcut: If the shortcut goes to
|
||||
// another page, we leave the rootline which the MP expects.
|
||||
$mountPoint = '';
|
||||
$pageInformation->setMountPoint($mountPoint);
|
||||
// Saving the page so that we can check later - when we know about languages - whether we took the correct shortcut
|
||||
// or if a translation of the page overwrites the shortcut target, and we need to follow the new target.
|
||||
$pageInformation = $this->settingLanguage($request, $pageInformation);
|
||||
// Reset vars to new state that may have been created by settingLanguage()
|
||||
$pageRecord = $pageInformation->getPageRecord();
|
||||
if ($pageDoktype === PageRepository::DOKTYPE_LINK) {
|
||||
$pageInformation->setOriginalShortcutPageRecord($pageRecord);
|
||||
$typolinkInformation = $this->pageTypeLinkResolver->resolveTypolinkParts($pageRecord);
|
||||
// The link destination was a page, we have to prevent infinitive loops
|
||||
if ($typolinkInformation['type'] === 'page') {
|
||||
try {
|
||||
$pageRecord = $this->pageRepository->resolveLinkPage($pageRecord);
|
||||
} catch (ShortcutTargetPageNotFoundException|LinkedPageNotResolvableException|CircularPageReferenceChainException|PageReferenceResolvingReachedIterationLimitException) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'ID was not an accessible page',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705335066);
|
||||
}
|
||||
$pageInformation->setPageRecord($pageRecord);
|
||||
$id = (int)$pageRecord['uid'];
|
||||
$pageInformation->setId($id);
|
||||
$pageDoktype = (int)($pageRecord['doktype'] ?? 0);
|
||||
}
|
||||
}
|
||||
if ($pageDoktype === PageRepository::DOKTYPE_SHORTCUT) {
|
||||
$pageInformation->setOriginalShortcutPageRecord($pageRecord);
|
||||
try {
|
||||
$pageRecord = $this->pageRepository->resolveShortcutPage($pageRecord);
|
||||
} catch (ShortcutTargetPageNotFoundException|LinkedPageNotResolvableException|CircularPageReferenceChainException|PageReferenceResolvingReachedIterationLimitException) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'ID was not an accessible page',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705335065);
|
||||
}
|
||||
$pageInformation->setPageRecord($pageRecord);
|
||||
$id = (int)$pageRecord['uid'];
|
||||
$pageInformation->setId($id);
|
||||
$pageDoktype = (int)($pageRecord['doktype'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if ($pageDoktype === PageRepository::DOKTYPE_MOUNTPOINT && $pageRecord['mount_pid_ol']) {
|
||||
// If the page is a mount point which should be overlaid with the contents of the mounted page,
|
||||
// it must never be accessible directly, but only in the mount point context.
|
||||
// We thus change the current page id.
|
||||
$originalMountPointPageRecord = $pageRecord;
|
||||
$pageInformation->setOriginalMountPointPageRecord($pageRecord);
|
||||
$pageRecord = $this->pageRepository->getPage((int)$originalMountPointPageRecord['mount_pid']);
|
||||
if (empty($pageRecord)) {
|
||||
// Target mount point page not accessible for some reason.
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'The requested page does not exist!',
|
||||
['code' => PageAccessFailureReasons::PAGE_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705425523);
|
||||
}
|
||||
$pageInformation->setPageRecord($pageRecord);
|
||||
if ($mountPoint === '' || !empty($pageInformation->getOriginalShortcutPageRecord())) {
|
||||
// If the current page is a shortcut, the MP parameter will be replaced
|
||||
$mountPoint = $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
|
||||
} else {
|
||||
$mountPoint = $mountPoint . ',' . $pageRecord['uid'] . '-' . $originalMountPointPageRecord['uid'];
|
||||
}
|
||||
$pageInformation->setMountPoint($mountPoint);
|
||||
$id = (int)$pageRecord['uid'];
|
||||
$pageInformation->setId($id);
|
||||
}
|
||||
|
||||
// Get rootLine and error out if it can not be retrieved.
|
||||
$pageInformation->setRootLine($this->getRootlineOrThrow($request, $id, $mountPoint));
|
||||
|
||||
// Check 'extendToSubpages' in rootLine and backend user section.
|
||||
$this->checkRootlineForIncludeSection($request, $pageInformation);
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the final Context language aspect, page record based on
|
||||
* language settings, existing page overlay and its rootLine.
|
||||
*
|
||||
* May reset:
|
||||
* $pageInformation->pageRecord
|
||||
* $pageInformation->pageRepository
|
||||
* $pageInformation->rootLine
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function settingLanguage(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
$language = $request->getAttribute('language', $site->getDefaultLanguage());
|
||||
$languageAspect = LanguageAspectFactory::createFromSiteLanguage($language);
|
||||
$languageId = $languageAspect->getId();
|
||||
$languageContentId = $languageAspect->getContentId();
|
||||
|
||||
$pageRecord = $pageInformation->getPageRecord();
|
||||
|
||||
$pageTranslationVisibility = new PageTranslationVisibility((int)($pageRecord['l18n_cfg'] ?? 0));
|
||||
if ($languageAspect->getId() > 0) {
|
||||
// If the incoming language is set to another language than default
|
||||
$olRec = $this->pageRepository->getPageOverlay($pageRecord, $languageAspect);
|
||||
$overlaidLanguageId = (int)($olRec['language_tag'] ?? 0);
|
||||
if ($overlaidLanguageId !== $languageAspect->getId()) {
|
||||
// If requested translation is not available
|
||||
if ($pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'Page is not available in the requested language.',
|
||||
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931388);
|
||||
}
|
||||
switch ($languageAspect->getLegacyLanguageMode()) {
|
||||
case 'strict':
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'Page is not available in the requested language (strict).',
|
||||
['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE_STRICT_MODE]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931395);
|
||||
case 'content_fallback':
|
||||
// Setting content uid (but leaving the sys_language_uid) when a content_fallback value was found.
|
||||
foreach ($languageAspect->getFallbackChain() as $orderValue) {
|
||||
if ($orderValue === '0' || $orderValue === 0 || $orderValue === '') {
|
||||
$languageContentId = 0;
|
||||
break;
|
||||
}
|
||||
if (MathUtility::canBeInterpretedAsInteger($orderValue) && $overlaidLanguageId === (int)$orderValue) {
|
||||
$languageContentId = (int)$orderValue;
|
||||
break;
|
||||
}
|
||||
if ($orderValue === 'pageNotFound') {
|
||||
// The existing fallbacks have not been found, but instead of continuing page rendering
|
||||
// with default language, a "page not found" message should be shown instead.
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'Page is not available in the requested language (fallbacks did not apply).',
|
||||
['code' => PageAccessFailureReasons::LANGUAGE_AND_FALLBACKS_NOT_AVAILABLE]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931402);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Default is that everything defaults to the default language.
|
||||
$languageId = ($languageContentId = 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Define the language aspect again now
|
||||
$languageAspect = new LanguageAspect(
|
||||
$languageId,
|
||||
$languageContentId,
|
||||
$languageAspect->getOverlayType(),
|
||||
$languageAspect->getFallbackChain()
|
||||
);
|
||||
|
||||
// Setting localized page record if an overlay record was found (which it is only if a language is used)
|
||||
// Doing this ensures that page properties like the page title are resolved in the correct language.
|
||||
$pageInformation->setPageRecord($olRec);
|
||||
}
|
||||
|
||||
// Set the final language aspect!
|
||||
$this->context->setAspect('language', $languageAspect);
|
||||
|
||||
if ((!$languageAspect->getContentId() || !$languageAspect->getId())
|
||||
&& $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
|
||||
) {
|
||||
// If default language is not available
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'Page is not available in default language.',
|
||||
['code' => PageAccessFailureReasons::LANGUAGE_DEFAULT_NOT_AVAILABLE]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931423);
|
||||
}
|
||||
|
||||
if ($languageAspect->getId() > 0) {
|
||||
// Updating rootLine with translations if the language key is set.
|
||||
$pageInformation->setRootLine(
|
||||
$this->getRootlineOrThrow($request, $pageInformation->getId(), $pageInformation->getMountPoint())
|
||||
);
|
||||
}
|
||||
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the value of 'content_from_pid' of the current page record, to see if the
|
||||
* current request should actually show content from another page.
|
||||
* If so, PageInformation->getContentFromPid() is set to the page id of the content
|
||||
* page, while PageInformation->getId() is kept as the original page id.
|
||||
* If there is no 'content_from_pid', PageInformation->getId() and
|
||||
* PageInformation->getContentFromPid() end up carrying the same page ids.
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function setContentFromPid(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$contentFromPid = (int)($pageInformation->getPageRecord()['content_from_pid'] ?? 0);
|
||||
if ($contentFromPid === 0) {
|
||||
// $pageInformation->contentFromPid is always initialized, usually identical with id.
|
||||
$pageInformation->setContentFromPid($pageInformation->getId());
|
||||
return $pageInformation;
|
||||
}
|
||||
// Verify content target pid is good and resolves all access restrictions and similar.
|
||||
$targetPageInformation = new PageInformation();
|
||||
$targetPageInformation->setId($contentFromPid);
|
||||
$targetPageInformation = $this->setPageAndRootline($request, $targetPageInformation);
|
||||
// Above call did not throw. Set the verified id.
|
||||
$pageInformation->setContentFromPid($targetPageInformation->getId());
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the selected backend layout for the current page and add it to the page information
|
||||
*/
|
||||
private function setPageLayout(PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$pageLayout = $this->pageLayoutResolver->getLayoutForPage(
|
||||
$pageInformation->getPageRecord(),
|
||||
$pageInformation->getRootLine()
|
||||
);
|
||||
if ($pageLayout !== null) {
|
||||
$pageInformation->setPageLayout($pageLayout);
|
||||
}
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if visibility of the page is blocked upwards in the root line.
|
||||
*
|
||||
* The blocking feature of a page must be turned on by setting the page
|
||||
* record field 'extendToSubpages' to 1 for 'hidden', 'starttime', 'endtime'
|
||||
* 'fe_group' restrictions to bubble down in rootLine.
|
||||
*
|
||||
* Additionally, this method checks for backend user sections in root line
|
||||
* and if found, evaluates if a backend user is logged in and has access.
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function checkRootlineForIncludeSection(ServerRequestInterface $request, PageInformation $pageInformation): void
|
||||
{
|
||||
$rootLine = $pageInformation->getRootLine();
|
||||
for ($a = 0; $a < count($rootLine); $a++) {
|
||||
$rootLineEntry = $rootLine[$a];
|
||||
if (!$this->accessVoter->accessGrantedForPageInRootLine($rootLineEntry, $this->context)) {
|
||||
// accessGrantedForPageInRootLine() does the main check for 'extendToSubpages'.
|
||||
$response = $this->errorController->accessDeniedAction(
|
||||
$request,
|
||||
'Subsection was found and not accessible',
|
||||
[
|
||||
'code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED,
|
||||
'sub_section' => [
|
||||
0 => $rootLineEntry,
|
||||
],
|
||||
],
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705337296);
|
||||
}
|
||||
if ((int)$rootLineEntry['doktype'] === PageRepository::DOKTYPE_BE_USER_SECTION) {
|
||||
// Only logged in backend users with 'PAGE_SHOW' permissions are allowed to FE render backend user sections.
|
||||
$isBackendUserLoggedIn = $this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false);
|
||||
if (!$isBackendUserLoggedIn) {
|
||||
$response = $this->errorController->accessDeniedAction(
|
||||
$request,
|
||||
'Subsection was found and not accessible',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705416744);
|
||||
}
|
||||
if (!$this->getBackendUser()->doesUserHaveAccess($pageInformation->getPageRecord(), Permission::PAGE_SHOW)) {
|
||||
$response = $this->errorController->accessDeniedAction(
|
||||
$request,
|
||||
'Subsection was found and not accessible',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_SUBSECTION_NOT_RESOLVED]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705337701);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When calling a page with a direct id 'https://my.domain/?id=123',
|
||||
* this the site object of 'my.domain' is determined from an earlier middleware.
|
||||
* If now '123' is *not* a (sub) page of the 'my.domain' site, we error out since
|
||||
* we don't want to directly render content of a different site page within "our" site.
|
||||
* Except if '123' is a shortcut, we still allow it, since that will trigger a
|
||||
* redirect to the url with the shortcut target domain later.
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function checkCrossDomainWithDirectId(ServerRequestInterface $request, PageInformation $pageInformation): void
|
||||
{
|
||||
$directlyRequestedId = (int)($request->getQueryParams()['id'] ?? 0);
|
||||
$shortcutId = (int)($pageInformation->getOriginalShortcutPageRecord()['uid'] ?? 0);
|
||||
if ($directlyRequestedId && $shortcutId !== $directlyRequestedId) {
|
||||
$rootLine = $pageInformation->getRootLine();
|
||||
$siteRootPageId = $request->getAttribute('site')->getRootPageId();
|
||||
$siteRootWithinRootlineFound = false;
|
||||
foreach ($rootLine as $pageInRootLine) {
|
||||
if ((int)$pageInRootLine['uid'] === $siteRootPageId) {
|
||||
$siteRootWithinRootlineFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$siteRootWithinRootlineFound) {
|
||||
$response = $this->errorController->pageNotFoundAction(
|
||||
$request,
|
||||
'ID was outside the domain',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_HOST_PAGE_MISMATCH]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705397417);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a backend user is logged in, it needs at least 'show' permissions.
|
||||
*
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function checkBackendUserAccess(ServerRequestInterface $request, PageInformation $pageInformation): void
|
||||
{
|
||||
// No backend user was logged in, nothing to check
|
||||
if (!$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
|
||||
return;
|
||||
}
|
||||
// PreviewSimulator did not detect anything
|
||||
if (!$this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) {
|
||||
return;
|
||||
}
|
||||
// Editor has no show permission for this page PLUS regular user is not allowed to see the page? 403.
|
||||
if (!$GLOBALS['BE_USER']->doesUserHaveAccess($pageInformation->getPageRecord(), Permission::PAGE_SHOW)
|
||||
&& !$this->accessVoter->accessGranted('pages', $pageInformation->getPageRecord(), $this->context)
|
||||
) {
|
||||
$response = $this->errorController->accessDeniedAction(
|
||||
$request,
|
||||
'ID was not an accessible page',
|
||||
['code' => PageAccessFailureReasons::ACCESS_DENIED_PAGE_NOT_RESOLVED]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705422293);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine relevant sys_template rows and set to PageInformation object.
|
||||
*
|
||||
* @todo: Even though all rootline sys_template records are fetched with only one query
|
||||
* in below implementation, we could potentially join or sub select sys_template
|
||||
* records already when pages rootline is queried. This will save one query.
|
||||
* This could be done when we manage to switch PageRepository / RootlineUtility to a CTE.
|
||||
* @see \TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::getTypoScriptSetup()
|
||||
* for similar usage.
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function setSysTemplateRows(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
$rootLine = $pageInformation->getRootLine();
|
||||
if ($site instanceof Site && $site->isTypoScriptRoot()) {
|
||||
$rootLineUntilSite = [];
|
||||
foreach ($rootLine as $index => $rootlinePage) {
|
||||
$rootLineUntilSite[$index] = $rootlinePage;
|
||||
$pageId = (int)($rootlinePage['uid'] ?? 0);
|
||||
if ($pageId === $site->getRootPageId()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$rootLine = $rootLineUntilSite;
|
||||
}
|
||||
$sysTemplateRows = $this->sysTemplateRepository->getSysTemplateRowsByRootline($rootLine, $request);
|
||||
$pageInformation->setSysTemplateRows($sysTemplateRows);
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate "local" rootLine that stops at first root=1 template.
|
||||
*/
|
||||
private function setLocalRootLine(ServerRequestInterface $request, PageInformation $pageInformation): PageInformation
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
$sysTemplateRows = $pageInformation->getSysTemplateRows();
|
||||
$rootLine = $pageInformation->getRootLine();
|
||||
$sysTemplateRowsIndexedByPid = array_combine(array_column($sysTemplateRows, 'pid'), $sysTemplateRows);
|
||||
$localRootline = [];
|
||||
foreach ($rootLine as $rootlinePage) {
|
||||
array_unshift($localRootline, $rootlinePage);
|
||||
$pageId = (int)($rootlinePage['uid'] ?? 0);
|
||||
if ($pageId === $site->getRootPageId() && $site->isTypoScriptRoot()) {
|
||||
break;
|
||||
}
|
||||
if ($pageId > 0 && (int)($sysTemplateRowsIndexedByPid[$pageId]['root'] ?? 0) === 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$pageInformation->setLocalRootLine($localRootline);
|
||||
return $pageInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-array
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function getRootlineOrThrow(ServerRequestInterface $request, int $pageId, string $mountPoint): array
|
||||
{
|
||||
$rootLine = [];
|
||||
try {
|
||||
$rootLine = GeneralUtility::makeInstance(RootlineUtility::class, $pageId, $mountPoint)->get();
|
||||
} catch (RootLineException) {
|
||||
// Empty / broken rootline handled below.
|
||||
}
|
||||
if (!empty($rootLine)) {
|
||||
// All good.
|
||||
return $rootLine;
|
||||
}
|
||||
// Error case: Log and render error.
|
||||
$message = 'The requested page did not have a proper connection to the tree root!';
|
||||
$context = [
|
||||
'pageId' => $pageId,
|
||||
'requestUrl' => $request->getAttribute('normalizedParams')->getRequestUrl(),
|
||||
];
|
||||
$this->logger->error($message, $context);
|
||||
try {
|
||||
$response = $this->errorController->internalErrorAction(
|
||||
$request,
|
||||
$message,
|
||||
['code' => PageAccessFailureReasons::ROOTLINE_BROKEN]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1533931350);
|
||||
} catch (StatusException $up) {
|
||||
$this->logger->error($message, ['exception' => $up]);
|
||||
throw $up;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PageInformationCreationFailedException
|
||||
* @throws StatusException
|
||||
*/
|
||||
private function verifySiteOrSysTemplateRowExists(ServerRequestInterface $request, PageInformation $pageInformation): void
|
||||
{
|
||||
$site = $request->getAttribute('site');
|
||||
if ((!$site instanceof NullSite && !$site->isTypoScriptRoot()) && $pageInformation->getSysTemplateRows() === []) {
|
||||
// @todo: The above check for NullSite is done for ext:redirects to not explode on "not existing" sites here.
|
||||
// This is of course a hack that should vanish when the early url creation of ext:redirects and its fragile
|
||||
// bootstrap strategy is resolved.
|
||||
// This check should be: "if (!$site->isTypoScriptRoot() && $pageInformation->getSysTemplateRows() === []) {"
|
||||
// Early exception if there is no typoscript definition in current site and no sys_template at all.
|
||||
$message = 'No site configuration or TypoScript template record found!';
|
||||
$this->logger->error($message);
|
||||
try {
|
||||
$response = $this->errorController->internalErrorAction(
|
||||
$request,
|
||||
$message,
|
||||
['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_FOUND]
|
||||
);
|
||||
throw new PageInformationCreationFailedException($response, 1705656657);
|
||||
} catch (StatusException $up) {
|
||||
$this->logger->error($message, ['exception' => $up]);
|
||||
throw $up;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getBackendUser(): ?FrontendBackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?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\Page;
|
||||
|
||||
/**
|
||||
* Data object to collect sections the full response body is compiled from.
|
||||
* This is used and fed by various related middlewares.
|
||||
*
|
||||
* This object is attached as "frontend.page.parts" attribute to the frontend
|
||||
* application request object.
|
||||
*
|
||||
* Note most of the data accumulated here is part of the page cache row, so the
|
||||
* state is either generated from scratch during first page rendering, or recreated
|
||||
* from cache upon consecutive renderings.
|
||||
*
|
||||
* This data object is highly experimental and marked as @internal since it
|
||||
* will likely change when the rendering and cache related parts of the frontend
|
||||
* middleware chain see further refactorings.
|
||||
*
|
||||
* @internal Experimental, will change.
|
||||
*/
|
||||
final class PageParts
|
||||
{
|
||||
/**
|
||||
* Feed with the initial http body content string in TypoScriptFrontendInitialization
|
||||
* when content is fetched from cache. It is later used in FE RequestHandler to substitute
|
||||
* INT placeholders with their actual content (among other things) and then set as
|
||||
* Response HTTP body.
|
||||
*
|
||||
* @todo: This exists here since we need some place to park the initial content retrieved from cache.
|
||||
* This property could (should?) vanish at some point when the middleware PrepareTypoScriptFrontendRendering
|
||||
* and the RequestHandler see further refactoring loops. Note there are events like AfterCacheableContentIsGeneratedEvent
|
||||
* that already allow custom manipulation of content.
|
||||
*/
|
||||
private string $content = '';
|
||||
|
||||
/**
|
||||
* "Last change" is page record "SYS_LASTCHANGED", initialized with pages record "tstamp", whichever is higher.
|
||||
* This value is later modified by ContentObjectRenderer when records are rendered. The pages record column
|
||||
* "SYS_LASTCHANGED" is then written to the highest value at the end of FE rendering.
|
||||
* The main goal of pages "SYS_LASTCHANGED" is to have a DB field on pages that "knows" when a record that is
|
||||
* displayed on the page is last changed. This information is for instance used in the ext:seo sitemap XML.
|
||||
*
|
||||
* This approach is flawed for multimple reasons: The "last updated" value can only be "gathered" during FE
|
||||
* rendering since BE does not necessarily know all elements rendered on a page when for example a news plugin
|
||||
* fetches record from "elsewhere", e.g. a record storage page. This means the "final" value of pages "SYS_LASTCHANGED"
|
||||
* is only ready after all content elements have been rendered. It also relies on such a plugin actively taking
|
||||
* care of updating $lastChanged here (@see ContentObjectRenderer->lastChanged()). Rendering a "Last updated"
|
||||
* value on a page thus only works when it is output at the end (or as USER_INT which are calculated in the end).
|
||||
* Additionally, a plugin like the ext:seo pages sitemap (which only gives a list of pages but does not actually
|
||||
* render all pages) can only render a correct value after a page containing a "just changed" content element
|
||||
* has been FE rendered at least once to have the newest "SYS_LASTCHANGED" value in DB.
|
||||
*/
|
||||
private int $lastChanged = 0;
|
||||
|
||||
/**
|
||||
* Becomes the HTTP Response header 'Content-Type'.
|
||||
* This is obviously Response and not Response body-stream related, but we currently "park" it here for
|
||||
* applications like Extbase that need to reset to something like application/json.
|
||||
*
|
||||
* @todo: This should be remodeled. This should be part of a construct that gathers page parts and
|
||||
* Response related details to finally compile a Response in the end. "Parking" that information
|
||||
* here is a temporary measure on the way to a better solution where for instance single
|
||||
* cObj return a data object instead of a string.
|
||||
*/
|
||||
private string $httpContentType = 'text/html; charset=utf-8';
|
||||
|
||||
/**
|
||||
* Registry of "not cached" elements, typically COA_INT, USER_INT and uncached extbase plugins. Each
|
||||
* entry contains state needed to render the element, most contain a placeholder name to be substituted
|
||||
* within the content (due to the current FE rendering structure), and the rendering definition of the
|
||||
* element. See the single consumers for details.
|
||||
*/
|
||||
private array $notCachedContentElementRegistry = [];
|
||||
|
||||
/**
|
||||
* Data the final page title is generated from.
|
||||
*/
|
||||
private array $pageTitle = [];
|
||||
|
||||
/**
|
||||
* Unique string for PageRenderer to substitute placeholders with final data.
|
||||
*/
|
||||
private string $pageRendererSubstitutionHash = '';
|
||||
|
||||
private bool $pageContentWasLoadedFromCache = false;
|
||||
|
||||
private int $pageCacheGeneratedTimestamp;
|
||||
|
||||
private ?int $pageCacheExpiresTimestamp = null;
|
||||
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setLastChanged(int $timestamp): void
|
||||
{
|
||||
$this->lastChanged = $timestamp;
|
||||
}
|
||||
|
||||
public function getLastChanged(): int
|
||||
{
|
||||
return $this->lastChanged;
|
||||
}
|
||||
|
||||
public function setHttpContentType(string $contentType): void
|
||||
{
|
||||
$this->httpContentType = $contentType;
|
||||
}
|
||||
|
||||
public function getHttpContentType(): string
|
||||
{
|
||||
return $this->httpContentType;
|
||||
}
|
||||
|
||||
public function addNotCachedContentElement(array $data): void
|
||||
{
|
||||
$this->notCachedContentElementRegistry[] = $data;
|
||||
}
|
||||
|
||||
public function hasNotCachedContentElements(): bool
|
||||
{
|
||||
return !empty($this->notCachedContentElementRegistry);
|
||||
}
|
||||
|
||||
public function getNotCachedContentElementRegistry(): array
|
||||
{
|
||||
return $this->notCachedContentElementRegistry;
|
||||
}
|
||||
|
||||
public function setPageTitle(array $pageTitle): void
|
||||
{
|
||||
$this->pageTitle = $pageTitle;
|
||||
}
|
||||
|
||||
public function getPageTitle(): array
|
||||
{
|
||||
return $this->pageTitle;
|
||||
}
|
||||
|
||||
public function setPageRendererSubstitutionHash(string $hash): void
|
||||
{
|
||||
$this->pageRendererSubstitutionHash = $hash;
|
||||
}
|
||||
|
||||
public function getPageRendererSubstitutionHash(): string
|
||||
{
|
||||
return $this->pageRendererSubstitutionHash;
|
||||
}
|
||||
|
||||
public function setPageContentWasLoadedFromCache(): void
|
||||
{
|
||||
$this->pageContentWasLoadedFromCache = true;
|
||||
}
|
||||
|
||||
public function hasPageContentBeenLoadedFromCache(): bool
|
||||
{
|
||||
return $this->pageContentWasLoadedFromCache;
|
||||
}
|
||||
|
||||
public function setPageCacheGeneratedTimestamp(int $timestamp): void
|
||||
{
|
||||
$this->pageCacheGeneratedTimestamp = $timestamp;
|
||||
}
|
||||
|
||||
public function getPageCacheGeneratedTimestamp(): int
|
||||
{
|
||||
return $this->pageCacheGeneratedTimestamp;
|
||||
}
|
||||
|
||||
public function setPageCacheExpireTimestamp(int $timestamp): void
|
||||
{
|
||||
$this->pageCacheExpiresTimestamp = $timestamp;
|
||||
}
|
||||
|
||||
public function getPageCacheExpireTimestamp(): ?int
|
||||
{
|
||||
return $this->pageCacheExpiresTimestamp;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user