TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,84 @@
<?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\Backend\Routing\Event;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Context\Context;
/**
* Listeners to this event will be able to modify the page preview
* URI, which had been generated for a page in the frontend.
*/
final class AfterPagePreviewUriGeneratedEvent
{
public function __construct(
private UriInterface $previewUri,
private readonly int $pageId,
private readonly int $languageId,
private readonly array $rootline,
private readonly string $section,
private readonly array $additionalQueryParameters,
private readonly Context $context,
private readonly array $options
) {}
public function setPreviewUri(UriInterface $previewUri): void
{
$this->previewUri = $previewUri;
}
public function getPreviewUri(): UriInterface
{
return $this->previewUri;
}
public function getPageId(): int
{
return $this->pageId;
}
public function getLanguageId(): int
{
return $this->languageId;
}
public function getRootline(): array
{
return $this->rootline;
}
public function getSection(): string
{
return $this->section;
}
public function getAdditionalQueryParameters(): array
{
return $this->additionalQueryParameters;
}
public function getContext(): Context
{
return $this->context;
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,116 @@
<?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\Backend\Routing\Event;
use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Core\Context\Context;
/**
* Listeners to this event will be able to modify the corresponding parameters, before
* the page preview URI is being generated, when linking to a page in the frontend.
*/
final class BeforePagePreviewUriGeneratedEvent implements StoppableEventInterface
{
private ?UriInterface $uri = null;
public function __construct(
private int $pageId,
private int $languageId,
private array $rootline,
private string $section,
private array $additionalQueryParameters,
private readonly Context $context,
private readonly array $options
) {}
public function setPreviewUri(UriInterface $uri): void
{
$this->uri = $uri;
}
public function getPreviewUri(): ?UriInterface
{
return $this->uri;
}
public function isPropagationStopped(): bool
{
return $this->uri !== null;
}
public function getPageId(): int
{
return $this->pageId;
}
public function setPageId(int $pageId): void
{
$this->pageId = $pageId;
}
public function getLanguageId(): int
{
return $this->languageId;
}
public function setLanguageId(int $languageId): void
{
$this->languageId = $languageId;
}
public function getRootline(): array
{
return $this->rootline;
}
public function setRootline(array $rootline): void
{
$this->rootline = $rootline;
}
public function getSection(): string
{
return $this->section;
}
public function setSection(string $section): void
{
$this->section = $section;
}
public function getAdditionalQueryParameters(): array
{
return $this->additionalQueryParameters;
}
public function setAdditionalQueryParameters(array $additionalQueryParameters): void
{
$this->additionalQueryParameters = $additionalQueryParameters;
}
public function getContext(): Context
{
return $this->context;
}
public function getOptions(): array
{
return $this->options;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Backend\Routing\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Exception thrown when request token was invalid
*/
class InvalidRequestTokenException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Backend\Routing\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Exception thrown when request token was missing.
*/
class MissingRequestTokenException extends Exception {}
@@ -0,0 +1,23 @@
<?php
/*
* 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\Backend\Routing\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Exception thrown when a resource was not found.
*/
class ResourceNotFoundException extends Exception {}
@@ -0,0 +1,21 @@
<?php
/*
* 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\Backend\Routing\Exception;
/**
* Exception thrown when a route does not exist
*/
class RouteNotFoundException extends \TYPO3\CMS\Core\Routing\RouteNotFoundException {}
@@ -0,0 +1,25 @@
<?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\Backend\Routing\Exception;
use TYPO3\CMS\Core\Exception;
/**
* Exception thrown when a resource was found but the type (e.g. AJAX routes) did not match.
*/
class RouteTypeNotAllowedException extends Exception {}
+654
View File
@@ -0,0 +1,654 @@
<?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\Backend\Routing;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\UriInterface;
use TYPO3\CMS\Backend\Routing\Event\AfterPagePreviewUriGeneratedEvent;
use TYPO3\CMS\Backend\Routing\Event\BeforePagePreviewUriGeneratedEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\DateTimeAspect;
use TYPO3\CMS\Core\Context\VisibilityAspect;
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\DataHandling\PageDoktypeRegistry;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Domain\RecordInterface;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException;
use TYPO3\CMS\Core\Routing\RouterInterface;
use TYPO3\CMS\Core\Routing\UnableToLinkToPageException;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\HttpUtility;
use TYPO3\CMS\Core\Versioning\VersionState;
/**
* Generate links to Frontend URLs with a modified scope.
*/
class PreviewUriBuilder
{
public const OPTION_SWITCH_FOCUS = 'switchFocus';
public const OPTION_WINDOW_NAME = 'windowName';
public const OPTION_WINDOW_FEATURES = 'windowFeatures';
public const OPTION_WINDOW_SCOPE = 'windowScope';
public const OPTION_WINDOW_SCOPE_LOCAL = 'local';
public const OPTION_WINDOW_SCOPE_GLOBAL = 'global';
protected array $record = [];
protected string $table = 'pages';
protected int $pageId;
protected int $languageId = 0;
protected array $rootLine = [];
protected string $section = '';
protected array $additionalQueryParameters = [];
protected bool $moduleLoading = true;
protected Context $context;
/**
* @param int|array $page Page ID to be previewed
*/
public static function create(int|array $page): self
{
$pageId = 0;
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages');
$laguageFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$transOrigPointerFieldName = $schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
if (!is_array($page)) {
$pageId = $page;
if ($pageId > 0) {
// If a page ID is given, we fetch the record from the database
$page = BackendUtility::getRecord('pages', $pageId) ?? [];
} else {
// If no valid page ID is given, we use an empty array
$page = [];
}
}
if ($page !== []) {
if (($page[$transOrigPointerFieldName] ?? 0) > 0) {
// If the page is a translation, we need to use the parent page's ID
$pageId = (int)$page[$transOrigPointerFieldName];
} else {
// Otherwise, use the current page's ID
$pageId = (int)($page['uid'] ?? 0);
}
}
$obj = new self($pageId);
$obj->pageId = $pageId;
$obj->record = $page;
$obj->languageId = (int)($page[$laguageFieldName] ?? 0);
return $obj;
}
/**
* @internal Only to be used by TYPO3 core - for now
*/
public static function createForRecordPreview(string $table, int|array|RecordInterface $record, int $pageId): self
{
if ($record instanceof RecordInterface) {
$record = $record->getRawRecord()->toArray();
}
$recordId = is_int($record) ? $record : (int)($record['uid'] ?? 0);
$previewPageId = self::getPreviewPageId($table, $recordId, $pageId);
$obj = self::create($previewPageId);
$obj->table = $table;
$obj = $obj->withRootLine(BackendUtility::BEgetRootLine($previewPageId));
$obj = $obj->withSection($table === 'tt_content' ? '#c' . $recordId : '');
$obj = $obj->withAdditionalQueryParameters(self::getPreviewUrlParameters($previewPageId, $table, $record));
return $obj;
}
/**
* @param int $pageId Page ID to be previewed
*/
public function __construct(int $pageId)
{
$this->pageId = $pageId;
$this->context = clone GeneralUtility::makeInstance(Context::class);
$this->context->setAspect('visibility', new VisibilityAspect(true, false, false, true));
}
/**
* @param bool $moduleLoading whether to enable JavaScript module loading
* @return static
*/
public function withModuleLoading(bool $moduleLoading): self
{
if ($this->moduleLoading === $moduleLoading) {
return $this;
}
$target = clone $this;
$target->moduleLoading = $moduleLoading;
return $target;
}
/**
* @param array $rootLine (alternative) root-line of pages
* @return static
*/
public function withRootLine(array $rootLine): self
{
if ($this->rootLine === $rootLine) {
return $this;
}
$target = clone $this;
$target->rootLine = $rootLine;
return $target;
}
/**
* @param int $language particular language
* @return static
*/
public function withLanguage(int $language): self
{
if ($this->languageId === $language) {
return $this;
}
$target = clone $this;
$target->languageId = $language;
return $target;
}
/**
* @param string $section particular section (anchor element)
* @return static
*/
public function withSection(string $section): self
{
if ($this->section === $section) {
return $this;
}
$target = clone $this;
$target->section = $section;
return $target;
}
/**
* @param string|array $additionalQueryParameters additional URI query parameters
* @return static
*/
public function withAdditionalQueryParameters(array|string $additionalQueryParameters): self
{
if (is_array($additionalQueryParameters)) {
$additionalQueryParams = $additionalQueryParameters;
} else {
$additionalQueryParams = [];
parse_str($additionalQueryParameters, $additionalQueryParams);
}
$languageId = $this->languageId;
if (isset($additionalQueryParams['_language'])) {
$languageId = (int)$additionalQueryParams['_language'];
unset($additionalQueryParams['_language']);
}
// No change
if ($this->languageId === $languageId && $additionalQueryParams === $this->additionalQueryParameters) {
return $this;
}
$target = clone $this;
$target->additionalQueryParameters = $additionalQueryParams;
$target->languageId = $languageId;
return $target;
}
public function isPreviewable(): bool
{
if ($this->pageId === 0 || !isset($this->record['doktype'])) {
return false;
}
$isDeletePlaceholder = VersionState::tryFrom($this->record['t3ver_state'] ?? 0) === VersionState::DELETE_PLACEHOLDER;
if ($isDeletePlaceholder) {
return false;
}
// Custom records need to be configured via pages TSconfig to allow previews
if ($this->table !== 'pages' && $this->table !== 'tt_content') {
$previewConfiguration = BackendUtility::getPagesTSconfig($this->pageId)['TCEMAIN.']['preview.'][$this->table . '.'] ?? null;
if (!is_array($previewConfiguration)) {
return false;
}
}
return self::isPreviewableDoktype($this->pageId, (int)$this->record['doktype']);
}
/**
* Builds preview URI.
*/
public function buildUri(?array $options = null, ?Context $context = null): ?UriInterface
{
$eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
try {
$event = new BeforePagePreviewUriGeneratedEvent(
$this->pageId,
$this->languageId,
$this->rootLine,
$this->section,
$this->additionalQueryParameters,
$context ?? $this->context,
$this->enrichOptions($options)
);
$eventDispatcher->dispatch($event);
// If there hasn't been a custom preview URI set by an event listener, generate it.
if ($event->getPreviewUri() === null) {
if (!$this->isPreviewable()) {
return null;
}
$permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
$pageInfo = BackendUtility::readPageAccess($event->getPageId(), $permissionClause) ?: [];
// Check if the page (= its rootline) has a site attached, otherwise just keep the URI as is
if ($event->getRootline() === []) {
$event->setRootline(BackendUtility::BEgetRootLine($event->getPageId()));
}
// prepare custom context for link generation (to allow for example time based previews)
$event->setAdditionalQueryParameters(
array_replace_recursive(
$event->getAdditionalQueryParameters(),
self::getAdditionalQueryParametersForAccessRestrictedPages($pageInfo, $event->getContext(), $event->getRootline())
)
);
// Build the URI with a site as prefix, if configured
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
try {
$site = $siteFinder->getSiteByPageId($event->getPageId(), $event->getRootline());
} catch (SiteNotFoundException $e) {
throw new UnableToLinkToPageException('The page ' . $event->getPageId() . ' had no proper connection to a site, no link could be built.', 1651499353);
}
try {
$previewRouteParameters = $event->getAdditionalQueryParameters();
// Reassemble encapsulated language id into route parameters to get proper localized page preview
// uri for non-default languages.
if ($event->getLanguageId() > 0) {
$previewRouteParameters['_language'] = $site->getLanguageById($event->getLanguageId());
}
$event->setPreviewUri(
$site->getRouter($event->getContext())->generateUri(
$event->getPageId(),
$previewRouteParameters,
$event->getSection(),
RouterInterface::ABSOLUTE_URL
)
);
} catch (\InvalidArgumentException|InvalidRouteArgumentsException $e) {
throw new UnableToLinkToPageException(sprintf('The link to the page with ID "%d" could not be generated: %s', $event->getPageId(), $e->getMessage()), 1651499354, $e);
}
}
$event = new AfterPagePreviewUriGeneratedEvent(
$event->getPreviewUri(),
$event->getPageId(),
$event->getLanguageId(),
$event->getRootline(),
$event->getSection(),
$event->getAdditionalQueryParameters(),
$event->getContext(),
$event->getOptions(),
);
$eventDispatcher->dispatch($event);
return $event->getPreviewUri();
} catch (UnableToLinkToPageException $e) {
return null;
}
}
/**
* Builds attributes array (e.g. `['dispatch-action' => ...]`).
* CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
*
* @param array|null $options
*/
public function buildDispatcherDataAttributes(?array $options = null): ?array
{
if (null === ($attributes = $this->buildAttributes($options))) {
return null;
}
$this->loadActionDispatcher();
return $this->prefixAttributeNames('dispatch-', $attributes);
}
/**
* Builds attributes array (e.g. `['data-dispatch-action' => ...]`).
* CAVE: Attributes are NOT XSS-protected and need to be put through `htmlspecialchars`
*
* @param array|null $options
*/
public function buildDispatcherAttributes(?array $options = null): ?array
{
if (null === ($attributes = $this->buildAttributes($options))) {
return null;
}
$this->loadActionDispatcher();
return $this->prefixAttributeNames('data-dispatch-', $attributes);
}
/**
* Serialized attributes are processed with `htmlspecialchars` and ready to be used.
*
* @param array|null $options
*/
public function serializeDispatcherAttributes(?array $options = null): ?string
{
if (null === ($attributes = $this->buildDispatcherAttributes($options))) {
return null;
}
return ' ' . GeneralUtility::implodeAttributes($attributes, true);
}
/**
* `<typo3-immediate-action>` does not have a specific meaning and is used to
* expose `data` attributes, see custom element in `ImmediateActionElement.ts`.
*
* @param array|null $options
*/
public function buildImmediateActionElement(?array $options = null): ?string
{
if (null === ($attributes = $this->buildAttributes($options))) {
return null;
}
$this->loadImmediateActionElement();
return sprintf(
// `<typo3-immediate-action action="TYPO3.WindowManager.localOpen" args="[...]">`
'<typo3-immediate-action %s></typo3-immediate-action>',
GeneralUtility::implodeAttributes($attributes, true)
);
}
/**
* Returns the preview page id, based on the given input, by checking
* preview configuration and alternatively looking up the rootline.
*
* @param string $table The table of the record to be previewed - might be empty for direct page preview ($pageId > 0)
* @param int $recordId The id of the record to be previewed - might be empty for direct page preview ($pageId > 0)
* @param int $pageId The page to preview the record on, also used to preview a page directly
* @internal Only to be used by TYPO3 core
*/
public static function getPreviewPageId(string $table, int $recordId, int $pageId): int
{
if ($table === 'pages') {
$rootPageId = $recordId;
if ($rootPageId) {
$l10nPointer = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages')->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
$page = BackendUtility::getRecord('pages', $rootPageId, 'is_siteroot,' . $l10nPointer);
if ($page['is_siteroot'] && $page[$l10nPointer]) {
$rootPageId = $page[$l10nPointer];
}
}
} else {
$rootPageId = max(0, $pageId);
}
$previewConfiguration = BackendUtility::getPagesTSconfig($rootPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
if (isset($previewConfiguration['previewPageId'])) {
return (int)$previewConfiguration['previewPageId'];
}
$rootPageData = null;
$rootLine = BackendUtility::BEgetRootLine($rootPageId);
$currentPage = (array)(reset($rootLine) ?: []);
if (VersionState::tryFrom($currentPage['t3ver_state'] ?? 0) !== VersionState::DELETE_PLACEHOLDER
&& self::isPreviewableDoktype((int)($currentPage['uid'] ?? 0), (int)($currentPage['doktype'] ?? 0))
) {
// try the current page
$previewPageId = $rootPageId;
} else {
// or search for the root page
foreach ($rootLine as $page) {
if ($page['is_siteroot'] ?? false) {
$rootPageData = $page;
break;
}
}
$previewPageId = isset($rootPageData)
? (int)$rootPageData['uid']
: $rootPageId;
}
return $previewPageId;
}
protected function buildAttributes(?array $options = null): ?array
{
$options = $this->enrichOptions($options);
if (null === ($uri = $this->buildUri($options))) {
return null;
}
$args = [
// target URI
(string)$uri,
// whether to switch focus to that window
$options[self::OPTION_SWITCH_FOCUS],
// name of the window instance for JavaScript references
$options[self::OPTION_WINDOW_NAME],
];
if (isset($options[self::OPTION_WINDOW_FEATURES])) {
// optional window features (e.g. 'width=500,height=300')
$args[] = $options[self::OPTION_WINDOW_FEATURES];
}
return [
'action' => $options[self::OPTION_WINDOW_SCOPE] === self::OPTION_WINDOW_SCOPE_GLOBAL
? 'TYPO3.WindowManager.globalOpen'
: 'TYPO3.WindowManager.localOpen',
'args' => json_encode($args),
];
}
/**
* Handles options to used for opening preview URI in a new window/tab.
* + `switchFocus` (bool): whether to focus new window in browser
* + `windowName` (string): name of window for internal reference
* + `windowScope` (string): `local` (current document) `global` (whole backend)
*
* @param array|null $options
*/
protected function enrichOptions(?array $options = null): array
{
return array_merge(
[
self::OPTION_SWITCH_FOCUS => null,
// 'newTYPO3frontendWindow' was used in BackendUtility::viewOnClick
self::OPTION_WINDOW_NAME => 'newTYPO3frontendWindow',
self::OPTION_WINDOW_SCOPE => self::OPTION_WINDOW_SCOPE_LOCAL,
],
$options ?? []
);
}
protected function loadActionDispatcher(): void
{
if (!$this->moduleLoading) {
return;
}
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js');
}
protected function loadImmediateActionElement(): void
{
if (!$this->moduleLoading) {
return;
}
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
$pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js');
}
protected function prefixAttributeNames(string $prefix, array $attributes): array
{
$attributeNames = array_map(
static function (string $name) use ($prefix): string {
return $prefix . $name;
},
array_keys($attributes)
);
return array_combine(
$attributeNames,
array_values($attributes)
);
}
/**
* Creates ADMCMD parameters for the "viewpage" extension / frontend
* @internal not part of TYPO3 Core API
*/
public static function getAdditionalQueryParametersForAccessRestrictedPages(array $pageInfo, Context $context, array $rootLine): array
{
if ($pageInfo === []) {
return [];
}
// Initialize access restriction values from current page
$access = [
'fe_group' => (string)($pageInfo['fe_group'] ?? ''),
'starttime' => (int)($pageInfo['starttime'] ?? 0),
'endtime' => (int)($pageInfo['endtime'] ?? 0),
];
// Only check rootline if the current page has not set extendToSubpages itself
if (!(bool)($pageInfo['extendToSubpages'] ?? false)) {
// remove the current page from the rootline
array_shift($rootLine);
foreach ($rootLine as $page) {
// Skip root node and pages which do not define extendToSubpages
if ((int)($page['uid'] ?? 0) === 0 || !(bool)($page['extendToSubpages'] ?? false)) {
continue;
}
$access['fe_group'] = (string)($page['fe_group'] ?? '');
$access['starttime'] = (int)($page['starttime'] ?? 0);
$access['endtime'] = (int)($page['endtime'] ?? 0);
// Stop as soon as a page in the rootline has extendToSubpages set
break;
}
}
$additionalQueryParameters = [];
if ((int)$access['fe_group'] === -2) {
// -2 means "show at any login". We simulate first available fe_group.
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('fe_groups');
$queryBuilder->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(HiddenRestriction::class));
$activeFeGroupId = $queryBuilder->select('uid')
->from('fe_groups')
->executeQuery()
->fetchOne();
if ($activeFeGroupId) {
$additionalQueryParameters['ADMCMD_simUser'] = $activeFeGroupId;
}
} elseif (!empty($access['fe_group'])) {
$additionalQueryParameters['ADMCMD_simUser'] = $access['fe_group'];
}
if ($access['starttime'] > $GLOBALS['EXEC_TIME']) {
// simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate
// a URL for it
$dateAspect = new DateTimeAspect(DateTimeFactory::createFromTimestamp($access['starttime']));
$context->setAspect('date', $dateAspect);
$additionalQueryParameters['ADMCMD_simTime'] = $access['starttime'];
}
if ($access['endtime'] < $GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) {
// Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and
// in turn PageRouter will generate a URL for it
$dateAspect = new DateTimeAspect(DateTimeFactory::createFromTimestamp($access['endtime'] - 1));
$context->setAspect('date', $dateAspect);
$additionalQueryParameters['ADMCMD_simTime'] = ($access['endtime'] - 1);
}
return $additionalQueryParameters;
}
/**
* Returns the parameters for the preview URL by evaluating language overlays and preview configuration
*/
protected static function getPreviewUrlParameters(int $previewPageId, string $table, int|array $record): string
{
if (is_array($record)) {
$recordId = (int)($record['uid'] ?? 0);
} else {
$recordId = $record;
$record = BackendUtility::getRecord($table, $record) ?? [];
}
$linkParameters = [];
$previewConfiguration = BackendUtility::getPagesTSconfig($previewPageId)['TCEMAIN.']['preview.'][$table . '.'] ?? [];
// language handling
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($table);
if ($schema->isLanguageAware()
&& ($languageField = $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName())
&& !empty($record[$languageField])
) {
$l10nPointer = $schema->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
if (!empty($record[$l10nPointer]) && ($previewConfiguration['useDefaultLanguageRecord'] ?? true) === false) {
$recordId = (int)$record[$l10nPointer];
} else {
$recordId = (int)$record['uid'];
}
$language = $record[$languageField];
if ($language > 0) {
$linkParameters['_language'] = $language;
}
}
// Always use live workspace record uid for the preview
if ($schema->isWorkspaceAware() && ($record['t3ver_oid'] ?? 0) > 0) {
$recordId = $record['t3ver_oid'];
}
// map record data to GET parameters
if (isset($previewConfiguration['fieldToParameterMap.'])) {
foreach ($previewConfiguration['fieldToParameterMap.'] as $field => $parameterName) {
$value = $record[$field] ?? '';
if ($field === 'uid') {
$value = $recordId;
}
$linkParameters[$parameterName] = $value;
}
}
// add/override parameters by configuration
if (isset($previewConfiguration['additionalGetParameters.'])) {
$linkParameters = array_replace(
$linkParameters,
GeneralUtility::removeDotsFromTS($previewConfiguration['additionalGetParameters.'])
);
}
return HttpUtility::buildQueryString($linkParameters, '&');
}
/**
* Check whether the current page has a doktype, which can be previewed
*/
protected static function isPreviewableDoktype(int $pageId, int $doktype): bool
{
if ($pageId <= 0 || $doktype <= 0) {
return false;
}
$doktypeRegistry = GeneralUtility::makeInstance(PageDoktypeRegistry::class);
return $doktypeRegistry->isPageViewable($doktype, $pageId);
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
/*
* 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\Backend\Routing;
use Symfony\Component\Routing\Route as SymfonyRoute;
/**
* This is a single entity for a Route.
*
* The architecture is highly inspired by the Symfony Routing Component.
*/
class Route
{
/**
* @var string
*/
protected $path = '/';
protected array $methods = [];
/**
* @var array
*/
protected $options = [];
public static function fromSymfonyRoute(SymfonyRoute $source, string $identifier): self
{
$options = $source->getOptions();
// Store the name of the Route in the _identifier option so the token can be checked against that
$options['_identifier'] = $identifier;
$methods = $options['methods'] ?? [];
unset($options['methods']);
$target = new self($source->getPath(), $options);
if (is_array($methods) && $methods !== []) {
$target->setMethods($methods);
}
return $target;
}
/**
* Constructor setting up the required path and options
*
* @param string $path The path pattern to match
* @param array $options An array of options
*/
public function __construct($path, $options)
{
$this->setPath($path)->setOptions($options);
}
/**
* Returns the path
*
* @return string The path pattern
*/
public function getPath()
{
return $this->path;
}
/**
* Sets the pattern for the path
* A pattern must start with a slash and must not have multiple slashes at the beginning because the
* generated path for this route would be confused with a network path, e.g. '//domain.com/path'.
*
* This method implements a fluent interface.
*
* @param string $pattern The path pattern
* @return Route The current Route instance
*/
public function setPath($pattern)
{
$this->path = '/' . ltrim(trim($pattern), '/');
return $this;
}
/**
* Returns the uppercased HTTP methods this route is restricted to.
* An empty array means that any method is allowed.
*
* @return string[] The methods
*/
public function getMethods(): array
{
return $this->methods;
}
/**
* Sets the HTTP methods (e.g. ['POST']) this route is restricted to.
* An empty array means that any method is allowed.
*
* This method implements a fluent interface.
*
* @param string[] $methods The array of allowed methods
*/
public function setMethods(array $methods): self
{
$this->methods = array_map(strtoupper(...), $methods);
return $this;
}
/**
* Returns the options set
*
* @return array The options
*/
public function getOptions()
{
return $this->options;
}
/**
* Sets the options
*
* This method implements a fluent interface.
*
* @param array $options The options
* @return Route The current Route instance
*/
public function setOptions(array $options)
{
$this->options = $options;
return $this;
}
/**
* Sets an option value
*
* This method implements a fluent interface.
*
* @param string $name An option name
* @param mixed $value The option value
* @return Route The current Route instance
*/
public function setOption($name, $value)
{
$this->options[$name] = $value;
return $this;
}
/**
* Get an option value
*
* @param string $name An option name
* @return mixed The option value or NULL when not given
*/
public function getOption($name)
{
return $this->options[$name] ?? null;
}
/**
* Checks if an option has been set
*
* @param string $name An option name
* @return bool TRUE if the option is set, FALSE otherwise
*/
public function hasOption($name)
{
return array_key_exists($name, $this->options);
}
}
+150
View File
@@ -0,0 +1,150 @@
<?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\Backend\Routing;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\Exception\RouteTypeNotAllowedException;
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
use TYPO3\CMS\Core\Utility\ArrayUtility;
/**
* A value object representing redirects within Backend routing.
*/
class RouteRedirect
{
/**
* Name of route to be redirected to
*/
private string $name;
/**
* Multi-dimensional query params array
* e.g. `['level1' => ['level2' => 'value']]`
*/
private array $parameters;
public static function create(string $name, $params): self
{
if (is_string($params)) {
parse_str($params, $parsedParameters);
$params = $parsedParameters;
} elseif (!is_array($params)) {
throw new \LogicException('Params must be array or string', 1627907107);
}
return new self($name, $params);
}
public static function createFromRoute(Route $route, array $parameters): self
{
return new self($route->getOption('_identifier'), $parameters);
}
public static function createFromRequest(ServerRequestInterface $request): ?self
{
$name = $request->getQueryParams()['redirect'] ?? null;
if (empty($name)) {
return null;
}
return self::create($name, $request->getQueryParams()['redirectParams'] ?? []);
}
private function __construct(string $name, array $params)
{
$this->name = $name;
$this->parameters = $this->sanitizeParameters($params);
}
private function sanitizeParameters(array $redirectParameters): array
{
unset($redirectParameters['token']);
unset($redirectParameters['route']);
unset($redirectParameters['redirect']);
unset($redirectParameters['redirectParams']);
return $redirectParameters;
}
public function getName(): string
{
return $this->name;
}
public function getParameters(): array
{
return $this->parameters;
}
public function getFormattedParameters(): string
{
$redirectParameters = http_build_query($this->parameters, '', '&', PHP_QUERY_RFC3986);
return ltrim($redirectParameters, '&?');
}
public function hasParameters(): bool
{
return !empty($this->parameters);
}
/**
* Checks if the route can be resolved as a redirect.
*
* @throws RouteNotFoundException
* @throws MethodNotAllowedException
* @throws RouteTypeNotAllowedException
*/
public function resolve(Router $router): void
{
$route = $router->getRoute($this->name);
if ($route === null) {
throw new RouteNotFoundException(
sprintf('Route "%s" was not found', $this->name),
1627907587
);
}
if ($route->getOption('ajax')) {
throw new RouteTypeNotAllowedException(
sprintf('AJAX route "%s" cannot be redirected', $this->name),
1627407451
);
}
// Links to modules are always allowed, no problem here
// Check for the AJAX option should be handled before
if ($route->getOption('module')) {
return;
}
if ($route->getMethods() !== [] && !in_array('GET', $route->getMethods(), true)) {
throw new MethodNotAllowedException(
$route->getMethods(),
1627407452
);
}
$settings = $route->getOption('redirect');
if (($settings['enable'] ?? false) !== true) {
throw new RouteNotFoundException(
sprintf('Route "%s" cannot be redirected', $this->name),
1627407511
);
}
// Only use allowed arguments, if set, otherwise no parameters are allowed
if (!empty($settings['parameters'])) {
$this->parameters = ArrayUtility::intersectRecursive($this->parameters, (array)$settings['parameters']);
} else {
$this->parameters = [];
}
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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\Backend\Routing;
use TYPO3\CMS\Core\Routing\RouteResultInterface;
/**
* A route result for the TYPO3 Backend Routing,
* containing the matched Route and the related arguments found in the URL
*/
class RouteResult implements RouteResultInterface
{
public function __construct(
protected Route $route,
protected array $arguments = [],
) {}
public function getRoute(): Route
{
return $this->route;
}
public function getRouteName(): string
{
return $this->route->getOption('_identifier');
}
public function getArguments(): array
{
return $this->arguments;
}
public function offsetExists($offset): bool
{
return $offset === 'route' || isset($this->arguments[$offset]);
}
public function offsetGet(mixed $offset): mixed
{
return match ($offset) {
'route' => $this->route,
default => $this->arguments[$offset],
};
}
public function offsetSet(mixed $offset = '', mixed $value = ''): void
{
switch ($offset) {
case 'route':
$this->route = $value;
break;
default:
$this->arguments[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
switch ($offset) {
case 'route':
throw new \InvalidArgumentException('You can never unset the Route in a route result', 1669839336);
default:
unset($this->arguments[$offset]);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
/*
* 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\Backend\Routing;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Route as SymfonyRoute;
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
use TYPO3\CMS\Core\Routing\RequestContextFactory;
use TYPO3\CMS\Core\Routing\RouteCollection;
/**
* Implementation of a class for adding routes, collecting throughout the Bootstrap
* to register all sorts of Backend Routes, and to fetch the main Collection in order
* to resolve a route (see ->match() and ->matchRequest()).
*
* Ideally, the Router is solely instantiated and accessed via the Bootstrap, the RequestHandler and the UriBuilder.
*
* See \TYPO3\CMS\Backend\Http\RequestHandler for more details on route matching() and Bootstrap->initializeBackendRouting().
*
* The architecture is inspired by the Symfony Routing Component.
*/
#[Autoconfigure(
public: true,
configurator: '@' . RouterConfigurator::class,
)]
class Router
{
/**
* All routes used in the TYPO3 Backend
*/
protected RouteCollection $routeCollection;
public function __construct(
protected readonly RequestContextFactory $requestContextFactory,
protected readonly BackendEntryPointResolver $backendEntryPointResolver,
) {
$this->routeCollection = new RouteCollection();
}
/**
* Adds a new route with a given identifier
*/
public function addRoute(string $routeIdentifier, Route $route, array $aliases = []): void
{
$symfonyRoute = new SymfonyRoute($route->getPath(), [], [], $route->getOptions());
$symfonyRoute->setMethods($route->getMethods());
$this->routeCollection->add($routeIdentifier, $symfonyRoute);
foreach ($aliases as $aliasName) {
$this->routeCollection->addAlias($aliasName, $routeIdentifier);
}
}
public function addRouteCollection(RouteCollection $routeCollection): void
{
$this->routeCollection->addCollection($routeCollection);
}
/**
* Fetch all registered routes, only use in UriBuilder. Does not care about aliases,
* so be careful with using this method.
*/
public function getRoutes(): \Iterator
{
return $this->routeCollection->getIterator();
}
public function hasRoute(string $routeName): bool
{
return $this->routeCollection->get($routeName) !== null;
}
/**
* Returns a route by its identifier, or null if nothing found.
*
* @return SymfonyRoute|Route|null
*/
public function getRoute(string $routeName)
{
return $this->routeCollection->get($routeName);
}
/**
* @internal only use in Core, this should not be exposed
*/
public function getRouteCollection(): RouteCollection
{
return $this->routeCollection;
}
/**
* Tries to match a URL path with a set of routes.
*
* @param string $pathInfo The path info to be parsed
* @return Route the first Route object found
* @throws ResourceNotFoundException If the resource could not be found
*/
public function match($pathInfo): Route
{
foreach ($this->routeCollection->getIterator() as $routeIdentifier => $route) {
// This check is done in a simple way as there are no parameters yet (get parameters only)
if ($route->getPath() === $pathInfo) {
return Route::fromSymfonyRoute($route, $routeIdentifier);
}
}
throw new ResourceNotFoundException('The requested resource "' . $pathInfo . '" was not found.', 1425389240);
}
/**
* Matches a PSR-7 Request and returns a RouteResult with parameters and the resolved route.
*/
public function matchResult(ServerRequestInterface $request): RouteResult
{
$path = $this->backendEntryPointResolver->getBackendRoutePath($request);
if ($path === null) {
throw new ResourceNotFoundException('The requested resource "' . $request->getUri()->getPath() . '" does not contain a known backend route prefix.', 1704787661);
}
if ($path === '' || $path === '/' || $path === '/index.php') {
// Allow the login page to be displayed if routing is not used and on index.php
// (consolidate RouteDispatcher::evaluateReferrer() when changing 'login' to something different)
$path = '/login';
}
$requestContext = $this->requestContextFactory->fromBackendRequest($request);
try {
$result = (new UrlMatcher($this->routeCollection, $requestContext))->match($path);
$matchedSymfonyRoute = $this->routeCollection->get($result['_route']);
if ($matchedSymfonyRoute === null) {
throw new ResourceNotFoundException('The requested resource "' . $path . '" was not found.', 1607596900);
}
} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
throw new MethodNotAllowedException($e->getAllowedMethods(), 1612649842, $e);
} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
throw new ResourceNotFoundException('The requested resource "' . $path . '" was not found.', 1612649840);
}
$route = Route::fromSymfonyRoute($matchedSymfonyRoute, $result['_route']);
unset($result['_route']);
return new RouteResult($route, $result);
}
/**
* Tries to match a URI against the registered routes.
* Use ->matchResult() instead, as this method will be deprecated in the future.
*
* @return Route the first Route object found
*/
public function matchRequest(ServerRequestInterface $request): Route
{
return $this->matchResult($request)->getRoute();
}
}
@@ -0,0 +1,28 @@
<?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\Backend\Routing;
/**
* @internal
*/
final readonly class RouterConfigurationEvent
{
public function __construct(
public Router $router,
) {}
}
+62
View File
@@ -0,0 +1,62 @@
<?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\Backend\Routing;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
/**
* @internal
*/
final readonly class RouterConfigurator
{
public function __construct(
#[Autowire(service: 'backend.routes', lazy: true)]
private \ArrayObject $backendRoutes,
#[Autowire(service: 'cache.core')]
private PhpFrontend $coreCache,
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("BackendRoutes").toString()')]
private string $cacheIdentifier,
private EventDispatcherInterface $eventDispatcher,
) {}
public function __invoke(Router $router): void
{
$routesFromPackages = $this->coreCache->require($this->cacheIdentifier);
if ($routesFromPackages === false) {
$routesFromPackages = $this->backendRoutes->getArrayCopy();
$this->coreCache->set($this->cacheIdentifier, 'return ' . var_export($routesFromPackages, true) . ';');
}
foreach ($routesFromPackages as $name => $options) {
$path = $options['path'];
$methods = $options['methods'] ?? [];
$aliases = $options['aliases'] ?? [];
unset($options['path'], $options['methods'], $options['aliases']);
$route = new Route($path, $options);
if ($methods !== []) {
$route->setMethods($methods);
}
$router->addRoute($name, $route, $aliases);
}
// Add routes from all modules
$this->eventDispatcher->dispatch(new RouterConfigurationEvent($router));
}
}
+205
View File
@@ -0,0 +1,205 @@
<?php
/*
* 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\Backend\Routing;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route as SymfonyRoute;
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
use TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException;
use TYPO3\CMS\Backend\Routing\Exception\RouteTypeNotAllowedException;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
use TYPO3\CMS\Core\Http\Error\MethodNotAllowedException;
use TYPO3\CMS\Core\Http\ServerRequestFactory;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Routing\RequestContextFactory;
use TYPO3\CMS\Core\SingletonInterface;
/**
* Main UrlGenerator for creating URLs for the Backend. Generates a URL based on
* an identifier defined by Configuration/Backend/Routes.php of an extension,
* and adds some more parameters to the URL.
*
* Currently only available and useful when called from Router->generate() as the information
* about possible routes needs to be handed over.
*/
class UriBuilder implements SingletonInterface
{
/**
* Generates an absolute URL
*/
public const ABSOLUTE_URL = 'url';
/**
* Generates an absolute path
*/
public const ABSOLUTE_PATH = 'absolute';
/**
* Generates an absolute url for URL sharing
*/
public const SHAREABLE_URL = 'share';
/**
* @var array<non-empty-string, UriInterface>
*/
protected array $generated = [];
protected ?RequestContext $requestContext = null;
/**
* Loads the router to fetch the available routes from the Router to be used for generating routes
*/
public function __construct(
protected readonly Router $router,
protected readonly FormProtectionFactory $formProtectionFactory,
protected readonly RequestContextFactory $requestContextFactory
) {}
/**
* @internal
*/
public function setRequestContext(RequestContext $requestContext): void
{
$this->requestContext = $requestContext;
}
/**
* Generates a URL or path for a specific route based on the given route.
* Currently used to link to the current script, it is encouraged to use "buildUriFromRoute" if possible.
*
* If there is no route with the given name, the generator throws the RouteNotFoundException.
*
* @param string $pathInfo The path to the route
* @param array $parameters An array of parameters
* @param string $referenceType The type of reference to be generated (one of the constants)
* @return UriInterface The generated Uri
* @throws RouteNotFoundException If the named route doesn't exist
* @throws ResourceNotFoundException If no route matches the path info
*/
public function buildUriFromRoutePath($pathInfo, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
$route = $this->router->match($pathInfo);
return $this->buildUriFromRoute($route->getOption('_identifier'), $parameters, $referenceType);
}
/**
* Creates a link to a page with a route targetted as a redirect, if a "deep link" is possible.
* Currently works just fine for URLs built for "main" and "login" pages.
*
* @param RouteRedirect|null $redirect
* @throws RouteNotFoundException
* @internal this is experimental API used for creating logins to redirect to a different route
*/
public function buildUriWithRedirect(string $name, array $parameters = [], ?RouteRedirect $redirect = null, string $referenceType = self::ABSOLUTE_PATH): UriInterface
{
if ($redirect === null) {
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}
try {
$redirect->resolve($this->router);
} catch (RouteNotFoundException|RouteTypeNotAllowedException|MethodNotAllowedException $e) {
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}
$parameters['redirect'] = $redirect->getName();
if ($redirect->hasParameters()) {
$parameters['redirectParams'] = $redirect->getFormattedParameters();
}
return $this->buildUriFromRoute($name, $parameters, $referenceType);
}
/**
* A shorthand function to link to e.g. the same as in the current request, so the internal attribute "route"
* is properly covered and does not need to be exposed all the time.
*/
public function buildUriFromRequest(ServerRequestInterface $request, array $parameters = [], string $referenceType = self::ABSOLUTE_PATH): UriInterface
{
$route = $request->getAttribute('route');
if (!$route instanceof Route && !$route instanceof SymfonyRoute) {
throw new RouteNotFoundException('No route object was given inside the request object', 1691423325);
}
return $this->buildUriFromRoute($route->getOption('_identifier'), $parameters, $referenceType);
}
/**
* Generates a URL or path for a specific route based on the given parameters.
* When the route is configured with "access=public" then the token generation is left out.
*
* If there is no route with the given name, the generator throws the RouteNotFoundException.
*
* @param string $name The name of the route
* @param array $parameters An array of parameters
* @param string $referenceType The type of reference to be generated (one of the constants)
* @return UriInterface The generated Uri
* @throws RouteNotFoundException If the named route doesn't exist
*/
public function buildUriFromRoute($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
$cacheIdentifier = 'route' . $name . serialize($parameters) . $referenceType;
if (isset($this->generated[$cacheIdentifier])) {
return $this->generated[$cacheIdentifier];
}
$route = $this->router->getRoute((string)$name);
if ($route === null) {
throw new RouteNotFoundException('Unable to generate a URL for the named route "' . $name . '" because this route was not found.', 1476050190);
}
$parameters = array_merge(
$route->getOption('parameters') ?? [],
$parameters
);
// If the route is not shareable and doesn't have the "public" option set, a token must be generated.
if ($referenceType !== self::SHAREABLE_URL && (!$route->hasOption('access') || $route->getOption('access') !== 'public')) {
$parameters = [
'token' => $this->formProtectionFactory->createForType('backend')->generateToken('route', $name),
] + $parameters;
}
$this->generated[$cacheIdentifier] = $this->buildUri($name, $parameters, (string)$referenceType);
return $this->generated[$cacheIdentifier];
}
/**
* Internal method building a Uri object, merging the GET parameters array into a flat queryString
*
* @param string $routeName The route name in the collection
* @param array $parameters An array of GET parameters
* @param string $referenceType The type of reference to be generated (one of the constants)
*/
protected function buildUri(string $routeName, array $parameters, string $referenceType): UriInterface
{
if (isset($this->requestContext)) {
$requestContext = $this->requestContext;
} elseif (isset($GLOBALS['TYPO3_REQUEST'])) {
$requestContext = $this->requestContextFactory->fromBackendRequest($GLOBALS['TYPO3_REQUEST']);
} elseif (!Environment::isCli()) {
$requestContext = $this->requestContextFactory->fromBackendRequest(ServerRequestFactory::fromGlobals()->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_BE));
} else {
$requestContext = new RequestContext('/typo3/');
}
$urlGenerator = new UrlGenerator($this->router->getRouteCollection(), $requestContext);
$url = $urlGenerator->generate($routeName, $parameters, $referenceType === self::ABSOLUTE_PATH ? UrlGeneratorInterface::ABSOLUTE_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
return new Uri($url);
}
}