TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -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',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user