Skip to content

Commit

Permalink
[FEATURE] Add Page TSconfig provider for sites and sets
Browse files Browse the repository at this point in the history
TYPO3 sites have been enhanced to be able to provide Page TSconfig on
a per site basis.

Releases: main
Resolves: #103522
Change-Id: I1558ca9265fcd0fd9bef83cb47ec9c54a7a0d59c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83586
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: Benjamin Franzke <ben@bnf.dev>
Tested-by: Benjamin Kott <benjamin.kott@outlook.com>
Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
  • Loading branch information
bnf committed Apr 7, 2024
1 parent 6107ba3 commit b90a463
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 15 deletions.
30 changes: 29 additions & 1 deletion typo3/sysext/core/Classes/Configuration/SiteConfiguration.php
Expand Up @@ -31,6 +31,7 @@
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteSettings;
use TYPO3\CMS\Core\Site\Entity\SiteTSconfig;
use TYPO3\CMS\Core\Site\Entity\SiteTypoScript;
use TYPO3\CMS\Core\Site\SiteSettingsFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
Expand Down Expand Up @@ -65,6 +66,13 @@ class SiteConfiguration implements SingletonInterface
*/
protected string $typoScriptConstantsFileName = 'constants.typoscript';

/**
* File naming containing page TSconfig definitions
*
* @internal
*/
protected string $pageTSconfigFileName = 'page.tsconfig';

/**
* YAML file name with all settings related to Content-Security-Policies.
*
Expand Down Expand Up @@ -123,11 +131,12 @@ public function resolveAllExistingSites(bool $useCache = true): array
$identifier = (string)$identifier;
$siteSettings = $this->siteSettingsFactory->getSettings($identifier, $configuration);
$siteTypoScript = $this->getSiteTypoScript($identifier);
$siteTSconfig = $this->getSiteTSconfig($identifier);
$configuration['contentSecurityPolicies'] = $this->getContentSecurityPolicies($identifier);

$rootPageId = (int)($configuration['rootPageId'] ?? 0);
if ($rootPageId > 0) {
$sites[$identifier] = new Site($identifier, $rootPageId, $configuration, $siteSettings, $siteTypoScript);
$sites[$identifier] = new Site($identifier, $rootPageId, $configuration, $siteSettings, $siteTypoScript, $siteTSconfig);
}
}
$this->firstLevelCache = $sites;
Expand Down Expand Up @@ -252,6 +261,25 @@ protected function getSiteTypoScript(string $siteIdentifier): ?SiteTypoScript
return new SiteTypoScript(...$definitions);
}

protected function getSiteTSconfig(string $siteIdentifier): ?SiteTSconfig
{
$pageTSconfig = null;
$path = $this->configPath . '/' . $siteIdentifier . '/' . $this->pageTSconfigFileName;
if (file_exists($path)) {
$contents = @file_get_contents(GeneralUtility::fixWindowsFilePath($path));
if ($contents !== false) {
$pageTSconfig = $contents;
}
}
if ($pageTSconfig === null) {
return null;
}

return new SiteTSconfig(
pageTSconfig: $pageTSconfig
);
}

protected function getContentSecurityPolicies(string $siteIdentifier): array
{
$fileName = $this->configPath . '/' . $siteIdentifier . '/' . $this->contentSecurityFileName;
Expand Down
15 changes: 13 additions & 2 deletions typo3/sysext/core/Classes/Site/Entity/Site.php
Expand Up @@ -84,10 +84,12 @@ class Site implements SiteInterface

protected ?SiteTypoScript $typoscript;

protected ?SiteTSconfig $tsConfig;

/**
* Sets up a site object, and its languages, error handlers and the settings
*/
public function __construct(string $identifier, int $rootPageId, array $configuration, SiteSettings $settings = null, ?SiteTypoScript $typoscript = null)
public function __construct(string $identifier, int $rootPageId, array $configuration, SiteSettings $settings = null, ?SiteTypoScript $typoscript = null, ?SiteTSconfig $tsConfig = null)
{
$this->identifier = $identifier;
$this->rootPageId = $rootPageId;
Expand All @@ -96,6 +98,7 @@ public function __construct(string $identifier, int $rootPageId, array $configur
}
$this->settings = $settings;
$this->typoscript = $typoscript;
$this->tsConfig = $tsConfig;
// Merge settings back in configuration for backwards-compatibility
$configuration['settings'] = $this->settings->getAll();
$this->configuration = $configuration;
Expand Down Expand Up @@ -331,14 +334,22 @@ public function getSettings(): SiteSettings
*/
public function isTypoScriptRoot(): bool
{
return $this->sets !== [] || $this->typoscript !== null;
return $this->sets !== [] || $this->typoscript !== null || $this->tsConfig !== null;
}

public function getTypoScript(): ?SiteTypoScript
{
return $this->typoscript;
}

/**
* @internal
*/
public function getTSconfig(): ?SiteTSconfig
{
return $this->tsConfig;
}

/**
* Returns a single configuration attribute
*
Expand Down
33 changes: 33 additions & 0 deletions typo3/sysext/core/Classes/Site/Entity/SiteTSconfig.php
@@ -0,0 +1,33 @@
<?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\Core\Site\Entity;

/**
* @internal
*/
final readonly class SiteTSconfig
{
public function __construct(
public ?string $pageTSconfig = null,
) {}

public static function __set_state(array $state): self
{
return new self(...$state);
}
}
1 change: 1 addition & 0 deletions typo3/sysext/core/Classes/Site/Set/SetDefinition.php
Expand Up @@ -32,6 +32,7 @@ public function __construct(
public array $optionalDependencies = [],
public array $settingsDefinitions = [],
public ?string $typoscript = null,
public ?string $pagets = null,
public array $settings = [],
) {}

Expand Down
Expand Up @@ -103,6 +103,7 @@ protected function createDefinition(array $set, string $basePath): SetDefinition
'settingsDefinitions' => $settingsDefinitions,
];
$setData['typoscript'] ??= $basePath;
$setData['pagets'] ??= $basePath . '/page.tsconfig';
return new SetDefinition(...$setData);
} catch (\Error $e) {
throw new \Exception('Invalid set definition: ' . json_encode($set), 1170859526, $e);
Expand Down
Expand Up @@ -20,7 +20,10 @@
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\EventDispatcher\EventDispatcher;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedPageTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\BeforeLoadedUserTsConfigEvent;
use TYPO3\CMS\Core\TypoScript\IncludeTree\Event\ModifyLoadedPageTsConfigEvent;
Expand All @@ -43,6 +46,8 @@ public function __construct(
private readonly TreeFromLineStreamBuilder $treeFromTokenStreamBuilder,
private readonly PackageManager $packageManager,
private readonly EventDispatcher $eventDispatcher,
private readonly SiteFinder $siteFinder,
private readonly SetRegistry $setRegistry,
) {}

public function getUserTsConfigTree(
Expand Down Expand Up @@ -109,6 +114,33 @@ public function getPagesTsConfigTree(
?PhpFrontend $cache = null
): RootInclude {
$collectedPagesTsConfigArray = [];

$collectedPagesTsConfigArray += $this->getPackagePageTsConfigTree($cache);

// @deprecated since TYPO3 v13. Remove in v14 along with defaultPageTSconfig and EMU::addPageTSConfig
if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '')) {
$collectedPagesTsConfigArray['pagesTsConfig-globals-defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
}

// HEADS up: rootLine may be modified by getSitePagesTsConfigTree
$collectedPagesTsConfigArray += $this->getSitePageTsConfigTree($rootLine, $cache);

$collectedPagesTsConfigArray += $this->getRootlinePageTsConfigTree($rootLine, $cache);

$event = $this->eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($collectedPagesTsConfigArray, $rootLine));
$collectedPagesTsConfigArray = $event->getTsConfig();

$includeTree = new RootInclude();
foreach ($collectedPagesTsConfigArray as $key => $typoScriptString) {
$includeTree->addChild($this->getTreeFromString((string)$key, $typoScriptString, $tokenizer, $cache));
}
return $includeTree;
}

private function getPackagePageTsConfigTree(
?PhpFrontend $cache = null
): array {
$collectedPagesTsConfigArray = [];
$gotPackagesPagesTsConfigFromCache = false;
if ($cache) {
$collectedPagesTsConfigArrayFromCache = $cache->require('pagestsconfig-packages-strings');
Expand Down Expand Up @@ -137,12 +169,64 @@ public function getPagesTsConfigTree(
}
$cache?->set('pagestsconfig-packages-strings', 'return unserialize(\'' . addcslashes(serialize($collectedPagesTsConfigArray), '\'\\') . '\');');
}
return $collectedPagesTsConfigArray;
}

// @deprecated since TYPO3 v13. Remove in v14 along with defaultPageTSconfig and EMU::addPageTSConfig
if (!empty($GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'] ?? '')) {
$collectedPagesTsConfigArray['pagesTsConfig-globals-defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
private function getSitePageTsConfigTree(
array &$rootLine,
?PhpFrontend $cache = null
): array {
$reverseRootLine = array_reverse($rootLine);
$rootlineUntilSite = [];
$rootSite = null;
foreach ($reverseRootLine as $rootLineEntry) {
array_unshift($rootlineUntilSite, $rootLineEntry);
$uid = (int)($rootLineEntry['uid'] ?? 0);
if ($uid === 0) {
continue;
}
try {
$site = $this->siteFinder->getSiteByRootPageId($uid);
} catch (SiteNotFoundException) {
continue;
}
if ($site->isTypoScriptRoot()) {
$rootSite = $site;
$rootLine = $rootlineUntilSite;
break;
}
}

if ($rootSite === null) {
return [];
}

$cacheIdentifier = 'pagestsconfig-site-' . $rootSite->getIdentifier();
$pageTsConfig = $cache?->require($cacheIdentifier) ?: null;

if ($pageTsConfig === null) {
$pageTsConfig = [];
$sets = $this->setRegistry->getSets(...$rootSite->getSets());
foreach ($sets as $set) {
if ($set->pagets !== null && file_exists($set->pagets)) {
$content = @file_get_contents($set->pagets);
if (!empty($content)) {
$pageTsConfig['pageTsConfig-set-' . str_replace('/', '-', $set->name)] = $content;
}
}
}

$pageTsConfig['pageTsConfig-site-' . $rootSite->getIdentifier()] = $rootSite->getTSconfig()->pageTSconfig ?? '';
$cache?->set($cacheIdentifier, 'return ' . var_export($pageTsConfig, true) . ';');
}
return $pageTsConfig;
}

private function getRootlinePageTsConfigTree(
array $rootLine,
?PhpFrontend $cache = null
): array {
$collectedPagesTsConfigArray = [];
foreach ($rootLine as $page) {
if (empty($page['uid'])) {
// Page 0 can happen when the rootline is given from BE context. It has not TSconfig. Skip this.
Expand Down Expand Up @@ -173,15 +257,7 @@ public function getPagesTsConfigTree(
$collectedPagesTsConfigArray['pagesTsConfig-page-' . $page['uid'] . '-tsConfig'] = $page['TSconfig'];
}
}

$event = $this->eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($collectedPagesTsConfigArray, $rootLine));
$collectedPagesTsConfigArray = $event->getTsConfig();

$includeTree = new RootInclude();
foreach ($collectedPagesTsConfigArray as $key => $typoScriptString) {
$includeTree->addChild($this->getTreeFromString((string)$key, $typoScriptString, $tokenizer, $cache));
}
return $includeTree;
return $collectedPagesTsConfigArray;
}

private function getTreeFromString(
Expand Down
@@ -0,0 +1,32 @@
.. include:: /Includes.rst.txt

.. _feature-103522-1712323334:

============================================================
Feature: #103522 - Page TSconfig provider for Sites and Sets
============================================================

See :issue:`103522`

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

TYPO3 sites have been enhanced to be able to provide Page TSconfig on a per site
basis.

Site Page TSconfig is loaded from :file:`page.tsconfig` if placed next to
site configuration file :file:`config.yaml` and is scoped to pages within that
site.


Impact
======

Sites and sets can ship Page TSconfig without the need for database entries or
by polluting global scope when registering Page TSconfig globally via
:file:`ext_localconf.php` or :file:`Configuration/page.tsconfig`.
Dependencies can expressed via sets, allowing for automatic ordering and
deduplication.


.. index:: Backend, Frontend, PHP-API, TypoScript, YAML, ext:core

0 comments on commit b90a463

Please sign in to comment.