Commit d05865f7 authored by Helmut Hummel's avatar Helmut Hummel Committed by Richard Haeser
Browse files

[BUGFIX] Remove caches for page title and meta tag



By concept for frontend rendering the page title and meta tags
are not meant to be stored in page cache in order to allow
non cachable plugins to modify those.

Currently both page title and meta tags are stored
in separate cache entries, which violates the concept above
and unnecessarily tightly couples those code parts to the
TypoScriptFrontendController and internal logic of it.

This patch removes these caches.

In order to properly handle the uncached rendering state,
we make sure the meta tag registry is properly
serialized with all managers.

Resolves: #88179
Releases: master, 9.5
Change-Id: If5200398bf9ab9db09ab97403c976d82cb33d01d
Signed-off-by: Frank Nägler's avatarFrank Naegler <frank.naegler@typo3.org>
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/60520


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Helmut Hummel's avatarHelmut Hummel <typo3@helhum.io>
Tested-by: Richard Haeser's avatarRichard Haeser <richard@maxserv.com>
Reviewed-by: Helmut Hummel's avatarHelmut Hummel <typo3@helhum.io>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@maxserv.com>
parent f04022b6
......@@ -16,9 +16,7 @@ namespace TYPO3\CMS\Core\MetaTag;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\SingletonInterface;
abstract class AbstractMetaTagManager implements MetaTagManagerInterface, SingletonInterface
abstract class AbstractMetaTagManager implements MetaTagManagerInterface
{
/**
* The default attribute that defines the name of the property
......
......@@ -16,13 +16,11 @@ namespace TYPO3\CMS\Core\MetaTag;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\SingletonInterface;
/**
* Handles typical meta tags (non-grouped). Use AbstractMetaTagManager
* to create you own MetaTags, this class is final by design
*/
final class GenericMetaTagManager implements MetaTagManagerInterface, SingletonInterface
final class GenericMetaTagManager implements MetaTagManagerInterface
{
/**
* The separator to define subproperties like og:image:width
......
......@@ -26,6 +26,8 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
class MetaTagManagerRegistry implements SingletonInterface
{
protected $registry = [];
private $instances = [];
private $managers;
public function __construct()
{
......@@ -53,6 +55,7 @@ class MetaTagManagerRegistry implements SingletonInterface
'before' => $before,
'after' => $after
];
$this->managers = null;
}
/**
......@@ -81,18 +84,24 @@ class MetaTagManagerRegistry implements SingletonInterface
*/
public function getAllManagers(): array
{
if ($this->managers !== null) {
return $this->managers;
}
$orderedManagers = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies(
$this->registry
);
$managers = [];
$this->managers = [];
foreach ($orderedManagers as $manager => $managerConfiguration) {
if (class_exists($managerConfiguration['module'])) {
$managers[$manager] = GeneralUtility::makeInstance($managerConfiguration['module']);
$module = $managerConfiguration['module'];
if (class_exists($module)) {
$this->instances[$module] = $this->instances[$module] ?? GeneralUtility::makeInstance($module);
$this->managers[$manager] = $this->instances[$module];
}
}
return $managers;
return $this->managers;
}
/**
......@@ -100,6 +109,7 @@ class MetaTagManagerRegistry implements SingletonInterface
*/
public function removeAllManagers()
{
unset($this->registry);
$this->registry = [];
$this->managers = null;
}
}
......@@ -18,7 +18,6 @@ use TYPO3\CMS\Backend\Routing\Router;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\DocumentTemplate;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Localization\LocalizationFactory;
......@@ -170,13 +169,6 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
*/
protected $metaTags = [];
/**
* META Tags added via the API
*
* @var array
*/
protected $metaTagsByAPI = [];
/**
* @var array
*/
......@@ -358,6 +350,15 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
$this->setMetaTag('name', 'generator', 'TYPO3 CMS');
}
/**
* Set restored meta tag managers as singletons
* so that uncached plugins can use them to add or remove meta tags
*/
public function __wakeup()
{
GeneralUtility::setSingletonInstance(get_class($this->metaTagRegistry), $this->metaTagRegistry);
}
/**
* @param FrontendInterface $cache
*/
......@@ -918,7 +919,6 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
1496402460
);
}
$manager = $this->metaTagRegistry->getManagerForProperty($name);
$manager->addProperty($name, $content, $subProperties, $replace, $type);
}
......@@ -1674,33 +1674,11 @@ class PageRenderer implements \TYPO3\CMS\Core\SingletonInterface
{
$metaTags = [];
$metaTagManagers = $this->metaTagRegistry->getAllManagers();
try {
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
} catch (NoSuchCacheException $e) {
$cache = null;
}
foreach ($metaTagManagers as $manager => $managerObject) {
$cacheIdentifier = $this->getTypoScriptFrontendController()->newHash . '-metatag-' . $manager;
$existingCacheEntry = false;
if ($cache instanceof FrontendInterface && $properties = $cache->get($cacheIdentifier)) {
$existingCacheEntry = true;
} else {
$properties = $managerObject->renderAllProperties();
}
$properties = $managerObject->renderAllProperties();
if (!empty($properties)) {
$metaTags[] = $properties;
if ($cache instanceof FrontendInterface && !$existingCacheEntry && ($this->getTypoScriptFrontendController()->page['uid'] ?? false)) {
$cache->set(
$cacheIdentifier,
$properties,
['pageId_' . $this->getTypoScriptFrontendController()->page['uid']],
$this->getTypoScriptFrontendController()->get_cache_timeout()
);
}
}
}
return $metaTags;
......
......@@ -16,30 +16,16 @@ namespace TYPO3\CMS\Core\PageTitle;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Service\DependencyOrderingService;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
/**
* This class will take care of the different providers and returns the title with the highest priority
*/
class PageTitleProviderManager implements SingletonInterface
{
/**
* @var FrontendInterface
*/
protected $pageCache;
public function __construct()
{
$this->initCaches();
}
/**
* @return string
* @throws \TYPO3\CMS\Core\Cache\Exception
......@@ -56,22 +42,10 @@ class PageTitleProviderManager implements SingletonInterface
->orderByDependencies($titleProviders);
foreach ($orderedTitleProviders as $provider => $configuration) {
$cacheIdentifier = $this->getTypoScriptFrontendController()->newHash . '-titleTag-' . $provider;
if ($this->pageCache instanceof FrontendInterface &&
$pageTitle = $this->pageCache->get($cacheIdentifier)
) {
break;
}
if (class_exists($configuration['provider']) && is_subclass_of($configuration['provider'], PageTitleProviderInterface::class)) {
/** @var PageTitleProviderInterface $titleProviderObject */
$titleProviderObject = GeneralUtility::makeInstance($configuration['provider']);
if ($pageTitle = $titleProviderObject->getTitle()) {
$this->pageCache->set(
$cacheIdentifier,
$pageTitle,
['pageId_' . $this->getTypoScriptFrontendController()->page['uid']],
$this->getTypoScriptFrontendController()->get_cache_timeout()
);
break;
}
}
......@@ -80,14 +54,6 @@ class PageTitleProviderManager implements SingletonInterface
return $pageTitle;
}
/**
* @return \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
*/
private function getTypoScriptFrontendController(): TypoScriptFrontendController
{
return $GLOBALS['TSFE'];
}
/**
* Get the TypoScript configuration for pageTitleProviders
* @return array
......@@ -96,24 +62,12 @@ class PageTitleProviderManager implements SingletonInterface
{
$typoscriptService = GeneralUtility::makeInstance(TypoScriptService::class);
$config = $typoscriptService->convertTypoScriptArrayToPlainArray(
$this->getTypoScriptFrontendController()->config['config'] ?? []
$GLOBALS['TSFE']->config['config'] ?? []
);
return $config['pageTitleProviders'] ?? [];
}
/**
* Initializes the caching system.
*/
protected function initCaches(): void
{
try {
$this->pageCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('pages');
} catch (NoSuchCacheException $e) {
// Intended fall-through
}
}
/**
* @param array $orderInformation
* @return string[]
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\TestMeta\Controller;
/*
* 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!
*/
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider;
class MetaPluginController
{
/**
* @param string Empty string (no content to process)
* @param array TypoScript configuration
* @return string
*/
public function setMetaData($content, $configuration): string
{
$pageId = $GLOBALS['TYPO3_REQUEST']->getQueryParams()['id'];
GeneralUtility::makeInstance(CustomPageTitleProvider::class)
->setTitle('static title with pageId: ' . $pageId . ' and pluginNumber: ' . $configuration['pluginNumber']);
$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)->getManagerForProperty('og:title');
$metaTagManager->addProperty(
'og:title',
'OG title from a controller with pageId: ' . $pageId . ' and pluginNumber: ' . $configuration['pluginNumber'],
[],
true
);
return 'TYPO3\CMS\TestMeta\Controller::setMetaData';
}
}
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\TestMeta\PageTitle;
/*
* 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!
*/
use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;
class CustomPageTitleProvider extends AbstractPageTitleProvider
{
public function setTitle(string $title): void
{
$this->title = $title;
}
}
config.pageTitleProviders {
testMetaProvider {
provider = TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider
before = record
after = altPageTitle
}
}
page = PAGE
page.5 = TEXT
page.5.value = MetaData-Test
page.5.stdWrap.wrap = <h1>|</h1>
config.pageTitleProviders {
testMetaProvider {
provider = TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider
before = record
after = altPageTitle
}
}
page = PAGE
page.5 = TEXT
page.5.value = MetaData-Test
page.5.stdWrap.wrap = <h1>|</h1>
page.10 = USER
page.10 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 10
}
page.20 = USER
page.20 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 20
}
config.pageTitleProviders {
testMetaProvider {
provider = TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider
before = record
after = altPageTitle
}
}
page = PAGE
page.5 = TEXT
page.5.value = MetaData-Test
page.5.stdWrap.wrap = <h1>|</h1>
page.10 = USER_INT
page.10 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 10
}
page.20 = USER_INT
page.20 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 20
}
config.pageTitleProviders {
testMetaProvider {
provider = TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider
before = record
after = altPageTitle
}
}
page = PAGE
page.5 = TEXT
page.5.value = MetaData-Test
page.5.stdWrap.wrap = <h1>|</h1>
page.10 = USER
page.10 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 10
}
page.20 = USER_INT
page.20 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 20
}
config.pageTitleProviders {
testMetaProvider {
provider = TYPO3\CMS\TestMeta\PageTitle\CustomPageTitleProvider
before = record
after = altPageTitle
}
}
page = PAGE
page.5 = TEXT
page.5.value = MetaData-Test
page.5.stdWrap.wrap = <h1>|</h1>
page.10 = USER_INT
page.10 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 10
}
page.20 = USER
page.20 {
userFunc = TYPO3\CMS\TestMeta\Controller\MetaPluginController->setMetaData
pluginNumber = 20
}
<?php
$EM_CONF[$_EXTKEY] = [
'title' => 'MetaData Test',
'description' => 'MetaData Test',
'category' => 'example',
'version' => '10.0.0',
'state' => 'beta',
'clearCacheOnLoad' => 0,
'author' => 'Frank Nägler',
'author_email' => 'frank.naegler@typo3.org',
'author_company' => '',
'constraints' => [
'depends' => [
'typo3' => '10.0.0',
'seo' => '10.0.0',
],
'conflicts' => [],
'suggests' => [],
],
];
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<pages>
<uid>1</uid>
<pid>0</pid>
<title>Rootpage for Tests</title>
<deleted>0</deleted>
</pages>
<pages>
<uid>2</uid>
<pid>1</pid>
<title>Page with USER Plugins</title>
<deleted>0</deleted>
</pages>
<pages>
<uid>3</uid>
<pid>1</pid>
<title>Page with USER_INT Plugins</title>
<deleted>0</deleted>
</pages>
<pages>
<uid>4</uid>
<pid>1</pid>
<title>Page with USER_INT and USER Plugins</title>
<deleted>0</deleted>
</pages>
<pages>
<uid>5</uid>
<pid>1</pid>
<title>Page with USER and USER_INT Plugins</title>
<deleted>0</deleted>
</pages>
</dataset>
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\Tests\Functional\MetaDataHandling;
/*
* 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!
*/
use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\AbstractTestCase;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
/**
* Functional test for the DataHandler
*/
class PluginsTest extends AbstractTestCase
{
protected function setUp(): void
{
$this->testExtensionsToLoad[] = 'typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_meta';
parent::setUp();
$this->importDataSet(ORIGINAL_ROOT . 'typo3/sysext/core/Tests/Functional/Fixtures/Scenarios/pages_with_plugins_seo_meta.xml');
$this->writeSiteConfiguration(
'website-local',
$this->buildSiteConfiguration(1, 'http://localhost/'),
[
$this->buildDefaultLanguageConfiguration('EN', '/')
]
);
}
public function ensurePageSetupIsOkDataProvider(): array
{
return [
'page:uid:1' => [1, false],
'page:uid:2' => [2, true],
'page:uid:3' => [3, true],
'page:uid:4' => [4, true],
'page:uid:5' => [5, true],
];
}
/**
* @test
* @dataProvider ensurePageSetupIsOkDataProvider
* @param int $pageId
* @param bool $expectPluginOutput
*/
public function ensurePageSetupIsOk(int $pageId, bool $expectPluginOutput): void
{
$this->setUpFrontendRootPage(1, ['typo3/sysext/core/Tests/Functional/Fixtures/Extensions/test_meta/Configuration/TypoScript/page' . $pageId . '.typoscript']);
$response = $this->executeFrontendRequest(
(new InternalRequest('http://localhost/'))->withQueryParameters([
'id' => $pageId,
])
);
$body = (string)$response->getBody();
$this->assertStringContainsString('<h1>MetaData-Test</h1>', $body);
if ($expectPluginOutput) {
$this->assertStringContainsString('TYPO3\CMS\TestMeta\Controller::setMetaData', $body);
} else {
$this->assertStringNotContainsString('TYPO3\CMS\TestMeta\Controller::setMetaData', $body);
}
}
public function ensureMetaDataAreCorrectDataProvider(): array
{
return [
'page:uid:1' => [1, 'Rootpage for Tests', ''],
'page:uid:2' => [2, 'static title with pageId: 2 and pluginNumber: 20', 'OG title from a controller with pageId: 2 and pluginNumber: 20'],
'page:uid:3' => [3, 'static title with pageId: 3 and pluginNumber: 20', 'OG title from a controller with pageId: 3 and pluginNumber: 20'],
'page:uid:4' => [4, 'static title with pageId: 4 and pluginNumber: 20', 'OG title from a controller with pageId: 4 and pluginNumber: 20'],
'page:uid:5' => [5, 'static title with pageId: 5 and pluginNumber: 10', 'OG title from a controller with pageId: 5 and pluginNumber: 10'],
];
}
/**
* This test ensures that the meta data and title of the page are the same
* even if the pages is delivered cached or uncached.
*
* @test
* @dataProvider ensureMetaDataAreCorrectDataProvider
* @param int $pageId
* @param string $expectedTitle
* @param string $expectedMetaOgTitle