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
+200
View File
@@ -0,0 +1,200 @@
<?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\Service;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Redirects\Event\AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent;
use TYPO3\CMS\Redirects\Event\RedirectIntegrityCheckEvent;
use TYPO3\CMS\Redirects\Utility\RedirectConflict;
/**
* Checks for redirects that conflict with existing pages
*/
readonly class IntegrityService
{
public function __construct(
private RedirectService $redirectService,
private SiteFinder $siteFinder,
private ConnectionPool $connectionPool,
private EventDispatcherInterface $eventDispatcher,
private TcaSchemaFactory $tcaSchemaFactory,
) {}
/**
* Resolves all conflicting redirects
*/
public function findConflictingRedirects(?string $siteIdentifier = null): \Generator
{
foreach ($this->getSites($siteIdentifier) as $site) {
// Collect page urls for all pages and languages for $site.
$urls = $this->getAllPageUrlsForSite($site);
foreach ($urls as $url) {
$uri = new Uri($url);
$matchingRedirect = $this->getMatchingRedirectByUri($uri);
if ($matchingRedirect !== null) {
// @todo Returning information should be improved in future to give more useful information in
// command output and report output, for example redirect uid, page/language details, which would
// make the life easier for using the command and finding the conflicts.
yield [
'uri' => (string)$uri,
'redirect' => [
'integrity_status' => RedirectConflict::SELF_REFERENCE,
'source_host' => $matchingRedirect['source_host'],
'source_path' => $matchingRedirect['source_path'],
'uid' => $matchingRedirect['uid'],
],
];
}
}
}
}
/**
* Checks all redirects by dispatching a PSR-14 event for each record,
* allowing listeners to validate targets and flag broken redirects.
*/
public function checkRedirectIntegrity(): \Generator
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$queryBuilder->getRestrictions()->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$result = $queryBuilder
->select('*')
->from('sys_redirect')
->executeQuery();
while ($row = $result->fetchAssociative()) {
$event = $this->eventDispatcher->dispatch(
new RedirectIntegrityCheckEvent($row)
);
if ($event->getIntegrityStatus() !== null
&& $event->getIntegrityStatus() !== RedirectConflict::NO_CONFLICT
) {
yield [
'uri' => $row['target'] ?? '',
'redirect' => [
'integrity_status' => $event->getIntegrityStatus(),
'source_host' => $row['source_host'],
'source_path' => $row['source_path'],
'uid' => $row['uid'],
],
];
}
}
}
public function setIntegrityStatus(array $redirect): void
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$queryBuilder
->update('sys_redirect')
->where(
$queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($redirect['uid'], Connection::PARAM_INT))
)
->set('integrity_status', $redirect['integrity_status'])
->executeStatement();
}
private function getMatchingRedirectByUri(Uri $uri): ?array
{
$port = $uri->getPort();
$domain = $uri->getHost() . ($port ? ':' . $port : '');
return $this->redirectService->matchRedirect($domain, $uri->getPath());
}
/**
* @return Site[]
*/
private function getSites(?string $siteIdentifier): array
{
if ($siteIdentifier !== null) {
return [$this->siteFinder->getSiteByIdentifier($siteIdentifier)];
}
return $this->siteFinder->getAllSites();
}
/**
* Generates a list of all slugs used in a site
*/
private function getAllPageUrlsForSite(Site $site): array
{
$schema = $this->tcaSchemaFactory->get('pages');
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
$pageUrls = [];
// language bases - redirects would be nasty, but should be checked also. We do not need to add site base
// here, as there is always at least one default language.
foreach ($site->getLanguages() as $siteLanguage) {
$pageUrls[] = rtrim((string)$siteLanguage->getBase(), '/') . '/';
}
$queryBuilder = $this->connectionPool
->getQueryBuilderForTable('pages')
->select('slug', $languageCapability->getLanguageField()->getName())
->from('pages');
$queryBuilder->where(
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($site->getRootPageId(), Connection::PARAM_INT)
),
$queryBuilder->expr()->eq(
$languageCapability->getTranslationOriginPointerField()->getName(),
$queryBuilder->createNamedParameter($site->getRootPageId(), Connection::PARAM_INT)
),
)
);
$result = $queryBuilder->executeQuery();
while ($row = $result->fetchAssociative()) {
// @todo Considering only page slug is not complete, as it does not match redirects with file extension,
// for ex. if PageTypeSuffix routeEnhancer are used and redirects are created based on that.
$slug = ltrim(($row['slug'] ?? ''), '/');
$language = $row[$languageCapability->getLanguageField()->getName()];
try {
$siteLanguage = $site->getLanguageById($language);
} catch (\InvalidArgumentException) {
// skip invalid languages which might occur due to previous changes in site configuration
continue;
}
// empty slug root pages has been already handled with language bases above, thus skip them here.
if ($slug === '') {
continue;
}
$pageUrls[] = rtrim((string)$siteLanguage->getBase(), '/') . '/' . $slug;
}
$pageUrls = $this->eventDispatcher->dispatch(
new AfterPageUrlsForSiteForRedirectIntegrityHaveBeenCollectedEvent($site, $pageUrls)
)->getPageUrls();
return array_unique($pageUrls);
}
}
@@ -0,0 +1,57 @@
<?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\Service;
use TYPO3\CMS\Redirects\Repository\Demand;
use TYPO3\CMS\Redirects\Repository\RedirectRepository;
/**
* @internal
*/
final readonly class ModulePaginationService
{
public function __construct(
private RedirectRepository $redirectRepository
) {}
public function preparePagination(Demand $demand): array
{
$count = $this->redirectRepository->countRedirectsByDemand($demand);
$numberOfPages = ceil($count / $demand->getLimit());
$endRecord = $demand->getOffset() + $demand->getLimit();
if ($endRecord > $count) {
$endRecord = $count;
}
$pagination = [
'current' => $demand->getPage(),
'numberOfPages' => $numberOfPages,
'hasLessPages' => $demand->getPage() > 1,
'hasMorePages' => $demand->getPage() < $numberOfPages,
'startRecord' => $demand->getOffset() + 1,
'endRecord' => $endRecord,
];
if ($pagination['current'] < $pagination['numberOfPages']) {
$pagination['nextPage'] = $pagination['current'] + 1;
}
if ($pagination['current'] > 1) {
$pagination['previousPage'] = $pagination['current'] - 1;
}
return $pagination;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?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\Service;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Ensure to clear the cache entry when a sys_redirect record is modified, also the main pool
* for getting all redirects.
*
* @internal
*/
#[Autoconfigure(public: true)]
class RedirectCacheService
{
public function __construct(
#[Autowire(service: 'cache.pages')]
protected readonly FrontendInterface $cache,
protected readonly ConnectionPool $connectionPool,
) {}
/**
* Fetches all redirects available to the system, grouped by domain and regexp/nonregexp
*/
public function getRedirects(string $sourceHost): array
{
$redirects = $this->cache->get($this->buildCacheIdentifier($sourceHost));
// empty array is considered as valid cache, so we need to check for array type here.
if (!is_array($redirects)) {
$redirects = $this->rebuildForHost($sourceHost);
}
return $redirects;
}
/**
* Rebuilds the cache for all redirects, grouped by host as well as by regular expressions and respect_query_parameters.
* Does not include hidden or deleted redirects, but includes the ones with dynamic starttime/endtime.
*/
public function rebuildForHost(string $sourceHost): array
{
$redirects = [];
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
$queryBuilder->getRestrictions()->removeAll()
->add(GeneralUtility::makeInstance(HiddenRestriction::class))
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
$queryBuilder
->select('*')
->from('sys_redirect');
if ($sourceHost === '' || $sourceHost === '*') {
$queryBuilder->where(
$queryBuilder->expr()->or(
$queryBuilder->expr()->eq('source_host', $queryBuilder->createNamedParameter('')),
$queryBuilder->expr()->eq('source_host', $queryBuilder->createNamedParameter('*')),
)
);
} else {
$queryBuilder->where(
$queryBuilder->expr()->in('source_host', $queryBuilder->createNamedParameter($sourceHost))
);
}
// Ensure we have redirects which respect query parameters first, paired with a
// cross dbms deterministic sorting criteria (`uid`) as last criteria.
$queryBuilder
->orderBy('respect_query_parameters', 'desc')
->addOrderBy('uid', 'asc');
$statement = $queryBuilder->executeQuery();
while ($row = $statement->fetchAssociative()) {
// Field "description" is not needed for FE redirect handling. Don't add it to cache.
unset($row['description']);
if ($row['is_regexp'] && $row['respect_query_parameters']) {
$redirects['regexp_query_parameters'][$row['source_path']][$row['uid']] = $row;
} elseif ($row['is_regexp'] && !$row['respect_query_parameters']) {
$redirects['regexp_flat'][$row['source_path']][$row['uid']] = $row;
} elseif ($row['respect_query_parameters']) {
$redirects['respect_query_parameters'][$row['source_path']][$row['uid']] = $row;
} else {
$redirects['flat'][rtrim($row['source_path'], '/') . '/'][$row['uid']] = $row;
}
}
$this->cache->set($this->buildCacheIdentifier($sourceHost), $redirects);
return $redirects;
}
/**
* Rebuild cache for each distinct redirect source_host.
*/
public function rebuildAll(): void
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_redirect');
// remove all restriction, as we need to retrieve the source host even for hidden or deleted redirects.
$queryBuilder->getRestrictions()->removeAll();
$resultSet = $queryBuilder
->select('source_host')
->distinct()
->from('sys_redirect')
->executeQuery();
while ($row = $resultSet->fetchAssociative()) {
$this->rebuildForHost($row['source_host'] ?? '*');
}
}
private function buildCacheIdentifier(string $sourceHost): string
{
return 'redirects_' . sha1($sourceHost);
}
}
+511
View File
@@ -0,0 +1,511 @@
<?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\Service;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Site\Entity\NullSite;
use TYPO3\CMS\Core\Site\Entity\SiteInterface;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Frontend\Aspect\PreviewAspect;
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\Page\PageInformationFactory;
use TYPO3\CMS\Frontend\Typolink\TypolinkBuilderInterface;
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
use TYPO3\CMS\Redirects\Event\BeforeRedirectMatchDomainEvent;
/**
* Creates a proper URL to redirect from a matched redirect of a request
*
* @internal due to some possible refactorings
*/
readonly class RedirectService
{
public function __construct(
private RedirectCacheService $redirectCacheService,
private LinkService $linkService,
private SiteFinder $siteFinder,
private EventDispatcherInterface $eventDispatcher,
private PageInformationFactory $pageInformationFactory,
private FrontendTypoScriptFactory $frontendTypoScriptFactory,
#[Autowire(service: 'cache.typoscript')]
private PhpFrontend $typoScriptCache,
private LoggerInterface $logger,
private TypoLinkCodecService $typoLinkCodecService,
private Locales $locales,
) {}
/**
* Checks against all available redirects "flat" or "regexp", and against starttime/endtime
*/
public function matchRedirect(string $domain, string $path, string $query = ''): ?array
{
$path = rawurldecode($path);
// Check if the domain matches, or if there is a
// redirect fitting for any domain
foreach ([$domain, '*'] as $domainName) {
$matchedRedirect = $this->eventDispatcher->dispatch(
new BeforeRedirectMatchDomainEvent(
$domain,
$path,
$query,
$domainName,
)
)->getMatchedRedirect();
if ($matchedRedirect !== null && $matchedRedirect !== []) {
return $matchedRedirect;
}
$redirects = $this->fetchRedirects($domainName);
if (empty($redirects)) {
continue;
}
// check if a flat redirect matches with the Query applied
if (!empty($query)) {
$pathWithQuery = rtrim($path, '/') . '?' . ltrim($query, '?');
if (!empty($redirects['respect_query_parameters'][$pathWithQuery])) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['respect_query_parameters'][$pathWithQuery])) {
return $matchedRedirect;
}
} else {
$pathWithQueryAndSlash = rtrim($path, '/') . '/?' . ltrim($query, '?');
if (!empty($redirects['respect_query_parameters'][$pathWithQueryAndSlash])) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['respect_query_parameters'][$pathWithQueryAndSlash])) {
return $matchedRedirect;
}
}
}
}
// check if a flat redirect matches
if (!empty($redirects['flat'][rtrim($path, '/') . '/'])) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['flat'][rtrim($path, '/') . '/'])) {
return $matchedRedirect;
}
}
// @todo Evaluate if regexp patterns could be validated on creation/edit to give feedback on creation.
// check all regex redirects respecting query arguments
if (!empty($redirects['regexp_query_parameters'])) {
$allRegexps = array_keys($redirects['regexp_query_parameters']);
$regExpPath = $path;
if (!empty($query)) {
$regExpPath .= '?' . ltrim($query, '?');
}
foreach ($allRegexps as $regexp) {
$matchResult = @preg_match((string)$regexp, $regExpPath);
if ($matchResult > 0) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_query_parameters'][$regexp])) {
return $matchedRedirect;
}
continue;
}
// Log invalid regular expression
if ($matchResult === false) {
$this->logger->warning('Invalid regex in redirect', ['regex' => $regexp]);
}
}
}
// @todo Evaluate if regexp patterns could be validated on creation/edit to give feedback on creation.
// check all redirects that are registered as regex
if (!empty($redirects['regexp_flat'])) {
$allRegexps = array_keys($redirects['regexp_flat']);
$regExpPath = $path;
if (!empty($query)) {
$regExpPath .= '?' . ltrim($query, '?');
}
foreach ($allRegexps as $regexp) {
$matchResult = @preg_match((string)$regexp, $regExpPath);
if ($matchResult > 0) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_flat'][$regexp])) {
return $matchedRedirect;
}
continue;
}
// Log invalid regular expression
if ($matchResult === false) {
$this->logger->warning('Invalid regex in redirect', ['regex' => $regexp]);
}
}
// We need a second match run to evaluate against path only, even when query parameters where
// provided to ensure regexp without query parameters in mind are still processed.
// We need to do this only if there are query parameters in the request, otherwise first
// preg_match would have found it.
if (!empty($query)) {
foreach ($allRegexps as $regexp) {
$matchResult = @preg_match((string)$regexp, $path);
if ($matchResult > 0) {
if ($matchedRedirect = $this->getFirstActiveRedirectFromPossibleRedirects($redirects['regexp_flat'][$regexp])) {
return $matchedRedirect;
}
}
}
}
}
}
return null;
}
/**
* Check if a redirect record matches the starttime and endtime and disable restrictions
*
* @return bool whether the redirect is active and should be used for redirecting the current request
*/
protected function isRedirectActive(array $redirectRecord): bool
{
return !$redirectRecord['disabled'] && $redirectRecord['starttime'] <= $GLOBALS['SIM_ACCESS_TIME']
&& (!$redirectRecord['endtime'] || $redirectRecord['endtime'] >= $GLOBALS['SIM_ACCESS_TIME']);
}
/**
* Fetches all redirects from cache, with fallback to rebuild cache from the DB if caches was empty,
* grouped by the domain does NOT take starttime/endtime into account, as it is cached.
*/
protected function fetchRedirects(string $sourceHost): array
{
return $this->redirectCacheService->getRedirects($sourceHost);
}
/**
* Check if the current request is actually a redirect, and then process the redirect.
*
* @return array the link details from the linkService
*/
protected function resolveLinkDetailsFromLinkTarget(string $redirectTarget): array
{
try {
$linkDetails = $this->linkService->resolve($redirectTarget);
// Having the `typoLinkParameter` in the linkDetails is required, if the linkDetails are used to generate
// an url out of it. Therefore, this should be set in `getUriFromCustomLinkDetails()` before calling the
// LinkBuilder->build() method. We have a really tight execution context here, so we can safely set it here
// for now.
// @todo This simply reflects the used value to resolve the details. Other places in core set this to the
// array before building an url. This looks kind of unfinished. We should check, if we should not set
// that linkDetail value directly in the LinkService()->resolve() method generally.
$linkDetails['typoLinkParameter'] = $redirectTarget;
switch ($linkDetails['type']) {
case LinkService::TYPE_URL:
// all set up, nothing to do
break;
case LinkService::TYPE_FILE:
$file = $linkDetails['file'];
if ($file instanceof File) {
$linkDetails['url'] = $file->getPublicUrl();
}
break;
case LinkService::TYPE_FOLDER:
$folder = $linkDetails['folder'];
if ($folder instanceof Folder) {
$linkDetails['url'] = $folder->getPublicUrl();
}
break;
case LinkService::TYPE_UNKNOWN:
// If $redirectTarget could not be resolved, we can only assume $redirectTarget with leading '/'
// as relative redirect and try to resolve it with enriched information from current request.
// That ensures that regexp redirects ending in replaceRegExpCaptureGroup(), but also ensures
// that relative urls are not left as unknown file here.
if (str_starts_with($redirectTarget, '/')) {
$linkDetails = [
'type' => LinkService::TYPE_URL,
'url' => $redirectTarget,
];
}
break;
default:
// we have to return the link details without having a "URL" parameter
}
} catch (InvalidPathException $e) {
return [];
}
return $linkDetails;
}
public function getTargetUrl(array $matchedRedirect, ServerRequestInterface $request): ?UriInterface
{
$site = $request->getAttribute('site');
$uri = $request->getUri();
$queryParams = $request->getQueryParams();
$this->logger->debug('Found a redirect to process', ['redirect' => $matchedRedirect]);
$linkParameterParts = $this->typoLinkCodecService->decode((string)$matchedRedirect['target']);
$redirectTarget = $linkParameterParts['url'];
$linkDetails = $this->resolveLinkDetailsFromLinkTarget($redirectTarget);
$this->logger->debug('Resolved link details for redirect', ['details' => $linkDetails]);
if (!empty($linkParameterParts['additionalParams']) && $matchedRedirect['keep_query_parameters']) {
$params = GeneralUtility::explodeUrl2Array($linkParameterParts['additionalParams']);
foreach ($params as $key => $value) {
$queryParams[$key] = $value;
}
}
// Do this for files, folders, external URLs or relative urls
if (!empty($linkDetails['url'])) {
if ($matchedRedirect['is_regexp'] ?? false) {
$linkDetails = $this->replaceRegExpCaptureGroup($matchedRedirect, $uri, $linkDetails);
}
$url = new Uri($linkDetails['url']);
if ($matchedRedirect['force_https']) {
$url = $url->withScheme('https');
}
if ($matchedRedirect['keep_query_parameters']) {
$url = $this->addQueryParams($queryParams, $url);
}
if (!$url->getHost()) {
$url = $url->withHost($uri->getHost());
}
return $url;
}
$site = $this->resolveSite($linkDetails, $site);
// If it's a record or page, then boot up and use typolink
return $this->getUriFromCustomLinkDetails(
$matchedRedirect,
$site,
$linkDetails,
$queryParams,
$request
);
}
/**
* If no site is given, try to find a valid site for the target page
*/
protected function resolveSite(array $linkDetails, ?SiteInterface $site): ?SiteInterface
{
if (($site === null || $site instanceof NullSite) && ($linkDetails['type'] ?? '') === LinkService::TYPE_PAGE) {
try {
return $this->siteFinder->getSiteByPageId((int)$linkDetails['pageuid']);
} catch (SiteNotFoundException $e) {
return new NullSite();
}
}
return $site;
}
/**
* Adds query parameters to a Uri object
*/
protected function addQueryParams(array $queryParams, Uri $url): Uri
{
// New query parameters overrule the ones that should be kept
$newQueryParamString = $url->getQuery();
if (!empty($newQueryParamString)) {
$newQueryParams = [];
parse_str($newQueryParamString, $newQueryParams);
$queryParams = array_replace_recursive($queryParams, $newQueryParams);
}
$query = http_build_query($queryParams, '', '&', PHP_QUERY_RFC3986);
if ($query) {
$url = $url->withQuery($query);
}
return $url;
}
/**
* Called when TypoScriptis available, so typolink is used to generate the URL
*/
protected function getUriFromCustomLinkDetails(array $redirectRecord, ?SiteInterface $site, array $linkDetails, array $queryParams, ServerRequestInterface $originalRequest): ?UriInterface
{
if (!isset($linkDetails['type'], $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
return null;
}
if ($site === null || $site instanceof NullSite) {
return null;
}
$builderType = $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']];
$contentObjectRenderer = $this->bootFrontendController($site, $queryParams, $originalRequest);
/** @var TypolinkBuilderInterface $linkBuilder */
$linkBuilder = GeneralUtility::makeInstance($builderType);
if (! $linkBuilder instanceof TypolinkBuilderInterface) {
throw new \RuntimeException('Single link builder must implement TypolinkBuilderInterface', 1780062714);
}
$configuration = [
'parameter' => (string)$redirectRecord['target'],
'forceAbsoluteUrl' => true,
'linkAccessRestrictedPages' => true,
];
if ($redirectRecord['force_https']) {
$configuration['forceAbsoluteUrl.']['scheme'] = 'https';
}
if ($redirectRecord['keep_query_parameters']) {
$configuration['additionalParams'] = HttpUtility::buildQueryString($queryParams, '&');
}
$request = $originalRequest->withAttribute('currentContentObject', $contentObjectRenderer);
try {
$result = $linkBuilder->buildLink($linkDetails, $configuration, $request);
$this->cleanupContext();
return new Uri($result->getUrl());
} catch (UnableToLinkException $e) {
$this->cleanupContext();
return null;
}
}
/**
* Finishing booting up, after that the following properties are available.
*
* Instantiating is done by the middleware stack (see Configuration/RequestMiddlewares.php)
* so a link to a page can be generated.
*
* @todo: This messes quite a bit with dependencies here. RedirectService is called by an early middleware
* *before* state has been set up at all. The code thus has to hop through various loops later middlewares
* would usually do.
*/
protected function bootFrontendController(SiteInterface $site, array $queryParams, ServerRequestInterface $originalRequest): ContentObjectRenderer
{
$context = GeneralUtility::makeInstance(Context::class);
$context->setAspect('frontend.preview', new PreviewAspect());
$cacheInstruction = $originalRequest->getAttribute('frontend.cache.instruction', new CacheInstruction());
$originalRequest = $originalRequest->withAttribute('frontend.cache.instruction', $cacheInstruction);
$queryParamsFromRequest = $originalRequest->getQueryParams();
$mergedQueryParams = array_merge($queryParams, $queryParamsFromRequest);
$originalRequest = $originalRequest->withQueryParams($mergedQueryParams);
$pageArguments = new PageArguments($site->getRootPageId(), '0', []);
$originalRequest = $originalRequest->withAttribute('routing', $pageArguments);
$pageInformation = $this->pageInformationFactory->create($originalRequest);
$originalRequest = $originalRequest->withAttribute('frontend.page.information', $pageInformation);
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$language = $originalRequest->getAttribute('language') ?? $originalRequest->getAttribute('site')->getDefaultLanguage();
if ($language->hasCustomTypo3Language()) {
$locale = $this->locales->createLocale($language->getTypo3Language());
} else {
$locale = $language->getLocale();
}
$pageRenderer->setLanguage($locale, $originalRequest);
$expressionMatcherVariables = $this->getExpressionMatcherVariables($site, $originalRequest);
$frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions(
$site,
$pageInformation->getSysTemplateRows(),
// $originalRequest does not contain site ...
$expressionMatcherVariables,
$this->typoScriptCache,
);
// Note, that we need the full TypoScript setup array, which is required for links created by
// DatabaseRecordLinkBuilder.
$frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(
true,
$frontendTypoScript,
$site,
$pageInformation->getSysTemplateRows(),
$expressionMatcherVariables,
'0',
$this->typoScriptCache,
null
);
$newRequest = $originalRequest->withAttribute('frontend.typoscript', $frontendTypoScript);
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObjectRenderer->setRequest($newRequest);
$contentObjectRenderer->start($newRequest->getAttribute('frontend.page.information')->getPageRecord(), 'pages');
return $contentObjectRenderer;
}
private function getExpressionMatcherVariables(SiteInterface $site, ServerRequestInterface $request): array
{
$pageInformation = $request->getAttribute('frontend.page.information');
$topDownRootLine = $pageInformation->getRootLine();
$localRootline = $pageInformation->getLocalRootLine();
ksort($topDownRootLine);
return [
'request' => $request,
'pageId' => $pageInformation->getId(),
'page' => $pageInformation->getPageRecord(),
'fullRootLine' => $topDownRootLine,
'localRootLine' => $localRootline,
'site' => $site,
'siteLanguage' => $request->getAttribute('language'),
];
}
protected function replaceRegExpCaptureGroup(array $matchedRedirect, UriInterface $uri, array $linkDetails): array
{
$uriToCheck = rawurldecode($uri->getPath());
if (($matchedRedirect['respect_query_parameters'] ?? false) && $uri->getQuery()) {
$uriToCheck .= '?' . rawurldecode($uri->getQuery());
}
$matchResult = @preg_match($matchedRedirect['source_path'], $uriToCheck, $matches);
if ($matchResult > 0) {
foreach ($matches as $key => $val) {
// Unsafe regexp captching group may lead to adding query parameters to result url, which we need
// to prevent here, thus throwing everything beginning with ? away
if (str_contains($val, '?')) {
$val = explode('?', $val, 2)[0];
$this->logger->warning(
sprintf(
'Unsafe captching group regex in redirect #%s, including query parameters in matched group',
$matchedRedirect['uid'] ?? 0
),
['regex' => $matchedRedirect['source_path']]
);
}
$linkDetails['url'] = str_replace('$' . $key, $val, $linkDetails['url']);
}
}
return $linkDetails;
}
/**
* Checks all possible redirects and return the first possible and active redirect if available.
*/
protected function getFirstActiveRedirectFromPossibleRedirects(array $possibleRedirects): ?array
{
foreach ($possibleRedirects as $possibleRedirect) {
if ($this->isRedirectActive($possibleRedirect)) {
return $possibleRedirect;
}
}
return null;
}
/**
* @todo: Needs to vanish. The existence of this method is a side-effect of the technical debt that
* a context has to be set up for link generation, see the comment on bootFrontendController()
* for more details.
*/
private function cleanupContext(): void
{
$context = GeneralUtility::makeInstance(Context::class);
$context->unsetAspect('language');
$context->unsetAspect('typoscript');
$context->unsetAspect('frontend.preview');
}
}
+75
View File
@@ -0,0 +1,75 @@
<?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\Service;
use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal Only to be used within TYPO3. Might change in the future.
*/
readonly class ShortUrlService
{
private const string TABLE = 'sys_redirect';
private const string CHARACTER_SET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
private const int PATH_LENGTH = 8;
private const int MAX_RETRIES = 10;
public function __construct(
private ConnectionPool $connectionPool,
private Random $random,
) {}
public function generateUniqueShortUrlPath(string $sourceHost): ?string
{
$charSetLength = mb_strlen(self::CHARACTER_SET);
for ($attempt = 0; $attempt < self::MAX_RETRIES; $attempt++) {
$path = '/';
for ($i = 0; $i < self::PATH_LENGTH; $i++) {
$path .= self::CHARACTER_SET[$this->random->generateRandomInteger(0, $charSetLength - 1)];
}
if ($this->isUniqueShortUrl($sourceHost, $path)) {
return $path;
}
}
return null;
}
public function isUniqueShortUrl(string $sourceHost, string $sourcePath): bool
{
$queryBuilder = $this->connectionPool->getQueryBuilderForTable(self::TABLE);
$queryBuilder->getRestrictions()->removeAll()->add(
GeneralUtility::makeInstance(DeletedRestriction::class)
);
$count = $queryBuilder
->count('uid')
->from(self::TABLE)
->where(
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('source_host', $queryBuilder->createNamedParameter($sourceHost)),
$queryBuilder->expr()->eq('source_path', $queryBuilder->createNamedParameter($sourcePath))
)
)
->executeQuery()
->fetchOne();
return $count === 0;
}
}
+409
View File
@@ -0,0 +1,409 @@
<?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\Service;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\Model\CorrelationId;
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
use TYPO3\CMS\Core\DataHandling\SlugHelper;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Redirects\Event\AfterAutoCreateRedirectHasBeenPersistedEvent;
use TYPO3\CMS\Redirects\Event\ModifyAutoCreateRedirectRecordBeforePersistingEvent;
use TYPO3\CMS\Redirects\Hooks\DataHandlerSlugUpdateHook;
use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItem;
use TYPO3\CMS\Redirects\RedirectUpdate\SlugRedirectChangeItemFactory;
/**
* @internal Due to some possible refactorings in TYPO3 v10+
*/
class SlugService
{
/**
* `dechex(1569615472)` (similar to timestamps used with exceptions, but in hex)
*/
final public const string CORRELATION_ID_IDENTIFIER = '5d8e6e70';
protected ?CorrelationId $correlationIdRedirectCreation = null;
protected ?CorrelationId $correlationIdSlugUpdate = null;
protected ?CorrelationId $correlationIdPageUpdate = null;
protected bool $autoUpdateSlugs = false;
protected bool $autoCreateRedirects = false;
protected int $redirectTTL = 0;
protected int $httpStatusCode = 307;
public function __construct(
private readonly Context $context,
private readonly PageRepository $pageRepository,
private readonly LinkService $linkService,
private readonly RedirectCacheService $redirectCacheService,
private readonly SlugRedirectChangeItemFactory $slugRedirectChangeItemFactory,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ConnectionPool $connectionPool,
private readonly TcaSchemaFactory $tcaSchemaFactory,
private readonly TemporaryPermissionMutationService $temporaryPermissionMutationService,
private readonly LoggerInterface $logger,
) {}
public function rebuildSlugsForSlugChange(int $pageId, SlugRedirectChangeItem $changeItem, CorrelationId $correlationId): void
{
$this->initializeSettings($changeItem->getSite());
if ($this->autoUpdateSlugs || $this->autoCreateRedirects) {
$sourceHosts = [];
$this->createCorrelationIds($pageId, $correlationId);
if ($this->autoCreateRedirects) {
$sourceHosts = $this->createRedirects(
$changeItem,
$changeItem->getDefaultLanguagePageId(),
(int)$changeItem->getChanged()['language_tag']
);
}
if ($this->autoUpdateSlugs) {
$sourceHosts += $this->checkSubPages($changeItem->getChanged(), $changeItem);
}
$this->sendNotification();
// rebuild caches only for matched source hosts
if ($sourceHosts !== []) {
foreach (array_unique($sourceHosts) as $sourceHost) {
$this->redirectCacheService->rebuildForHost($sourceHost);
}
}
}
}
protected function initializeSettings(Site $site): void
{
$settings = $site->getSettings();
$this->autoUpdateSlugs = (bool)$settings->get('redirects.autoUpdateSlugs', true);
$this->autoCreateRedirects = (bool)$settings->get('redirects.autoCreateRedirects', true);
if (!$this->context->getPropertyFromAspect('workspace', 'isLive')) {
$this->autoCreateRedirects = false;
}
$this->redirectTTL = (int)$settings->get('redirects.redirectTTL', 0);
$this->httpStatusCode = (int)$settings->get('redirects.httpStatusCode', 307);
}
protected function createCorrelationIds(int $pageId, CorrelationId $correlationId): void
{
if ($correlationId->getSubject() === null) {
$subject = md5('pages:' . $pageId);
$correlationId = $correlationId->withSubject($subject);
}
$this->correlationIdPageUpdate = $correlationId;
$this->correlationIdRedirectCreation = $correlationId->withAspects(self::CORRELATION_ID_IDENTIFIER, 'redirect');
$this->correlationIdSlugUpdate = $correlationId->withAspects(self::CORRELATION_ID_IDENTIFIER, 'slug');
}
/**
* @return string[] All unique source hosts for created redirects.
*/
protected function createRedirects(SlugRedirectChangeItem $changeItem, int $pageId, int $languageId): array
{
$sourceHosts = [];
$storagePid = $changeItem->getSite()->getRootPageId();
foreach ($changeItem->getSourcesCollection()->all() as $source) {
/** @var DateTimeAspect $date */
$date = $this->context->getAspect('date');
$endtime = $date->getDateTime()->modify('+' . $this->redirectTTL . ' days');
$targetLinkParameters = array_replace(['_language' => $languageId], $source->getTargetLinkParameters());
$targetLink = $this->linkService->asString([
'type' => 'page',
'pageuid' => $pageId,
'parameters' => HttpUtility::buildQueryString($targetLinkParameters),
]);
$record = array_replace(
$this->getTableDefaultValues('sys_redirect'),
[
'pid' => $storagePid,
'createdby' => $this->context->getPropertyFromAspect('backend.user', 'id', 0),
'endtime' => $this->redirectTTL > 0 ? $endtime->getTimestamp() : 0,
'source_host' => $source->getHost(),
'source_path' => $source->getPath(),
'target' => $targetLink,
'target_statuscode' => $this->httpStatusCode,
'creation_type' => 0,
]
);
$record = $this->eventDispatcher->dispatch(
new ModifyAutoCreateRedirectRecordBeforePersistingEvent(
slugRedirectChangeItem: $changeItem,
source: $source,
redirectRecord: $record,
)
)->getRedirectRecord();
// Temporary add permissions to the user to perform the action.
// Store if we need to revert those changes after the actions.
$addedTableModify = $this->temporaryPermissionMutationService->addTableModify();
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$redirectNewId = StringUtility::getUniqueId('NEW');
$data = [
'sys_redirect' => [
$redirectNewId => $record,
],
];
$dataHandler->start($data, [], null, null, $this->correlationIdRedirectCreation);
$dataHandler->process_datamap();
if ($addedTableModify) {
// Revert temporary permissions
$this->temporaryPermissionMutationService->removeTableModify();
}
$record['uid'] = $dataHandler->substNEWwithIDs[$redirectNewId] ?? null;
if ($dataHandler->errorLog !== [] || $record['uid'] === null) {
$this->logger->error(
'Could not create redirect record for source "{host}{path}"',
[
'host' => $source->getHost(),
'path' => $source->getPath(),
'persistedUid' => $record['uid'],
'errorLog' => $dataHandler->errorLog,
]
);
continue;
}
$this->eventDispatcher->dispatch(
new AfterAutoCreateRedirectHasBeenPersistedEvent(
slugRedirectChangeItem: $changeItem,
source: $source,
redirectRecord: $record,
)
);
if (!in_array($source->getHost(), $sourceHosts)) {
$sourceHosts[] = $source->getHost();
}
}
return $sourceHosts;
}
/**
* @return string[] All unique source hosts for created redirects.
*/
protected function checkSubPages(array $currentPageRecord, SlugRedirectChangeItem $parentChangeItem): array
{
$sourceHosts = [];
$languageUid = (int)$currentPageRecord['language_tag'];
// resolveSubPages needs the page id of the default language
$pageId = $languageUid === 0 ? (int)$currentPageRecord['uid'] : (int)$currentPageRecord['l10n_parent'];
$subPageRecords = $this->resolveSubPages($pageId, $languageUid);
foreach ($subPageRecords as $subPageRecord) {
$changeItem = $this->slugRedirectChangeItemFactory->create(
(int)$subPageRecord['uid'],
$subPageRecord
);
if ($changeItem === null) {
continue;
}
$updatedPageRecord = $this->updateSlug($subPageRecord, $parentChangeItem);
if ($updatedPageRecord !== null && $this->autoCreateRedirects) {
$subPageId = (int)$subPageRecord['language_tag'] === 0 ? (int)$subPageRecord['uid'] : (int)$subPageRecord['l10n_parent'];
$changeItem = $changeItem->withChanged($updatedPageRecord);
$sourceHosts += array_values($this->createRedirects($changeItem, $subPageId, $languageUid));
}
}
return $sourceHosts;
}
protected function resolveSubPages(int $id, int $languageUid): array
{
// First resolve all sub-pages in default language
$queryBuilder = $this->getQueryBuilderForPages();
$subPages = $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($id, Connection::PARAM_INT)),
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
// if the language is not the default language, resolve the language related records.
if ($languageUid > 0) {
$queryBuilder = $this->getQueryBuilderForPages();
$subPages = $queryBuilder
->select('*')
->from('pages')
->where(
$queryBuilder->expr()->in('l10n_parent', $queryBuilder->createNamedParameter(array_column($subPages, 'uid'), Connection::PARAM_INT_ARRAY)),
$queryBuilder->expr()->eq('language_tag', $queryBuilder->createNamedParameter($languageUid, Connection::PARAM_INT))
)
->orderBy('uid', 'ASC')
->executeQuery()
->fetchAllAssociative();
}
$results = [];
if (!empty($subPages)) {
$subPages = $this->pageRepository->getPagesOverlay($subPages, $languageUid);
foreach ($subPages as $subPage) {
$results[] = $subPage;
// resolveSubPages needs the page id of the default language
$pageId = $languageUid === 0 ? (int)$subPage['uid'] : (int)$subPage['l10n_parent'];
foreach ($this->resolveSubPages($pageId, $languageUid) as $page) {
$results[] = $page;
}
}
}
return $results;
}
/**
* Update a slug by given record, old parent page slug and new parent page slug.
* In case no update is required, the method returns null else the new slug.
*/
protected function updateSlug(array $subPageRecord, SlugRedirectChangeItem $changeItem): ?array
{
if ($changeItem->getChanged() === null
|| !str_starts_with($subPageRecord['slug'], $changeItem->getOriginal()['slug'])
) {
return null;
}
$oldSlugOfParentPage = $changeItem->getOriginal()['slug'];
$newSlugOfParentPage = $changeItem->getChanged()['slug'];
$newSlug = rtrim($newSlugOfParentPage, '/') . '/'
. substr($subPageRecord['slug'], strlen(rtrim($oldSlugOfParentPage, '/') . '/'));
$state = RecordStateFactory::forName('pages')
->fromArray($subPageRecord, $subPageRecord['pid'], $subPageRecord['uid']);
$schema = $this->tcaSchemaFactory->get('pages');
$slugHelper = GeneralUtility::makeInstance(SlugHelper::class, 'pages', 'slug', $schema->getField('slug')->getConfiguration());
if (!$slugHelper->isUniqueInSite($newSlug, $state)) {
$newSlug = $slugHelper->buildSlugForUniqueInSite($newSlug, $state);
}
$this->persistNewSlug((int)$subPageRecord['uid'], $newSlug);
return BackendUtility::getRecord('pages', (int)$subPageRecord['uid']);
}
protected function persistNewSlug(int $uid, string $newSlug): void
{
$this->disableHook();
$data = [];
$data['pages'][$uid]['slug'] = $newSlug;
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
$dataHandler->start($data, [], null, null, $this->correlationIdSlugUpdate);
$dataHandler->process_datamap();
$this->enabledHook();
}
protected function sendNotification(): void
{
$data = [
'componentName' => 'redirects',
'eventName' => 'slugChanged',
'correlations' => [
'correlationIdPageUpdate' => (string)$this->correlationIdPageUpdate,
'correlationIdSlugUpdate' => (string)$this->correlationIdSlugUpdate,
'correlationIdRedirectCreation' => (string)$this->correlationIdRedirectCreation,
],
'autoUpdateSlugs' => (bool)$this->autoUpdateSlugs,
'autoCreateRedirects' => (bool)$this->autoCreateRedirects,
];
BackendUtility::setUpdateSignal('redirects:slugChanged', $data);
}
protected function getQueryBuilderForPages(): QueryBuilder
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->context->getPropertyFromAspect('workspace', 'id')));
return $queryBuilder;
}
protected function enabledHook(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects']
= DataHandlerSlugUpdateHook::class;
}
protected function disableHook(): void
{
unset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['redirects']);
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
/**
* Gather table default values from TCA and from the cached table schema information as fallback.
*
* @param string $tableName
* @return array<non-empty-string, string|float|int|bool|null>
* @todo Consider to provide this in Connection if use-full for different places.
*/
private function getTableDefaultValues(string $tableName): array
{
$defaults = [];
if ($this->tcaSchemaFactory->has($tableName)) {
$tcaSchema = $this->tcaSchemaFactory->get($tableName);
foreach ($tcaSchema->getFields() as $columnName => $column) {
if ($column->hasDefaultValue()) {
$defaults[$columnName] = $column->getDefaultValue();
}
}
}
$connection = $this->connectionPool->getConnectionForTable($tableName);
$tableColumnInfos = $connection->getSchemaInformation()->listTableColumnInfos($tableName);
foreach ($tableColumnInfos as $columnName => $columnInfo) {
if ($columnName === 'uid' || $columnInfo->autoincrement === true) {
// Autoincrement fields and therefore the default TYPO3 `uid` column
// should be not provided in a data array to ensure the behaviour
// kicks correctly in.
continue;
}
if (array_key_exists($columnName, $defaults)) {
// Already having TCA default value, which weights higher.
continue;
}
$columnDefaultValue = $columnInfo->default;
if ($columnDefaultValue === null && $columnInfo->notNull === false) {
// No need to set null as default value for a nullable column.
continue;
}
$defaults[$columnName] = $columnDefaultValue;
}
return $defaults;
}
}
@@ -0,0 +1,80 @@
<?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\Service;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* @internal This class is a workaround to temporarily mutate user permissions to create and delete redirects,
* even if the current user has no access to the table.
*/
final class TemporaryPermissionMutationService
{
public function addTableSelect(): bool
{
if (!$this->containsSysRedirectPermission('tables_select')) {
$GLOBALS['BE_USER']->groupData['tables_select'] = $this->addSysRedirectPermission('tables_select');
return true;
}
return false;
}
public function addTableModify(): bool
{
if (!$this->containsSysRedirectPermission('tables_modify')) {
$GLOBALS['BE_USER']->groupData['tables_modify'] = $this->addSysRedirectPermission('tables_modify');
return true;
}
return false;
}
public function removeTableSelect(): void
{
if ($this->containsSysRedirectPermission('tables_select')) {
$GLOBALS['BE_USER']->groupData['tables_select'] = $this->removeSysRedirectPermission('tables_select');
}
}
public function removeTableModify(): void
{
if ($this->containsSysRedirectPermission('tables_modify')) {
$GLOBALS['BE_USER']->groupData['tables_modify'] = $this->removeSysRedirectPermission('tables_modify');
}
}
private function addSysRedirectPermission(string $groupData): string
{
$permissions = GeneralUtility::trimExplode(',', $GLOBALS['BE_USER']->groupData[$groupData], true);
$permissions[] = 'sys_redirect';
return implode(',', array_unique($permissions));
}
private function removeSysRedirectPermission(string $groupData): string
{
$permissions = GeneralUtility::trimExplode(',', $GLOBALS['BE_USER']->groupData[$groupData], true);
$permissions = array_diff($permissions, ['sys_redirect']);
return implode(',', array_unique($permissions));
}
private function containsSysRedirectPermission(string $groupData): bool
{
return GeneralUtility::inList($GLOBALS['BE_USER']->groupData[$groupData], 'sys_redirect');
}
}