Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jobs:
php-version:
- 8.1
- 8.2
- 8.3
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down Expand Up @@ -106,9 +107,11 @@ jobs:
matrix:
include:
- php-version: '8.1'
typo3-version: '^11.5'
typo3-version: '^12.4'
- php-version: '8.2'
typo3-version: '^11.5'
typo3-version: '^12.4'
- php-version: '8.3'
typo3-version: '^12.4'
steps:
- uses: actions/checkout@v3

Expand Down
1 change: 1 addition & 0 deletions Classes/Domain/Model/Marker.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the TYPO3 CMS project.
*
Expand Down
3 changes: 2 additions & 1 deletion Classes/Domain/Model/MarkerCollection.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the TYPO3 CMS project.
*
Expand Down Expand Up @@ -34,7 +35,7 @@ public function add(Marker $marker): void
public function get(string $markerKey): Marker
{
if (!isset($this[$markerKey])) {
throw new \RuntimeException('Marker not found');
throw new \RuntimeException('Marker not found', 6494338102);
}

return $this[$markerKey];
Expand Down
30 changes: 30 additions & 0 deletions Classes/EventListener/ModifyCacheLifetime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Sinso\Variables\EventListener;

use Sinso\Variables\Service\VariablesService;
use TYPO3\CMS\Frontend\Event\ModifyCacheLifetimeForPageEvent;

final class ModifyCacheLifetime
{
public function __construct(
private VariablesService $variablesService,
) {
}

/**
* Calculate shortest lifetime (aka duration) respecting data from
* markers
*/
public function __invoke(ModifyCacheLifetimeForPageEvent $event): void
{
$event->setCacheLifetime(
min(
$event->getCacheLifetime(),
$this->variablesService->getLifetime(),
)
);
}
}
12 changes: 5 additions & 7 deletions Classes/Hooks/ContentProcessor.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the TYPO3 CMS project.
*
Expand All @@ -17,7 +18,7 @@
use Sinso\Variables\Service\VariablesService;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Event\AfterCacheableContentIsGeneratedEvent;

class ContentProcessor
{
Expand All @@ -28,13 +29,10 @@ public function __construct()
$this->variablesService = GeneralUtility::makeInstance(VariablesService::class);
}

/**
* Dynamically replaces variables by user content.
*/
public function replaceContent(array &$parameters, TypoScriptFrontendController $parentObject): void
public function __invoke(AfterCacheableContentIsGeneratedEvent $event): void
{
$extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
$this->variablesService->initialize($extensionConfiguration, $parentObject);
$this->variablesService->replaceMarkersInStructureAndAdjustCaching($parentObject->content);
$this->variablesService->initialize($extensionConfiguration, $event->getController());
$this->variablesService->replaceMarkersInStructureAndAdjustCaching($event->getController()->content);
}
}
71 changes: 59 additions & 12 deletions Classes/Hooks/DataHandler.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the TYPO3 CMS project.
*
Expand All @@ -14,29 +15,75 @@

namespace Sinso\Variables\Hooks;

use Sinso\Variables\Domain\Model\Marker;
use Sinso\Variables\Utility\CacheKeyUtility;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;

class DataHandler
{
public function __construct(
private readonly ConnectionPool $connectionPool,
private readonly CacheManager $cacheManager,
) {
}

/**
* Flushes the cache if a marker record was edited.
*/
public function clearCachePostProc(array $params): void
public function clearCachePostProc(array $params, \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler): void
{
$marker = $this->getMarkerFromHook($params, $dataHandler);

if (!$marker instanceof \Sinso\Variables\Domain\Model\Marker) {
return;
}

$cacheTagToFlush = CacheKeyUtility::getCacheKey(
$marker->getMarkerWithBrackets()
);

$this->cacheManager->flushCachesInGroupByTag('pages', $cacheTagToFlush);
}

protected function getMarkerFromHook(array $params, \TYPO3\CMS\Core\DataHandling\DataHandler $dataHandler): ?Marker
{
if (
isset($params['table'], $params['uid']) && $params['table'] === 'tx_variables_marker'
($params['table'] !== 'tx_variables_marker')
|| !isset($params['uid'])
) {
$cacheTagsToFlush = [];
if (isset($params['marker'])) {
$cacheTagsToFlush[] = CacheKeyUtility::getCacheKey($params['marker']);
}

$cacheManager = GeneralUtility::makeInstance(CacheManager::class);
foreach ($cacheTagsToFlush as $cacheTag) {
$cacheManager->flushCachesInGroupByTag('pages', $cacheTag);
}
return null;
}

$marker = $dataHandler->datamap[$params['table']][$params['uid']]['marker'] ?? null;

if (!$marker) {
$marker = $this->findVariableMarkerByUidEventIfHiddenOrDeleted($params['uid']);
}

if (!$marker) {
return null;
}

return new Marker(
uid: $params['uid'],
key: $marker,
replacement: '', // value doesn't matter here
);
}

protected function findVariableMarkerByUidEventIfHiddenOrDeleted(int $uid): ?string
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('tx_variables_marker');
$queryBuilder->getRestrictions()->removeAll();
return $queryBuilder
->select('marker')
->from('tx_variables_marker')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
)
->executeQuery()
->fetchOne();
}
}
1 change: 1 addition & 0 deletions Classes/Hooks/MarkersProcessorInterface.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

/*
* This file is part of the TYPO3 CMS project.
*
Expand Down
62 changes: 35 additions & 27 deletions Classes/Service/VariablesService.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public function initialize(
ExtensionConfiguration $extensionConfiguration = null,
TypoScriptFrontendController $typoScriptFrontendController = null,
): void {
if (!$typoScriptFrontendController) {
if (!$typoScriptFrontendController instanceof \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController) {
$typoScriptFrontendController = $this->getTypoScriptFrontendController();
}

if (!$extensionConfiguration) {
if (!$extensionConfiguration instanceof \TYPO3\CMS\Core\Configuration\ExtensionConfiguration) {
$extensionConfiguration = GeneralUtility::makeInstance(ExtensionConfiguration::class);
}

Expand All @@ -54,19 +54,17 @@ public function initialize(
/**
* Iterates over a structure (array, object) and replaces markers in every string found.
*
* @param mixed $structure
*
* @return void
* @throws \Exception
*/
public function replaceMarkersInStructureAndAdjustCaching(
mixed &$structure
): void {
if ($this->markerCollection === null) {
if (!$this->markerCollection instanceof \Sinso\Variables\Domain\Model\MarkerCollection) {
throw new \Exception('Markers not initialized. Please run initialize() first.', 1726241619);
}
$this->replaceMarkersInStructure($structure);
$this->setCacheTagsAndLifetimeInTsfe();
$this->setCacheTagsInTsfe();
}

/**
Expand Down Expand Up @@ -111,12 +109,18 @@ protected function replaceMarkersInText(string &$text): void
}

// Assign a cache key associated with the marker
$this->cacheTags->add(CacheKeyUtility::getCacheKey($marker->getMarkerWithBrackets()));
$this->cacheTags->add(
CacheKeyUtility::getCacheKey(
$marker->getMarkerWithBrackets()
)
);
$this->usedMarkerKeys[] = $marker->key;
$text = $newContent;
}
}

$this->usedMarkerKeys = array_unique($this->usedMarkerKeys);

// Remove all markers (avoids empty entries)
if ($this->extensionConfiguration->get('variables', 'removeUnreplacedMarkers')) {
$text = preg_replace('/{{.*?}}/', '', $text);
Expand All @@ -132,8 +136,8 @@ protected function getMarkers(): MarkerCollection
return $page['uid'];
}, $this->typoScriptFrontendController->rootLine);

if (!empty($this->typoScriptFrontendController->tmpl->setup['plugin.']['tx_variables.']['persistence.']['storagePid'])) {
$pids[] = (int)$this->typoScriptFrontendController->tmpl->setup['plugin.']['tx_variables.']['persistence.']['storagePid'];
if (!empty($GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.typoscript')->getSetupArray()['plugin.']['tx_variables.']['persistence.']['storagePid'])) {
$pids[] = (int)$GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.typoscript')->getSetupArray()['plugin.']['tx_variables.']['persistence.']['storagePid'];
}

$table = 'tx_variables_marker';
Expand Down Expand Up @@ -169,36 +173,40 @@ protected function getMarkers(): MarkerCollection
return $markers;
}

protected function setCacheTagsAndLifetimeInTsfe(): void
protected function setCacheTagsInTsfe(): void
{
$this->usedMarkerKeys = array_unique($this->usedMarkerKeys);

$minLifetime = min(
$this->getSmallestLifetimeForMarkers($this->usedMarkerKeys),
$this->typoScriptFrontendController->page['cache_timeout'] ?: PHP_INT_MAX
);

$this->typoScriptFrontendController->page['cache_timeout'] = $minLifetime;

if (count($this->cacheTags) > 0) {
$this->typoScriptFrontendController->addCacheTags($this->cacheTags->toArray());
}
}

public function getSmallestLifetimeForMarkers(array $usedMarkerKeys): int
public function getLifetime(): int
{
return $this->getNearestTimestampForMarkers($this->usedMarkerKeys) - \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Context\Context::class)->getPropertyFromAspect('date', 'timestamp');
}

/**
* Get the nearest timestamp in the future when changes for Markers should happen.
* This respects starttime and endtime.
* The result will be used to calculate the maximal caching duration
*
* @throws \Doctrine\DBAL\Exception
*/
public function getNearestTimestampForMarkers(array $usedMarkerKeys): int
{
// Max value possible to keep an int \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->realPageCacheContent ($timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;)
$result = PHP_INT_MAX;

$tableName = 'tx_variables_marker';
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->createQueryBuilder();
$queryBuilder->getRestrictions()->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));

// Code heavily inspired by:
// \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->getFirstTimeValueForRecord
$now = (int)$GLOBALS['ACCESS_TIME'];
// Max value possible to keep an int \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->realPageCacheContent ($timeOutTime = $GLOBALS['EXEC_TIME'] + $cacheTimeout;)
$result = PHP_INT_MAX - $GLOBALS['EXEC_TIME'];
$now = (int)\TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Context\Context::class)->getPropertyFromAspect('date', 'timestamp');
$timeFields = [];
$timeConditions = $queryBuilder->expr()->orX();
$timeConditions = $queryBuilder->expr()->or();
foreach (['starttime', 'endtime'] as $field) {
if (isset($GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field])) {
$timeFields[$field] = $GLOBALS['TCA'][$tableName]['ctrl']['enablecolumns'][$field];
Expand All @@ -212,7 +220,7 @@ public function getSmallestLifetimeForMarkers(array $usedMarkerKeys): int
. ' THEN NULL ELSE ' . $queryBuilder->quoteIdentifier($timeFields[$field]) . ' END'
. ') AS ' . $queryBuilder->quoteIdentifier($timeFields[$field])
);
$timeConditions->add(
$timeConditions->with(
$queryBuilder->expr()->gt(
$timeFields[$field],
$queryBuilder->createNamedParameter($now, \PDO::PARAM_INT)
Expand All @@ -222,7 +230,7 @@ public function getSmallestLifetimeForMarkers(array $usedMarkerKeys): int
}

// if starttime or endtime are defined, evaluate them
if (!empty($timeFields)) {
if ($timeFields !== []) {
// find the timestamp, when the current page's content changes the next time
$queryBuilder
->from($tableName)
Expand All @@ -235,7 +243,7 @@ public function getSmallestLifetimeForMarkers(array $usedMarkerKeys): int
->fetch();

if ($row) {
foreach ($timeFields as $timeField => $_) {
foreach (array_keys($timeFields) as $timeField) {
// if a MIN value is found, take it into account for the
// cache lifetime we have to filter out start/endtimes < $now,
// as the SQL query also returns rows with starttime < $now
Expand Down
11 changes: 11 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,16 @@ services:
Sinso\Variables\:
resource: '../Classes/*'

Sinso\Variables\Hooks\DataHandler:
public: true

Sinso\Variables\Hooks\ContentProcessor:
tags:
- name: event.listener

Sinso\Variables\Service\VariablesService:
public: true

Sinso\Variables\EventListener\ModifyCacheLifetime:
tags:
- name: event.listener
3 changes: 3 additions & 0 deletions Configuration/TCA/Overrides/sys_template.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile('variables', 'Configuration/TypoScript', 'Content Variables');
3 changes: 3 additions & 0 deletions Configuration/TCA/Overrides/tx_variables_marker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

$GLOBALS['TCA']['tx_variables_marker']['ctrl']['security']['ignorePageTypeRestriction'] = true;
Loading