TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:33 +02:00
commit 1a6eae9988
124 changed files with 11393 additions and 0 deletions
@@ -0,0 +1,92 @@
<?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\Redirects\Hooks;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Redirects\Service\RedirectCacheService;
/**
* Ensure to clear the cache entry when a sys_redirect record is modified or deleted
* @internal This class is a specific TYPO3 hook implementation and is not part of the Public TYPO3 API.
*/
class DataHandlerCacheFlushingHook
{
/**
* Check if the data handler processed a sys_redirect record, if so, rebuild the redirect index cache
*
* @todo This hook is called for each record which needs to clear cache, which means this gets called
* for other records than sys_redirects, but also for each sys_redirect record which has been
* modified with this DataHandler call. Even if we can narrow down to rebuild only for specific
* source_hosts, this still means that we eventually rebuild the "same" cache multiple times.
* Find a better way to aggregate them and rebuild only once at the end.
*/
public function rebuildRedirectCacheIfNecessary(array $parameters, DataHandler $dataHandler): void
{
if (
($parameters['table'] ?? false) !== 'sys_redirect'
|| !($parameters['uid'] ?? false)
|| (
!isset($dataHandler->datamap['sys_redirect'])
&& !isset($dataHandler->cmdmap['sys_redirect'][(int)$parameters['uid']])
)
) {
return;
}
$redirectCacheService = GeneralUtility::makeInstance(RedirectCacheService::class);
$sourceHosts = [];
if (isset($dataHandler->getHistoryRecords()['sys_redirect:' . (int)$parameters['uid']]['oldRecord']['source_host'])) {
$sourceHosts[] = $dataHandler->getHistoryRecords()['sys_redirect:' . (int)$parameters['uid']]['oldRecord']['source_host'];
}
if (isset($dataHandler->getHistoryRecords()['sys_redirect:' . (int)$parameters['uid']]['newRecord']['source_host'])) {
$sourceHosts[] = $dataHandler->getHistoryRecords()['sys_redirect:' . (int)$parameters['uid']]['newRecord']['source_host'];
}
// only do record lookup for delete cmd, otherwise we cannot get old and new source_host,
// thus rebuildAll() should be executed as a safety net anyway.
if ($sourceHosts === [] && isset($dataHandler->cmdmap['sys_redirect'][(int)$parameters['uid']])) {
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_redirect');
$queryBuilder->getRestrictions()->removeAll();
$row = $queryBuilder
->select('source_host')
->from('sys_redirect')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($parameters['uid'], Connection::PARAM_INT))
)
->executeQuery()
->fetchAssociative();
if (isset($row['source_host'])) {
$sourceHosts[] = $row['source_host'] ?: '*';
}
}
// rebuild only specific source_host redirect caches
if ($sourceHosts !== []) {
foreach (array_unique($sourceHosts) as $sourceHost) {
$redirectCacheService->rebuildForHost($sourceHost);
}
return;
}
// Hopefully we get distinct source_host before. However, rebuild all redirect caches as a safety fallback.
$redirectCacheService->rebuildAll();
}
}
@@ -0,0 +1,77 @@
<?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\Redirects\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\SysLog\Action\Database as SystemLogDatabaseAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Redirects\Security\RedirectPermissionGuard;
/**
* @internal This class is a specific TYPO3 hook implementation and is not part of the Public TYPO3 API.
*/
#[Autoconfigure(public: true)]
final readonly class DataHandlerPermissionGuardHook
{
public function __construct(
private RedirectPermissionGuard $redirectPermissionGuard,
) {}
/**
* @param array<string, mixed>|null $incomingFieldArray
* @param-out array<string, mixed>|null $incomingFieldArray
*/
public function processDatamap_preProcessFieldArray(
?array &$incomingFieldArray,
string $table,
string|int $id,
DataHandler $dataHandler,
): void {
if ($table === 'sys_redirect' && !$this->redirectPermissionGuard->isAllowedRedirect($incomingFieldArray ?? [])) {
// Reset incoming field array to avoid further processing in DataHandler
// in case the given source host is not allowed for the current user
$incomingFieldArray = null;
if (MathUtility::canBeInterpretedAsInteger($id)) {
// Record update
$dataHandler->log(
'sys_redirect',
(int)$id,
SystemLogDatabaseAction::UPDATE,
null,
SystemLogErrorClassification::USER_ERROR,
'Attempt to modify sys_redirect record "%d" is disallowed',
null,
[$id],
);
} else {
// New record
$dataHandler->log(
'sys_redirect',
0,
SystemLogDatabaseAction::INSERT,
null,
SystemLogErrorClassification::USER_ERROR,
'Attempt to create a new sys_redirect record is disallowed',
);
}
}
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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\Redirects\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory;
use TYPO3\CMS\Redirects\Service\SlugService;
/**
* @internal This class is a specific TYPO3 hook implementation and is not part of the Public TYPO3 API.
*/
#[Autoconfigure(public: true)]
class DataHandlerSlugUpdateHook
{
/**
* Persisted slug values per record UID
* e.g. `[13 => SlugRedirectChangeItem( $original = ['slug' => 'slug-a'] ), 14 => SlugRedirectChangeItem( $original = ['slug' => 'slug-x/example'] )`
*
* @var array<int, SlugRedirectChangeItem>
*/
protected $persistedChangedItems;
public function __construct(
protected SlugService $slugService,
protected SlugRedirectChangeItemFactory $slugRedirectChangeItemFactory,
) {}
/**
* Sets the current user id for new records in the "createdby" field.
* Collects slugs of persisted records before having been updated.
*
* @param string|int $id (id could be string, for this reason no type hint)
*/
public function processDatamap_preProcessFieldArray(array &$incomingFieldArray, string $table, $id, DataHandler $dataHandler): void
{
if ($table === 'sys_redirect' && !MathUtility::canBeInterpretedAsInteger($id)) {
$incomingFieldArray['createdby'] = $dataHandler->BE_USER->user['uid'];
return;
}
if ($table !== 'pages'
|| empty($incomingFieldArray['slug'])
|| $this->isNestedHookInvocation($dataHandler)
|| !MathUtility::canBeInterpretedAsInteger($id)
|| !$dataHandler->hasPermissionToUpdate('pages', BackendUtility::getRecord('pages', (int)$id) ?? [])
) {
return;
}
$changeItem = $this->slugRedirectChangeItemFactory->create((int)$id);
if ($changeItem === null) {
return;
}
$this->persistedChangedItems[(int)$id] = $changeItem;
}
/**
* Acts on potential slug changes.
*
* Hook `processDatamap_afterDatabaseOperations` is a record has been persisted and after `DataHandler::fillInFields`
* which ensure access to `pages.slug` field and applies possible evaluations (`eval => 'trim,...`).
*/
public function processDatamap_afterDatabaseOperations(string $status, string $table, $id, array $fieldArray, DataHandler $dataHandler): void
{
$persistedChangedItem = $this->persistedChangedItems[(int)$id] ?? null;
if (
$persistedChangedItem === null
|| $table !== 'pages'
|| $status !== 'update'
|| empty($fieldArray['slug'])
|| $persistedChangedItem->getOriginal()['slug'] === $fieldArray['slug']
|| $this->isNestedHookInvocation($dataHandler)
) {
return;
}
// We merge the fieldArray dataset into with the original record to spare a database query here.
$persistedChangedItem = $persistedChangedItem->withChanged(array_merge($persistedChangedItem->getOriginal(), $fieldArray));
$this->slugService->rebuildSlugsForSlugChange($id, $persistedChangedItem, $dataHandler->getCorrelationId());
}
/**
* Determines whether our identifier is part of correlation id aspects.
* In that case it would be a nested call which has to be ignored.
*/
protected function isNestedHookInvocation(DataHandler $dataHandler): bool
{
$correlationId = $dataHandler->getCorrelationId();
$correlationIdAspects = $correlationId ? $correlationId->getAspects() : [];
return in_array(SlugService::CORRELATION_ID_IDENTIFIER, $correlationIdAspects, true);
}
}
@@ -0,0 +1,47 @@
<?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\Redirects\Hooks;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal
*/
final class DispatchNotificationHook
{
/**
* Called as a hook in \TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignalDetails
* calls a JS function to send the slug change notification
*
* @param array $params
*/
public function dispatchNotification(&$params)
{
$javaScriptRenderer = GeneralUtility::makeInstance(PageRenderer::class)->getJavaScriptRenderer();
$javaScriptRenderer->addJavaScriptModuleInstruction(
// @todo refactor to directly invoke the redirects slugChanged() method
// instead of dispatching an event that is only catched by the event dispatcher itself
JavaScriptModuleInstruction::create('@typo3/redirects/event-handler.js')
->addFlags(JavaScriptModuleInstruction::FLAG_USE_TOP_WINDOW)
->invoke('dispatchCustomEvent', 'typo3:redirects:slugChanged', $params['parameter'])
);
// not modifying `$params`, since instruction is added to global `PageRenderer`
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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\Redirects\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Initially set values for sys_redirects of type "qrcode"
*
* @internal
*/
#[Autoconfigure(public: true)]
final class HandleNewQrCodeRecord
{
public function processDatamap_preProcessFieldArray(&$incomingFieldArray, $table, $id, DataHandler $dataHandler): void
{
if ($table !== 'sys_redirect') {
return;
}
if (isset($incomingFieldArray['redirect_type'])
&& $incomingFieldArray['redirect_type'] === 'qrcode'
&& !isset($incomingFieldArray['source_path'])
&& !MathUtility::canBeInterpretedAsInteger($id)
) {
$incomingFieldArray['source_path'] = StringUtility::getUniqueId('/_redirect/');
$incomingFieldArray['keep_query_parameters'] = 1;
$incomingFieldArray['protected'] = 1;
$incomingFieldArray['is_regexp'] = 0;
$incomingFieldArray['disabled'] = 0;
}
}
}
+86
View File
@@ -0,0 +1,86 @@
<?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\Redirects\Hooks;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Redirects\Service\ShortUrlService;
/**
* Initially set values for sys_redirects of type "short_url"
*
* @internal
*/
#[Autoconfigure(public: true)]
final readonly class HandleNewShortUrlRecord
{
public function __construct(
private ShortUrlService $shortUrlService,
private FlashMessageService $flashMessageService,
) {}
public function processDatamap_preProcessFieldArray(
?array &$incomingFieldArray,
string $table,
int|string $id,
DataHandler $dataHandler
): void {
if ($table !== 'sys_redirect'
|| MathUtility::canBeInterpretedAsInteger($id)
|| ($incomingFieldArray['redirect_type'] ?? '') !== 'short_url'
) {
return;
}
// Set defaults when creating a new Short URL
$incomingFieldArray['keep_query_parameters'] = 1;
$incomingFieldArray['protected'] = 1;
$incomingFieldArray['is_regexp'] = 0;
$incomingFieldArray['disabled'] = 0;
// Prevent saving the record if source_path is empty
if (empty($incomingFieldArray['source_path'])) {
$incomingFieldArray = null;
return;
}
// Add '/' at the beginning of the source_path if not present
$incomingFieldArray['source_path'] = $incomingFieldArray['source_path'][0] === '/' ? $incomingFieldArray['source_path'] : '/' . $incomingFieldArray['source_path'];
// Check that the short URL does not already exist
if (!$this->shortUrlService->isUniqueShortUrl($incomingFieldArray['source_host'], $incomingFieldArray['source_path'])) {
$incomingFieldArray = null;
$message = $this->getLanguageService()->sL('redirects.modules.short_urls:validation.duplicate_short_url');
$flashMessage = new FlashMessage(
$message,
'',
ContextualFeedbackSeverity::ERROR,
true
);
$defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
$defaultFlashMessageQueue->enqueue($flashMessage);
}
}
private function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}