TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Page\FrontendUrlPrefix;
|
||||
|
||||
/**
|
||||
* Abstract class to provide proper helper for most types necessary
|
||||
*/
|
||||
abstract class AbstractTypolinkBuilder
|
||||
{
|
||||
/**
|
||||
* Forces a given URL to be absolute.
|
||||
*
|
||||
* @param string $url The URL to be forced to be absolute
|
||||
* @param array $configuration TypoScript configuration of typolink
|
||||
* @return string The absolute URL
|
||||
*/
|
||||
protected function forceAbsoluteUrl(string $url, array $configuration, ?ServerRequestInterface $request = null): string
|
||||
{
|
||||
$frontendTypoScriptConfigArray = $request ? $request->getAttribute('frontend.typoscript')?->getConfigArray() : [];
|
||||
if ($frontendTypoScriptConfigArray['forceAbsoluteUrls'] ?? false) {
|
||||
$forceAbsoluteUrl = true;
|
||||
} else {
|
||||
$forceAbsoluteUrl = !empty($configuration['forceAbsoluteUrl']);
|
||||
}
|
||||
// This part typically touches ONLY files/folders and external URLs, and ONLY the ones that do not have
|
||||
// "config.forceAbsoluteUrls" but only "typolink.forceAbsoluteUrl" set. Ideally, we could evaluate this
|
||||
// in the FAL ResourceUriGenerator and then remove this logic. Also see the comment below with the @todo
|
||||
if (!empty($url) && $forceAbsoluteUrl && preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
|
||||
$urlParts = [
|
||||
'scheme' => $matches[1],
|
||||
'delimiter' => '://',
|
||||
'host' => $matches[3],
|
||||
'path' => $matches[4],
|
||||
];
|
||||
$isUrlModified = false;
|
||||
// Set scheme and host if not yet part of the URL
|
||||
if (empty($urlParts['host'])) {
|
||||
// absRefPrefix has been prepended to $url beforehand
|
||||
// so we only modify the path if no absRefPrefix has been set
|
||||
// otherwise we would destroy the path
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
// @todo: This fallback should vanish mid-term: typolink has a dependency to ServerRequest
|
||||
// and should expect the normalizedParams argument is properly set as well. When for
|
||||
// instance CLI triggers this code, it should have set up a proper request.
|
||||
$normalizedParams ??= NormalizedParams::createFromRequest($request);
|
||||
$urlParts['scheme'] = $normalizedParams->isHttps() ? 'https' : 'http';
|
||||
$urlParts['host'] = $normalizedParams->getHttpHost();
|
||||
if (GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($request) === '') {
|
||||
// Remove any possible leading slashes
|
||||
$urlParts['path'] = ltrim($urlParts['path'], '/');
|
||||
// Ensure that sitePath will have a "/" between path and sitePath
|
||||
if (str_starts_with($normalizedParams->getSitePath(), '/')) {
|
||||
$urlParts['path'] = '/' . $urlParts['path'];
|
||||
}
|
||||
$urlParts['path'] = rtrim($normalizedParams->getSitePath(), '/') . $urlParts['path'];
|
||||
}
|
||||
$isUrlModified = true;
|
||||
}
|
||||
// Override scheme:
|
||||
$forcedScheme = $configuration['forceAbsoluteUrl.']['scheme'] ?? null;
|
||||
if (!empty($forcedScheme) && $urlParts['scheme'] !== $forcedScheme) {
|
||||
$urlParts['scheme'] = $forcedScheme;
|
||||
$isUrlModified = true;
|
||||
}
|
||||
// Also ensure the path has a "/" at the beginning when concatenating everything else together
|
||||
if ($urlParts['path'] !== '') {
|
||||
$urlParts['path'] = '/' . ltrim($urlParts['path'], '/');
|
||||
$isUrlModified = true;
|
||||
}
|
||||
// Recreate the absolute URL:
|
||||
if ($isUrlModified) {
|
||||
$url = implode('', $urlParts);
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to a fallback method properly encoding HTML.
|
||||
*
|
||||
* @param string $originalLinkText the original string, if empty, the fallback link text
|
||||
* @param string $fallbackLinkText the string to be used.
|
||||
* @return string the final text
|
||||
*/
|
||||
protected function encodeFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
|
||||
{
|
||||
if ($originalLinkText !== '') {
|
||||
return $originalLinkText;
|
||||
}
|
||||
return htmlspecialchars($fallbackLinkText, ENT_QUOTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the value for target="..." in a typolink configuration
|
||||
*
|
||||
* @param array $conf the typolink configuration
|
||||
* @param string $name the key, usually "target", "extTarget" or "fileTarget"
|
||||
* @return string the value of the target attribute, if there is one
|
||||
*/
|
||||
protected function resolveTargetAttribute(array $conf, string $name, ?ContentObjectRenderer $contentObjectRenderer = null): string
|
||||
{
|
||||
$target = '';
|
||||
if (isset($conf[$name]) && $conf[$name] !== '') {
|
||||
$target = $conf[$name];
|
||||
} elseif (!($conf['directImageLink'] ?? false)) {
|
||||
$frontendTypoScriptConfigArray = $contentObjectRenderer ? $contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray() : [];
|
||||
switch ($name) {
|
||||
case 'extTarget':
|
||||
case 'fileTarget':
|
||||
$target = (string)($frontendTypoScriptConfigArray[$name] ?? '');
|
||||
break;
|
||||
case 'target':
|
||||
$target = (string)($frontendTypoScriptConfigArray['intTarget'] ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($conf[$name . '.']) && $conf[$name . '.']) {
|
||||
if ($contentObjectRenderer) {
|
||||
$target = (string)$contentObjectRenderer->stdWrap($target, $conf[$name . '.'] ?? []);
|
||||
}
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
|
||||
use TYPO3\CMS\Core\Http\ApplicationType;
|
||||
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
||||
use TYPO3\CMS\Core\Site\Entity\NullSite;
|
||||
use TYPO3\CMS\Core\TypoScript\PageTsConfig;
|
||||
use TYPO3\CMS\Core\TypoScript\PageTsConfigFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Event\BeforeDatabaseRecordLinkResolvedEvent;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to a database record
|
||||
*/
|
||||
readonly class DatabaseRecordLinkBuilder implements TypolinkBuilderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TcaSchemaFactory $schemaFactory,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
private FrontendInterface $runtimeCache,
|
||||
private TypoLinkCodecService $typoLinkCodecService,
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private PageRepository $pageRepository,
|
||||
) {}
|
||||
|
||||
public function buildLink(
|
||||
array $linkDetails,
|
||||
array $configuration,
|
||||
ServerRequestInterface $request,
|
||||
string $linkText = '',
|
||||
): LinkResultInterface {
|
||||
$pageTsConfig = $this->getPageTsConfig($request);
|
||||
$configurationKey = $linkDetails['identifier'] . '.';
|
||||
$typoScriptArray = $request->getAttribute('frontend.typoscript')?->getSetupArray() ?? [];
|
||||
$typoScriptLinkHandlerConfiguration = $typoScriptArray['config.']['recordLinks.'] ?? [];
|
||||
$linkHandlerConfiguration = $pageTsConfig['TCEMAIN.']['linkHandler.'] ?? [];
|
||||
|
||||
if (!isset($typoScriptLinkHandlerConfiguration[$configurationKey], $linkHandlerConfiguration[$configurationKey])) {
|
||||
throw new UnableToLinkException(
|
||||
'Configuration how to link "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.',
|
||||
1490989149,
|
||||
null,
|
||||
$linkText
|
||||
);
|
||||
}
|
||||
$typoScriptConfiguration = $typoScriptLinkHandlerConfiguration[$configurationKey]['typolink.'];
|
||||
$linkHandlerConfiguration = $linkHandlerConfiguration[$configurationKey]['configuration.'];
|
||||
$databaseTable = (string)($linkHandlerConfiguration['table'] ?? '');
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new BeforeDatabaseRecordLinkResolvedEvent(
|
||||
$linkDetails,
|
||||
$databaseTable,
|
||||
$typoScriptLinkHandlerConfiguration,
|
||||
$linkHandlerConfiguration,
|
||||
$request
|
||||
)
|
||||
);
|
||||
$record = $event->record;
|
||||
if ($record === null) {
|
||||
if ($typoScriptLinkHandlerConfiguration[$configurationKey]['forceLink'] ?? false) {
|
||||
$record = $this->pageRepository->getRawRecord($databaseTable, (int)$linkDetails['uid']);
|
||||
} else {
|
||||
$record = $this->pageRepository->checkRecord($databaseTable, (int)$linkDetails['uid']);
|
||||
$languageAspect = GeneralUtility::makeInstance(Context::class)->getAspect('language');
|
||||
if (is_array($record) && $this->schemaFactory->has($databaseTable)) {
|
||||
$schema = $this->schemaFactory->get($databaseTable);
|
||||
if ($schema->isLanguageAware()) {
|
||||
$languageField = $schema->getCapability(TcaSchemaCapability::Language)->getLanguageField(
|
||||
)->getName();
|
||||
$languageIdOfRecord = $record[$languageField];
|
||||
// If a record is already in a localized version OR if the record is set to "All Languages"
|
||||
// we allow the generation of the link
|
||||
if ($languageIdOfRecord === 0 && $languageAspect->doOverlays()) {
|
||||
$overlay = $this->pageRepository->getLanguageOverlay(
|
||||
$databaseTable,
|
||||
$record,
|
||||
$languageAspect
|
||||
);
|
||||
// If the record is not translated (overlays enabled), even though it should have been done
|
||||
// We avoid linking to it
|
||||
if (!isset($overlay['_LOCALIZED_UID'])) {
|
||||
$record = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($record === null) {
|
||||
throw new UnableToLinkException(
|
||||
'Record not found for "' . $linkDetails['typoLinkParameter'] . '" was not found, so "' . $linkText . '" was not linked.',
|
||||
1490989659,
|
||||
null,
|
||||
$linkText
|
||||
);
|
||||
}
|
||||
|
||||
// Unset the parameter part of the given TypoScript configuration while keeping
|
||||
// config that has been set in addition.
|
||||
unset($configuration['parameter.']);
|
||||
|
||||
$parameterFromDb = $this->typoLinkCodecService->decode((string)($configuration['parameter'] ?? ''));
|
||||
unset($parameterFromDb['url']);
|
||||
$parameterFromTypoScript = $this->typoLinkCodecService->decode((string)($typoScriptConfiguration['parameter'] ?? ''));
|
||||
$parameter = array_replace_recursive($parameterFromTypoScript, array_filter($parameterFromDb));
|
||||
$typoScriptConfiguration['parameter'] = $this->typoLinkCodecService->encode($parameter);
|
||||
|
||||
$typoScriptConfiguration = array_replace_recursive($configuration, $typoScriptConfiguration);
|
||||
|
||||
if (!empty($linkDetails['fragment'])) {
|
||||
$typoScriptConfiguration['section'] = $linkDetails['fragment'];
|
||||
}
|
||||
// Build the full link to the record by calling LinkFactory again ("inception")
|
||||
$localContentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
$localContentObjectRenderer->setRequest($request);
|
||||
$localContentObjectRenderer->start($record, $databaseTable);
|
||||
$localContentObjectRenderer->parameters = $request->getAttribute('currentContentObject')->parameters ?? [];
|
||||
return $localContentObjectRenderer->createLink($linkText, $typoScriptConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to calculate pageTsConfig in frontend scope, we can't use BackendUtility::getPagesTSconfig() here.
|
||||
*/
|
||||
protected function getPageTsConfig(ServerRequestInterface $request): array
|
||||
{
|
||||
if (!ApplicationType::fromRequest($request)->isFrontend()) {
|
||||
return [];
|
||||
}
|
||||
$pageInformation = $request->getAttribute('frontend.page.information');
|
||||
$id = $pageInformation->getId();
|
||||
$fullRootLine = $pageInformation->getRootLine();
|
||||
$pageTsConfig = $this->runtimeCache->get('pageTsConfig-' . $id);
|
||||
if ($pageTsConfig instanceof PageTsConfig) {
|
||||
return $pageTsConfig->getPageTsConfigArray();
|
||||
}
|
||||
ksort($fullRootLine);
|
||||
$site = $request->getAttribute('site') ?? new NullSite();
|
||||
$pageTsConfig = GeneralUtility::makeInstance(PageTsConfigFactory::class)->create($fullRootLine, $site);
|
||||
$this->runtimeCache->set('pageTsConfig-' . $id, $pageTsConfig);
|
||||
return $pageTsConfig->getPageTsConfigArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to an email address, also takes care of additional functionality for the time being
|
||||
* such as the infamous config.spamProtectedEmailAddresses option.
|
||||
*/
|
||||
class EmailLinkBuilder implements LoggerAwareInterface, TypolinkBuilderInterface
|
||||
{
|
||||
use DefaultJavaScriptAssetTrait;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly LinkService $linkService,
|
||||
) {}
|
||||
|
||||
public function buildLink(
|
||||
array $linkDetails,
|
||||
array $configuration,
|
||||
ServerRequestInterface $request,
|
||||
string $linkText = '',
|
||||
): LinkResultInterface {
|
||||
[$url, $linkText, $attributes] = $this->processEmailLink($linkDetails['email'], $linkText, $linkDetails, $request);
|
||||
return (new LinkResult(LinkService::TYPE_EMAIL, $url))
|
||||
->withTarget($linkDetails['target'] ?? '')
|
||||
->withLinkConfiguration($configuration)
|
||||
->withLinkText($linkText)
|
||||
->withAttributes($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a href attribute for given $mailAddress.
|
||||
* The function uses spamProtectEmailAddresses for encoding the mailto statement.
|
||||
* If spamProtectEmailAddresses is disabled, it'll just return a string like "mailto:user@example.tld".
|
||||
*
|
||||
* Returns an array with three items (numeric index)
|
||||
* #0: $mailToUrl (string), ready to be inserted into the href attribute of the <a> tag
|
||||
* #1: $linkText (string), content between starting and ending `<a>` tag
|
||||
* #2: $attributes (array<string, string>), additional attributes for `<a>` tag
|
||||
*
|
||||
* @param string $mailAddress Email address
|
||||
* @param string $linkText Link text, default will be the email address.
|
||||
* @return array{0: string, 1: string, 2: array<string, string>} A numerical array with three items
|
||||
* @internal this method is not part of TYPO3's public API
|
||||
*/
|
||||
public function processEmailLink(string $mailAddress, string $linkText, array $linkDetails, ServerRequestInterface $request): array
|
||||
{
|
||||
$linkText = $linkText ?: htmlspecialchars($mailAddress);
|
||||
$attributes = [];
|
||||
if ($linkDetails !== []) {
|
||||
// Ensure to add also additional query parameters to the string
|
||||
$mailToUrl = $this->linkService->asString($linkDetails);
|
||||
} else {
|
||||
$mailToUrl = 'mailto:' . $mailAddress;
|
||||
}
|
||||
|
||||
// no processing happened, therefore, the default processing kicks in
|
||||
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
||||
$spamProtectEmailAddresses = (int)($frontendTypoScriptConfigArray['spamProtectEmailAddresses'] ?? 0);
|
||||
$spamProtectEmailAddresses = MathUtility::forceIntegerInRange($spamProtectEmailAddresses, -10, 10);
|
||||
|
||||
if ($spamProtectEmailAddresses !== 0) {
|
||||
$mailToUrl = $this->encryptEmail($mailToUrl, $spamProtectEmailAddresses);
|
||||
$attributes = [
|
||||
'data-mailto-token' => $mailToUrl,
|
||||
'data-mailto-vector' => $spamProtectEmailAddresses,
|
||||
];
|
||||
$mailToUrl = '#';
|
||||
$this->addDefaultFrontendJavaScript($request);
|
||||
$atLabel = '(at)';
|
||||
if (($atLabelFromConfig = trim($frontendTypoScriptConfigArray['spamProtectEmailAddresses_atSubst'] ?? '')) !== '') {
|
||||
$atLabel = $atLabelFromConfig;
|
||||
}
|
||||
$spamProtectedMailAddress = str_replace('@', $atLabel, htmlspecialchars($mailAddress));
|
||||
if ($frontendTypoScriptConfigArray['spamProtectEmailAddresses_lastDotSubst'] ?? false) {
|
||||
$lastDotLabel = trim($frontendTypoScriptConfigArray['spamProtectEmailAddresses_lastDotSubst']);
|
||||
$lastDotLabel = $lastDotLabel ?: '(dot)';
|
||||
$spamProtectedMailAddress = preg_replace('/\\.([^\\.]+)$/', $lastDotLabel . '$1', $spamProtectedMailAddress);
|
||||
if ($spamProtectedMailAddress === null) {
|
||||
$this->logger->debug('Error replacing the last dot in email address "{email}"', ['email' => $spamProtectedMailAddress]);
|
||||
$spamProtectedMailAddress = '';
|
||||
}
|
||||
}
|
||||
$linkText = str_ireplace(htmlspecialchars($mailAddress), $spamProtectedMailAddress, $linkText);
|
||||
}
|
||||
|
||||
return [$mailToUrl, $linkText, $attributes];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption of email addresses for <A>-tags See the spam protection setup in TS 'config.'
|
||||
*
|
||||
* @param string $string Input string to en/decode: "mailto:some@example.com
|
||||
* @param int $offset a number between -10 and 10, taken from config.spamProtectEmailAddresses
|
||||
* @return string encoded version of $string
|
||||
*/
|
||||
protected function encryptEmail(string $string, int $offset): string
|
||||
{
|
||||
$out = '';
|
||||
// like str_rot13() but with a variable offset and a wider character range
|
||||
$len = strlen($string);
|
||||
for ($i = 0; $i < $len; $i++) {
|
||||
$charValue = ord($string[$i]);
|
||||
// 0-9 . , - + / :
|
||||
if ($charValue >= 43 && $charValue <= 58) {
|
||||
$out .= $this->encryptCharcode($charValue, 43, 58, $offset);
|
||||
} elseif ($charValue >= 64 && $charValue <= 90) {
|
||||
// A-Z @
|
||||
$out .= $this->encryptCharcode($charValue, 64, 90, $offset);
|
||||
} elseif ($charValue >= 97 && $charValue <= 122) {
|
||||
// a-z
|
||||
$out .= $this->encryptCharcode($charValue, 97, 122, $offset);
|
||||
} else {
|
||||
$out .= $string[$i];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption (or decryption) of a single character.
|
||||
* Within the given range the character is shifted with the supplied offset.
|
||||
*
|
||||
* @param int $n Ordinal of input character
|
||||
* @param int $start Start of range
|
||||
* @param int $end End of range
|
||||
* @param int $offset Offset
|
||||
* @return string encoded/decoded version of character
|
||||
*/
|
||||
protected function encryptCharcode(int $n, int $start, int $end, int $offset): string
|
||||
{
|
||||
$n = $n + $offset;
|
||||
if ($offset > 0 && $n > $end) {
|
||||
$n = $start + ($n - $end - 1);
|
||||
} elseif ($offset < 0 && $n < $start) {
|
||||
$n = $end - ($start - $n - 1);
|
||||
}
|
||||
return chr($n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to an external URL
|
||||
*/
|
||||
class ExternalUrlLinkBuilder extends AbstractTypolinkBuilder implements TypolinkBuilderInterface
|
||||
{
|
||||
public function buildLink(
|
||||
array $linkDetails,
|
||||
array $configuration,
|
||||
ServerRequestInterface $request,
|
||||
string $linkText = '',
|
||||
): LinkResultInterface {
|
||||
$url = $linkDetails['url'] ?? '';
|
||||
$target = $linkDetails['target'] ?? '';
|
||||
// issue https://forge.typo3.org/issues/101083 forces absolute path URLs
|
||||
// like `/path/some-file.png` to be handled as external URL, and that's
|
||||
// why the URL is forced to contain a fully qualified domain name as well
|
||||
$url = $this->forceAbsoluteUrl($url, $configuration, $request);
|
||||
$fallbackTarget = str_starts_with($url, '/') && !str_starts_with($url, '//') ? 'target' : 'extTarget';
|
||||
|
||||
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, $url);
|
||||
return (new LinkResult(LinkService::TYPE_URL, (string)$url))
|
||||
->withLinkConfiguration($configuration)
|
||||
->withTarget(
|
||||
$target ?: $this->resolveTargetAttribute($configuration, $fallbackTarget, $request->getAttribute('currentContentObject')),
|
||||
)
|
||||
->withLinkText($linkText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Resource\FileInterface;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to a folder or file
|
||||
*/
|
||||
class FileOrFolderLinkBuilder extends AbstractTypolinkBuilder implements TypolinkBuilderInterface
|
||||
{
|
||||
public function buildLink(
|
||||
array $linkDetails,
|
||||
array $configuration,
|
||||
ServerRequestInterface $request,
|
||||
string $linkText = '',
|
||||
): LinkResultInterface {
|
||||
$target = $linkDetails['target'] ?? '';
|
||||
$fileOrFolderObject = ($linkDetails['file'] ?? false) ?: ($linkDetails['folder'] ?? null);
|
||||
// check if the file exists or if a / is contained (same check as in detectLinkType)
|
||||
if (!($fileOrFolderObject instanceof FileInterface) && !($fileOrFolderObject instanceof Folder)) {
|
||||
throw new UnableToLinkException(
|
||||
'File "' . $linkDetails['typoLinkParameter'] . '" did not exist, so "' . $linkText . '" was not linked.',
|
||||
1490989449,
|
||||
null,
|
||||
$linkText
|
||||
);
|
||||
}
|
||||
$linkLocation = $fileOrFolderObject->getPublicUrl();
|
||||
if ($linkLocation === null) {
|
||||
// set the linkLocation to an empty string if null,
|
||||
// so it does not collide with the various string functions
|
||||
$linkLocation = '';
|
||||
}
|
||||
// Setting title if blank value to link
|
||||
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
|
||||
$url = $linkLocation;
|
||||
if (!empty($linkDetails['fragment'])) {
|
||||
$url .= '#' . $linkDetails['fragment'];
|
||||
}
|
||||
return (new LinkResult($linkDetails['type'], $this->forceAbsoluteUrl($url, $configuration, $request)))
|
||||
->withLinkConfiguration($configuration)
|
||||
->withTarget($target ?: $this->resolveTargetAttribute($configuration, 'fileTarget', $request->getAttribute('currentContentObject')))
|
||||
->withLinkText($linkText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\Page\FrontendUrlPrefix;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to a file (relative to fileadmin/ or something)
|
||||
* or otherwise detects as an external URL
|
||||
*/
|
||||
class LegacyLinkBuilder extends AbstractTypolinkBuilder implements TypolinkBuilderInterface
|
||||
{
|
||||
public function buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = ''): LinkResultInterface
|
||||
{
|
||||
$target = $linkDetails['target'] ?? '';
|
||||
if ($linkDetails['file'] ?? false) {
|
||||
$linkDetails['type'] = LinkService::TYPE_FILE;
|
||||
$linkLocation = $linkDetails['file'];
|
||||
// Setting title if blank value to link
|
||||
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, rawurldecode($linkLocation));
|
||||
$absRefPrefix = GeneralUtility::makeInstance(FrontendUrlPrefix::class)->getUrlPrefix($request);
|
||||
$linkLocation = (!str_starts_with($linkLocation, '/') ? $absRefPrefix : '') . $linkLocation;
|
||||
$url = $linkLocation;
|
||||
$url = $this->forceAbsoluteUrl($url, $configuration, $request);
|
||||
$target = $target ?: $this->resolveTargetAttribute($configuration, 'fileTarget', $request->getAttribute('currentContentObject'));
|
||||
} elseif ($linkDetails['url'] ?? false) {
|
||||
$linkDetails['type'] = LinkService::TYPE_URL;
|
||||
$target = $target ?: $this->resolveTargetAttribute($configuration, 'extTarget', $request->getAttribute('currentContentObject'));
|
||||
$linkText = $this->encodeFallbackLinkTextIfLinkTextIsEmpty($linkText, $linkDetails['url']);
|
||||
$url = $linkDetails['url'];
|
||||
} else {
|
||||
throw new UnableToLinkException('Unknown link detected, so ' . $linkText . ' was not linked.', 1490990031, null, $linkText);
|
||||
}
|
||||
return (new LinkResult((string)$linkDetails['type'], (string)$url))->withTarget($target)->withLinkConfiguration($configuration)->withLinkText($linkText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
|
||||
use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
||||
use TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent;
|
||||
|
||||
/**
|
||||
* Main class for generating any kind of frontend links.
|
||||
* Contains all logic for the infamous typolink() functionality.
|
||||
*/
|
||||
#[Autoconfigure(public: true)]
|
||||
readonly class LinkFactory
|
||||
{
|
||||
use DefaultJavaScriptAssetTrait;
|
||||
|
||||
public function __construct(
|
||||
protected LinkService $linkService,
|
||||
protected EventDispatcherInterface $eventDispatcher,
|
||||
protected TypoLinkCodecService $typoLinkCodecService,
|
||||
#[Autowire(service: 'cache.runtime')]
|
||||
protected FrontendInterface $runtimeCache,
|
||||
protected SiteFinder $siteFinder,
|
||||
protected LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main method to create links from typolink strings and configuration.
|
||||
* @throws UnableToLinkException
|
||||
*/
|
||||
public function create(string $linkText, array $linkConfiguration, ContentObjectRenderer $contentObjectRenderer): LinkResultInterface
|
||||
{
|
||||
if (isset($linkConfiguration['parameter.'])) {
|
||||
// Evaluate "parameter." stdWrap but keep additional information (like target, class and title)
|
||||
$linkParameterParts = $this->typoLinkCodecService->decode((string)($linkConfiguration['parameter'] ?? ''));
|
||||
$modifiedLinkParameterString = $contentObjectRenderer->stdWrap($linkParameterParts['url'], $linkConfiguration['parameter.']);
|
||||
// As the stdWrap result might contain target etc. as well again (".field = header_link")
|
||||
// the result is then taken from the stdWrap and overridden if the value is not empty.
|
||||
$modifiedLinkParameterParts = $this->typoLinkCodecService->decode((string)($modifiedLinkParameterString ?? ''));
|
||||
$linkParameterParts = array_replace($linkParameterParts, array_filter($modifiedLinkParameterParts, static fn($value) => trim((string)$value) !== ''));
|
||||
$linkParameter = $this->typoLinkCodecService->encode($linkParameterParts);
|
||||
} else {
|
||||
$linkParameter = trim((string)($linkConfiguration['parameter'] ?? ''));
|
||||
}
|
||||
try {
|
||||
[$linkParameter, $target, $classList, $title, $rel, $download] = $this->resolveTypolinkParameterString($linkParameter, $linkConfiguration);
|
||||
} catch (UnableToLinkException $e) {
|
||||
$this->logger->warning($e->getMessage(), ['linkConfiguration' => $linkConfiguration]);
|
||||
throw $e;
|
||||
}
|
||||
$linkDetails = $this->resolveLinkDetails($linkParameter, $linkConfiguration, $contentObjectRenderer);
|
||||
if ($linkDetails === null) {
|
||||
throw new UnableToLinkException('Could not resolve link details from ' . $linkParameter, 1642001442, null, $linkText);
|
||||
}
|
||||
|
||||
$linkResult = $this->buildLinkResult($linkText, $linkDetails, $target, $linkConfiguration, $contentObjectRenderer);
|
||||
|
||||
// Enrich the link result with resolved attributes and run post processing
|
||||
$linkResult = $this->addAdditionalAnchorTagAttributes($linkResult, $linkConfiguration, $contentObjectRenderer);
|
||||
if ($rel !== '') {
|
||||
$linkResult = $linkResult->withAttribute('rel', $rel);
|
||||
}
|
||||
|
||||
// Check, if the target is coded as a JS open window link:
|
||||
$linkResult = $this->addJavaScriptOpenWindowInformationAttributes($linkResult, $linkConfiguration, $contentObjectRenderer);
|
||||
$linkResult = $this->addSecurityRelValues($linkResult, $contentObjectRenderer);
|
||||
// Title attribute, will override any title attribute from ->addAdditionalAnchorTagAttributes()
|
||||
$title = $title ?: trim((string)$contentObjectRenderer->stdWrapValue('title', $linkConfiguration));
|
||||
if (!empty($title)) {
|
||||
$linkResult = $linkResult->withAttribute('title', $title);
|
||||
}
|
||||
// Class attribute, will override any class attribute from ->addAdditionalAnchorTagAttributes()
|
||||
if (!empty($classList)) {
|
||||
$linkResult = $linkResult->withAttribute('class', $classList);
|
||||
}
|
||||
// Download attribute
|
||||
if ($download !== '') {
|
||||
$linkResult = $linkResult->withAttribute('download', $download === 'true' ? '' : $download);
|
||||
}
|
||||
|
||||
if ($linkConfiguration['userFunc'] ?? false) {
|
||||
$linkResult = $contentObjectRenderer->callUserFunction($linkConfiguration['userFunc'], $linkConfiguration['userFunc.'] ?? [], $linkResult);
|
||||
if (!($linkResult instanceof LinkResultInterface)) {
|
||||
throw new UnableToLinkException('Calling typolink.userFunc resulted in not returning a valid typolink', 1642171035, null, $linkText);
|
||||
}
|
||||
}
|
||||
|
||||
$event = new AfterLinkIsGeneratedEvent($linkResult, $contentObjectRenderer, $linkConfiguration);
|
||||
$event = $this->eventDispatcher->dispatch($event);
|
||||
return $event->getLinkResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a link result for a given URL (usually something like "19 _blank css-class "testtitle with whitespace" &X=y").
|
||||
* Helpful if you want to create any kind of URL (also possible in TYPO3 Backend).
|
||||
*/
|
||||
public function createUri(string $urlParameter, ?ContentObjectRenderer $contentObjectRenderer = null): LinkResultInterface
|
||||
{
|
||||
if ($contentObjectRenderer === null) {
|
||||
$contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
||||
// @todo: LinkFactory needs the request to determine fallback page uid when link config has none.
|
||||
$contentObjectRenderer->setRequest($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
return $this->create('', ['parameter' => $urlParameter], $contentObjectRenderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the linkDetails + configuration to the concrete typolink Builder (page, email etc)
|
||||
* and returns a LinkResultInterface.
|
||||
*/
|
||||
protected function buildLinkResult(string $linkText, array $linkDetails, string $target, array $linkConfiguration, ContentObjectRenderer $contentObjectRenderer): LinkResultInterface
|
||||
{
|
||||
if (isset($linkDetails['type'])) {
|
||||
$builderType = $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']] ?? null;
|
||||
} else {
|
||||
$builderType = null;
|
||||
}
|
||||
if ($builderType && is_subclass_of($builderType, TypolinkBuilderInterface::class)) {
|
||||
/** @var TypolinkBuilderInterface $linkBuilder */
|
||||
$linkBuilder = GeneralUtility::makeInstance($builderType);
|
||||
$linkDetails['target'] = $target;
|
||||
try {
|
||||
$request = $contentObjectRenderer->getRequest();
|
||||
$request = $request->withAttribute('currentContentObject', $contentObjectRenderer);
|
||||
return $linkBuilder->buildLink($linkDetails, $linkConfiguration, $request, $linkText);
|
||||
} catch (UnableToLinkException $e) {
|
||||
$this->logger->debug('Unable to link "{text}"', [
|
||||
'text' => $e->getLinkText(),
|
||||
'exception' => $e,
|
||||
]);
|
||||
// Only return the link text directly
|
||||
throw $e;
|
||||
}
|
||||
} elseif (isset($linkDetails['url'])) {
|
||||
$linkResult = new LinkResult($linkDetails['type'], $linkDetails['url']);
|
||||
return $linkResult
|
||||
->withTarget($target)
|
||||
->withLinkConfiguration($linkConfiguration)
|
||||
->withLinkText($linkText);
|
||||
}
|
||||
throw new UnableToLinkException('No suitable link handler for resolving ' . $linkDetails['typoLinkParameter'], 1642000232, null, $linkText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates $linkDetails out of the link parameter so the concrete LinkBuilder can be resolved.
|
||||
*/
|
||||
protected function resolveLinkDetails(string $linkParameter, array $linkConfiguration, ContentObjectRenderer $contentObjectRenderer): ?array
|
||||
{
|
||||
$linkDetails = null;
|
||||
if (!$linkParameter) {
|
||||
// Support anchors without href value if id or name attribute is present.
|
||||
$aTagParams = (string)$contentObjectRenderer->stdWrapValue('ATagParams', $linkConfiguration);
|
||||
$aTagParams = GeneralUtility::get_tag_attributes($aTagParams);
|
||||
// If it looks like an anchor tag, render it anyway
|
||||
if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
|
||||
$linkDetails = [
|
||||
'type' => LinkService::TYPE_INPAGE,
|
||||
'url' => '',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Detecting kind of link and resolve all necessary parameters
|
||||
try {
|
||||
$linkDetails = $this->linkService->resolve($linkParameter);
|
||||
} catch (UnknownLinkHandlerException|InvalidPathException $exception) {
|
||||
$this->logger->warning('The link could not be generated', ['exception' => $exception]);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (is_array($linkDetails)) {
|
||||
$linkDetails['typoLinkParameter'] = $linkParameter;
|
||||
}
|
||||
return $linkDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the magic to split the full "typolink" string like "15,13 _blank myclass &more=1" into separate parts
|
||||
*
|
||||
* @param string $mixedLinkParameter destination data like "15,13 _blank myclass &more=1" used to create the link
|
||||
* @param array $linkConfiguration TypoScript configuration
|
||||
*/
|
||||
protected function resolveTypolinkParameterString(string $mixedLinkParameter, array &$linkConfiguration = []): array
|
||||
{
|
||||
$linkParameterParts = $this->typoLinkCodecService->decode($mixedLinkParameter);
|
||||
[$linkHandlerKeyword] = explode(':', $linkParameterParts['url'], 2);
|
||||
if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', (string)$linkHandlerKeyword)), ['javascript', 'data'], true)) {
|
||||
// Disallow insecure scheme's like javascript: or data:
|
||||
throw new UnableToLinkException('Insecure scheme for linking detected with "' . $mixedLinkParameter . "'", 1641986533);
|
||||
}
|
||||
|
||||
// additional parameters that need to be set
|
||||
if ($linkParameterParts['additionalParams'] !== '') {
|
||||
$forceParams = $linkParameterParts['additionalParams'];
|
||||
// params value
|
||||
$linkConfiguration['additionalParams'] = ($linkConfiguration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
|
||||
}
|
||||
|
||||
return [
|
||||
$linkParameterParts['url'],
|
||||
$linkParameterParts['target'],
|
||||
$linkParameterParts['class'],
|
||||
$linkParameterParts['title'],
|
||||
$linkParameterParts['rel'] ?? '',
|
||||
$linkParameterParts['download'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
protected function addJavaScriptOpenWindowInformationAttributes(LinkResultInterface $linkResult, array $linkConfiguration, ContentObjectRenderer $contentObjectRenderer): LinkResultInterface
|
||||
{
|
||||
$JSwindowParts = [];
|
||||
if ($linkResult->getTarget() && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $linkResult->getTarget(), $JSwindowParts)) {
|
||||
// Take all pre-configured and inserted parameters and compile parameter list, including width+height:
|
||||
$JSwindow_tempParamsArr = GeneralUtility::trimExplode(',', strtolower(($linkConfiguration['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
|
||||
$JSwindow_paramsArr = [];
|
||||
$target = $linkConfiguration['target'] ?? 'FEopenLink';
|
||||
foreach ($JSwindow_tempParamsArr as $JSv) {
|
||||
[$JSp, $JSv] = explode('=', $JSv, 2);
|
||||
// If the target is set as JS param, this is extracted
|
||||
if ($JSp === 'target') {
|
||||
$target = $JSv;
|
||||
} else {
|
||||
$JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
|
||||
}
|
||||
}
|
||||
// Add width/height:
|
||||
$JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
|
||||
$JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
|
||||
|
||||
$JSwindowAttrs = [
|
||||
'data-window-url' => $linkResult->getUrl(),
|
||||
'data-window-target' => $target,
|
||||
'data-window-features' => implode(',', $JSwindow_paramsArr),
|
||||
];
|
||||
$linkResult = $linkResult->withAttributes($JSwindowAttrs);
|
||||
$linkResult = $linkResult->withAttribute('target', $target);
|
||||
$this->addDefaultFrontendJavaScript($contentObjectRenderer->getRequest());
|
||||
}
|
||||
return $linkResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstraction method to add parameters to an A tag.
|
||||
* Uses the ATagParams property, also includes the global TypoScript config.ATagParams
|
||||
*/
|
||||
protected function addAdditionalAnchorTagAttributes(LinkResultInterface $linkResult, array $linkConfiguration, ContentObjectRenderer $contentObjectRenderer): LinkResultInterface
|
||||
{
|
||||
$request = $contentObjectRenderer->getRequest();
|
||||
$frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
|
||||
$aTagParams = $contentObjectRenderer->stdWrapValue('ATagParams', $linkConfiguration);
|
||||
// Add the global config.ATagParams
|
||||
$globalParams = $frontendTypoScriptConfigArray['ATagParams'] ?? '';
|
||||
$aTagParams = trim($globalParams . ' ' . $aTagParams);
|
||||
if (!empty($aTagParams)) {
|
||||
// Decode entities here, as they are doubly escaped again when using HTML output
|
||||
$aTagParams = GeneralUtility::get_tag_attributes($aTagParams, true);
|
||||
// Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
|
||||
unset($aTagParams['href']);
|
||||
$linkResult = $linkResult->withAttributes($aTagParams);
|
||||
}
|
||||
return $linkResult;
|
||||
}
|
||||
|
||||
protected function addSecurityRelValues(LinkResultInterface $linkResult, ContentObjectRenderer $contentObjectRenderer): LinkResultInterface
|
||||
{
|
||||
$target = (string)($linkResult->getTarget() ?: $linkResult->getAttribute('data-window-target'));
|
||||
if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($linkResult->getUrl(), $contentObjectRenderer->getRequest())) {
|
||||
return $linkResult;
|
||||
}
|
||||
|
||||
// build array of existing rel attribute values
|
||||
if ($linkResult->getAttribute('rel') !== null) {
|
||||
$relAttributeArray = GeneralUtility::trimExplode(' ', $linkResult->getAttribute('rel'));
|
||||
} else {
|
||||
$relAttributeArray = [];
|
||||
}
|
||||
|
||||
// neither "noopener" nor "noreferrer" exists
|
||||
if (!array_intersect(['noopener', 'noreferrer'], $relAttributeArray)) {
|
||||
$typoScriptConfigArray = $contentObjectRenderer->getRequest()->getAttribute('frontend.typoscript')?->getConfigArray();
|
||||
if (isset($typoScriptConfigArray['linkSecurityRelValue']) && strtolower($typoScriptConfigArray['linkSecurityRelValue']) === 'noopener') {
|
||||
$relAttributeArray[] = 'noopener';
|
||||
} else {
|
||||
$relAttributeArray[] = 'noreferrer';
|
||||
}
|
||||
}
|
||||
|
||||
return $linkResult->withAttribute('rel', implode(' ', $relAttributeArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given url is an internal url.
|
||||
*
|
||||
* It will check the host part only, against all configured sites
|
||||
* whether the given host is any. If so, the url is considered internal.
|
||||
*
|
||||
* Note: It would be good to move this to EXT:core/Classes/Site which accepts also a PSR-7 request and
|
||||
* also accepts a PSR-7 Uri.
|
||||
*/
|
||||
protected function isInternalUrl(string $url, ServerRequestInterface $request): bool
|
||||
{
|
||||
$parsedUrl = parse_url($url);
|
||||
$foundDomains = 0;
|
||||
if (!isset($parsedUrl['host'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
|
||||
|
||||
if ($this->runtimeCache->has($cacheIdentifier) === false) {
|
||||
foreach ($this->siteFinder->getAllSites() as $site) {
|
||||
if ($site->getBase()->getHost() === $parsedUrl['host']) {
|
||||
++$foundDomains;
|
||||
break;
|
||||
}
|
||||
if ($site->getBase()->getHost() === '' && GeneralUtility::isOnCurrentHost($url, $request)) {
|
||||
++$foundDomains;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$this->runtimeCache->set($cacheIdentifier, $foundDomains > 0);
|
||||
}
|
||||
|
||||
return (bool)$this->runtimeCache->get($cacheIdentifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* This class represents a created link to a resource (page, email etc.), coming from LinkService.
|
||||
* After it was executed by the LinkBuilders (mostly in Frontend) after it is called from Typolink.
|
||||
*/
|
||||
class LinkResult implements LinkResultInterface, \Stringable, \JsonSerializable
|
||||
{
|
||||
public const STRING_CAST_HTML = 1;
|
||||
public const STRING_CAST_JSON = 2;
|
||||
|
||||
protected string $type = LinkService::TYPE_UNKNOWN;
|
||||
protected string $url;
|
||||
protected string $target = '';
|
||||
protected array $additionalAttributes = [];
|
||||
protected ?string $linkText = null;
|
||||
protected array $linkConfiguration = [];
|
||||
protected int $flags = self::STRING_CAST_HTML;
|
||||
|
||||
/**
|
||||
* Use this method to create a new LinkResult for a specific output format (HTML or JSON)
|
||||
*/
|
||||
public static function adapt(LinkResultInterface $other, int $flags = self::STRING_CAST_HTML): self
|
||||
{
|
||||
$target = $other;
|
||||
if (!$target instanceof self) {
|
||||
$target = GeneralUtility::makeInstance(self::class, $other->getType(), $other->getUrl());
|
||||
$target->target = $other->getTarget();
|
||||
$target->additionalAttributes = $target->filterAdditionalAttributes($other->getAttributes());
|
||||
$target->linkText = $other->getLinkText();
|
||||
$target->linkConfiguration = $other->getLinkConfiguration();
|
||||
}
|
||||
return $target->withFlags($flags);
|
||||
}
|
||||
|
||||
public function __construct(string $type, string $url)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getTarget(): string
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function withTarget(string $target): self
|
||||
{
|
||||
$newObject = clone $this;
|
||||
$newObject->target = $target;
|
||||
return $newObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getLinkConfiguration(): array
|
||||
{
|
||||
return $this->linkConfiguration;
|
||||
}
|
||||
|
||||
public function withLinkConfiguration(array $configuration): self
|
||||
{
|
||||
$newObject = clone $this;
|
||||
$newObject->linkConfiguration = $configuration;
|
||||
return $newObject;
|
||||
}
|
||||
|
||||
public function withLinkText(string $linkText): self
|
||||
{
|
||||
$newObject = clone $this;
|
||||
$newObject->linkText = $linkText;
|
||||
return $newObject;
|
||||
}
|
||||
|
||||
public function getLinkText(): ?string
|
||||
{
|
||||
return $this->linkText;
|
||||
}
|
||||
|
||||
public function withAttributes(array $additionalAttributes, bool $resetExistingAttributes = false): self
|
||||
{
|
||||
$newObject = clone $this;
|
||||
if ($resetExistingAttributes) {
|
||||
$newObject->additionalAttributes = [];
|
||||
$newObject->url = '';
|
||||
$newObject->target = '';
|
||||
}
|
||||
foreach ($additionalAttributes as $attributeName => $attributeValue) {
|
||||
switch ($attributeName) {
|
||||
case 'href':
|
||||
$newObject->url = $attributeValue;
|
||||
break;
|
||||
case 'target':
|
||||
$newObject->target = $attributeValue;
|
||||
break;
|
||||
}
|
||||
if ($attributeValue !== null) {
|
||||
$newObject->additionalAttributes[$attributeName] = $attributeValue;
|
||||
} else {
|
||||
unset($newObject->additionalAttributes[$attributeName]);
|
||||
}
|
||||
}
|
||||
return $newObject;
|
||||
}
|
||||
|
||||
public function withAttribute(string $attributeName, ?string $attributeValue): self
|
||||
{
|
||||
$newObject = clone $this;
|
||||
switch ($attributeName) {
|
||||
case 'href':
|
||||
$newObject->url = $attributeValue ?? '';
|
||||
break;
|
||||
case 'target':
|
||||
$newObject->target = $attributeValue ?? '';
|
||||
break;
|
||||
default:
|
||||
if ($attributeValue !== null) {
|
||||
$newObject->additionalAttributes[$attributeName] = $attributeValue;
|
||||
} else {
|
||||
unset($newObject->additionalAttributes[$attributeName]);
|
||||
}
|
||||
}
|
||||
return $newObject;
|
||||
}
|
||||
|
||||
public function hasAttribute(string $attributeName): bool
|
||||
{
|
||||
switch ($attributeName) {
|
||||
case 'href':
|
||||
return $this->url !== '';
|
||||
case 'target':
|
||||
return $this->target !== '';
|
||||
default:
|
||||
return isset($this->additionalAttributes[$attributeName]);
|
||||
}
|
||||
}
|
||||
public function getAttribute(string $attributeName): ?string
|
||||
{
|
||||
switch ($attributeName) {
|
||||
case 'href':
|
||||
return $this->url;
|
||||
case 'target':
|
||||
return $this->target;
|
||||
default:
|
||||
return $this->additionalAttributes[$attributeName] ?? null;
|
||||
}
|
||||
}
|
||||
public function getAttributes(): array
|
||||
{
|
||||
$attributes = [];
|
||||
if ($this->url) {
|
||||
$attributes['href'] = $this->url;
|
||||
}
|
||||
if ($this->target) {
|
||||
$attributes['target'] = $this->target;
|
||||
}
|
||||
return array_merge($attributes, $this->additionalAttributes);
|
||||
}
|
||||
|
||||
public function withFlags(int $flags): self
|
||||
{
|
||||
if ($flags !== self::STRING_CAST_HTML && $flags !== self::STRING_CAST_JSON) {
|
||||
$flags = self::STRING_CAST_HTML;
|
||||
}
|
||||
if ($this->flags === $flags) {
|
||||
return $this;
|
||||
}
|
||||
$target = clone $this;
|
||||
$target->flags = $flags;
|
||||
return $target;
|
||||
}
|
||||
|
||||
protected function filterAdditionalAttributes(array $attributes): array
|
||||
{
|
||||
return array_filter(
|
||||
$attributes,
|
||||
static fn(string $key) => !in_array($key, ['href', 'target', 'class', 'title'], true),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{href: ?string, target: ?string, class: ?string, title: ?string, linkText: ?string, additionalAttributes: array}
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'href' => $this->url ?: null,
|
||||
'target' => $this->target ?: null,
|
||||
'class' => $this->getAttribute('class') ?: null,
|
||||
'title' => $this->getAttribute('title') ?: null,
|
||||
'linkText' => $this->linkText ?: null,
|
||||
'additionalAttributes' => $this->filterAdditionalAttributes($this->getAttributes()),
|
||||
];
|
||||
}
|
||||
|
||||
public function getHtml(): string
|
||||
{
|
||||
return sprintf(
|
||||
'<a %s>%s</a>',
|
||||
GeneralUtility::implodeAttributes($this->getAttributes(), true, true),
|
||||
$this->linkText
|
||||
);
|
||||
}
|
||||
|
||||
public function getJson(): string
|
||||
{
|
||||
try {
|
||||
return json_encode($this, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->flags === self::STRING_CAST_HTML) {
|
||||
return $this->getHtml();
|
||||
}
|
||||
if ($this->flags === self::STRING_CAST_JSON) {
|
||||
return $this->getJson();
|
||||
}
|
||||
throw new \LogicException('Unsupported flags assignment', 1666024513);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
/**
|
||||
* Interface representing a created link to any type (page, file etc).
|
||||
*/
|
||||
interface LinkResultInterface
|
||||
{
|
||||
public function getUrl(): string;
|
||||
|
||||
public function getType(): string;
|
||||
|
||||
public function getTarget(): string;
|
||||
|
||||
public function getLinkConfiguration(): array;
|
||||
|
||||
public function getLinkText(): ?string;
|
||||
|
||||
public function withLinkText(string $linkText): self;
|
||||
|
||||
public function withTarget(string $target): self;
|
||||
|
||||
public function withAttributes(array $additionalAttributes, bool $resetExistingAttributes = false): self;
|
||||
public function withAttribute(string $attributeName, ?string $attributeValue): self;
|
||||
public function hasAttribute(string $attributeName): bool;
|
||||
public function getAttribute(string $attributeName): ?string;
|
||||
public function getAttributes(): array;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Class to calculate so-called "linkVars", which is a TypoScript setting
|
||||
* to always append these query parameters (if available in an existing request)
|
||||
* to a URL when using TypoLink for pages.
|
||||
*/
|
||||
readonly class LinkVarsCalculator
|
||||
{
|
||||
/**
|
||||
* Calculates and sets the internal linkVars based upon the current request's GET parameters
|
||||
* and the setting "config.linkVars".
|
||||
*/
|
||||
public function getAllowedLinkVarsFromRequest(string $linkVarsSetting, array $queryParams, Context $context): string
|
||||
{
|
||||
$calculatedLinkVars = '';
|
||||
$adminCommand = $queryParams['ADMCMD_prev'] ?? '';
|
||||
$isBackendUserLoggedIn = $context->getAspect('backend.user')->isLoggedIn();
|
||||
|
||||
// This allows to keep the current logged-in workspace when navigating through the Frontend from a Backend link, and keep the logged-in state
|
||||
if (($adminCommand === 'LIVE' || $adminCommand === 'IGNORE') && $isBackendUserLoggedIn) {
|
||||
$linkVarsSetting = ltrim($linkVarsSetting . ',ADMCMD_prev', ',');
|
||||
}
|
||||
// This allows to keep the "ADMCMD_simUser" parameter when navigating through the Frontend from a Backend link, and keep the logged-in state
|
||||
if (!empty($queryParams['ADMCMD_simUser']) && $isBackendUserLoggedIn) {
|
||||
$linkVarsSetting = ltrim($linkVarsSetting . ',ADMCMD_simUser', ',');
|
||||
}
|
||||
// This allows to keep the "ADMCMD_simUser" parameter when navigating through the Frontend from a Backend link, and keep the logged-in state
|
||||
if (!empty($queryParams['ADMCMD_simTime']) && $isBackendUserLoggedIn) {
|
||||
$linkVarsSetting = ltrim($linkVarsSetting . ',ADMCMD_simTime', ',');
|
||||
}
|
||||
|
||||
if (empty($linkVarsSetting)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$linkVars = $this->splitLinkVarsString($linkVarsSetting);
|
||||
if (empty($linkVars)) {
|
||||
return '';
|
||||
}
|
||||
foreach ($linkVars as $linkVar) {
|
||||
$test = '';
|
||||
if (preg_match('/^(.*)\\((.+)\\)$/', $linkVar, $match)) {
|
||||
$linkVar = trim($match[1]);
|
||||
$test = trim($match[2]);
|
||||
}
|
||||
|
||||
$keys = explode('|', $linkVar);
|
||||
$numberOfLevels = count($keys);
|
||||
$rootKey = trim($keys[0]);
|
||||
if (!isset($queryParams[$rootKey])) {
|
||||
continue;
|
||||
}
|
||||
$value = $queryParams[$rootKey];
|
||||
for ($i = 1; $i < $numberOfLevels; $i++) {
|
||||
$currentKey = trim($keys[$i]);
|
||||
if (isset($value[$currentKey])) {
|
||||
$value = $value[$currentKey];
|
||||
} else {
|
||||
$value = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($value !== false) {
|
||||
$parameterName = $keys[0];
|
||||
for ($i = 1; $i < $numberOfLevels; $i++) {
|
||||
$parameterName .= '[' . $keys[$i] . ']';
|
||||
}
|
||||
if (!is_array($value)) {
|
||||
$temp = rawurlencode((string)$value);
|
||||
if ($test !== '' && !$this->isAllowedLinkVarValue($temp, $test)) {
|
||||
// Error: This value was not allowed for this key
|
||||
continue;
|
||||
}
|
||||
$value = '&' . $parameterName . '=' . $temp;
|
||||
} else {
|
||||
if ($test !== '' && $test !== 'array') {
|
||||
// Error: This key must not be an array!
|
||||
continue;
|
||||
}
|
||||
$value = HttpUtility::buildQueryString([$parameterName => $value], '&');
|
||||
}
|
||||
$calculatedLinkVars .= $value;
|
||||
}
|
||||
}
|
||||
return $calculatedLinkVars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the link vars string by "," but not if the "," is inside of braces
|
||||
*/
|
||||
protected function splitLinkVarsString(string $string): array
|
||||
{
|
||||
$tempCommaReplacementString = '###KASPER###';
|
||||
|
||||
// replace every "," wrapped in "()" by a "unique" string
|
||||
$string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', static function ($result) use ($tempCommaReplacementString) {
|
||||
return str_replace(',', $tempCommaReplacementString, $result[0]);
|
||||
}, $string) ?? '';
|
||||
|
||||
$string = GeneralUtility::trimExplode(',', $string);
|
||||
|
||||
// replace all "unique" strings back to ","
|
||||
return str_replace($tempCommaReplacementString, ',', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the value defined in "config.linkVars" contains an allowed value.
|
||||
* Otherwise, return FALSE which means the value will not be added to any links.
|
||||
*
|
||||
* @param string $haystack The string in which to find $value
|
||||
* @param string $value The string to find in $haystack
|
||||
* @return bool Returns TRUE if $value matches or is found in $haystack
|
||||
*/
|
||||
protected function isAllowedLinkVarValue(string $haystack, string $value): bool
|
||||
{
|
||||
$isAllowed = false;
|
||||
// Integer
|
||||
if ($value === 'int' || $value === 'integer') {
|
||||
if (MathUtility::canBeInterpretedAsInteger($haystack)) {
|
||||
$isAllowed = true;
|
||||
}
|
||||
} elseif (preg_match('/^\\/.+\\/[imsxeADSUXu]*$/', $value)) {
|
||||
// Regular expression, only "//" is allowed as delimiter
|
||||
if (@preg_match($value, $haystack)) {
|
||||
$isAllowed = true;
|
||||
}
|
||||
} elseif (str_contains($value, '-')) {
|
||||
// Range
|
||||
if (MathUtility::canBeInterpretedAsInteger($haystack)) {
|
||||
$range = explode('-', $value);
|
||||
if ($range[0] <= $haystack && $range[1] >= $haystack) {
|
||||
$isAllowed = true;
|
||||
}
|
||||
}
|
||||
} elseif (str_contains($value, '|')) {
|
||||
// List
|
||||
// Trim the input
|
||||
$haystack = str_replace(' ', '', $haystack);
|
||||
if (str_contains('|' . $value . '|', '|' . $haystack . '|')) {
|
||||
$isAllowed = true;
|
||||
}
|
||||
} elseif ($value === $haystack) {
|
||||
// String comparison
|
||||
$isAllowed = true;
|
||||
}
|
||||
return $isAllowed;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Builds a TypoLink to a telephone number
|
||||
*/
|
||||
class TelephoneLinkBuilder implements TypolinkBuilderInterface
|
||||
{
|
||||
public function buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = ''): LinkResultInterface
|
||||
{
|
||||
$linkText = $linkText ?: $linkDetails['telephone'] ?? '';
|
||||
return (new LinkResult($linkDetails['type'], $linkDetails['typoLinkParameter']))->withLinkConfiguration($configuration)->withLinkText($linkText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Used to generate a link to a page or file, an external URL or any other protocol
|
||||
* in the frontend or backend.
|
||||
* The actual resolving of the Link happens in LinkFactory
|
||||
*/
|
||||
interface TypolinkBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @param array $linkDetails parsed link details by the LinkService
|
||||
* @param array $configuration the TypoLink configuration array
|
||||
* @param string $linkText the link text
|
||||
* @throws UnableToLinkException
|
||||
*/
|
||||
public function buildLink(array $linkDetails, array $configuration, ServerRequestInterface $request, string $linkText = ''): LinkResultInterface;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Frontend\Typolink;
|
||||
|
||||
use TYPO3\CMS\Frontend\Exception;
|
||||
|
||||
/**
|
||||
* Exception which is thrown when a link could not be set
|
||||
*/
|
||||
class UnableToLinkException extends Exception
|
||||
{
|
||||
public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, protected string $linkText = '')
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the link text when the link could not been set
|
||||
*/
|
||||
public function getLinkText(): string
|
||||
{
|
||||
return $this->linkText;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user