Your IP : 216.73.216.43


Current Path : /var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Typolink/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/typo3/cms-frontend/Classes/Typolink/LinkFactory.php

<?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\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
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\Controller\TypoScriptFrontendController;
use TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent;

/**
 * Main class for generating any kind of frontend links.
 * Contains all logic for the infamous typolink() functionality.
 */
class LinkFactory implements LoggerAwareInterface
{
    use DefaultJavaScriptAssetTrait;
    use LoggerAwareTrait;

    public function __construct(
        protected readonly LinkService $linkService,
        protected readonly EventDispatcherInterface $eventDispatcher,
        protected readonly TypoLinkCodecService $typoLinkCodecService,
        protected readonly FrontendInterface $runtimeCache,
        protected readonly SiteFinder $siteFinder,
    ) {}

    /**
     * Main method to create links from typolink strings and configuration.
     */
    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($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($modifiedLinkParameterString ?? '');
            $linkParameterParts = array_replace($linkParameterParts, array_filter($modifiedLinkParameterParts, 'trim'));
            $linkParameter = $this->typoLinkCodecService->encode($linkParameterParts);
        } else {
            $linkParameter = trim((string)($linkConfiguration['parameter'] ?? ''));
        }
        try {
            [$linkParameter, $target, $classList, $title] = $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);

        // Check, if the target is coded as a JS open window link:
        $linkResult = $this->addJavaScriptOpenWindowInformationAttributes($linkResult, $linkConfiguration, $contentObjectRenderer);
        $linkResult = $this->addSecurityRelValues($linkResult);
        // 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);
        }

        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
    {
        $contentObjectRenderer = $contentObjectRenderer ?? GeneralUtility::makeInstance(ContentObjectRenderer::class);
        return $this->create('', ['parameter' => $urlParameter], $contentObjectRenderer);
    }

    /**
     * Legacy method, use createUri() instead.
     * @deprecated will be removed in TYPO3 v13.0.
     */
    public function createFromUriString(string $urlParameter): LinkResultInterface
    {
        trigger_error('LinkFactory->createFromUriString() will be removed in TYPO3 v13.0. Use createUri() instead.', E_USER_DEPRECATED);
        return $this->createUri($urlParameter);
    }

    /**
     * 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']) && isset($GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
            /** @var AbstractTypolinkBuilder $linkBuilder */
            $linkBuilder = GeneralUtility::makeInstance(
                $GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
                $contentObjectRenderer,
                // AbstractTypolinkBuilder type hints an optional dependency to TypoScriptFrontendController.
                // Some core parts however "fake" $GLOBALS['TSFE'] to stdCLass() due to its long list of
                // dependencies. f:html view helper is such a scenario. This of course crashes if given to typolink builder
                // classes. For now, we check the instance and hand over 'null', giving the link builders the option
                // to take care of tsfe themselves. This scenario is for instance triggered when in BE login when sys_news
                // records set links.
                $contentObjectRenderer->getTypoScriptFrontendController() instanceof TypoScriptFrontendController ? $contentObjectRenderer->getTypoScriptFrontendController() : null
            );
            try {
                return $linkBuilder->build($linkDetails, $linkText, $target, $linkConfiguration);
            } catch (UnableToLinkException $e) {
                $this->logger->debug('Unable to link "{text}"', [
                    'text' => $e->getLinkText(),
                    'exception' => $e,
                ]);
                // Only return the link text directly (done in cObj->typolink)
                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('Insuecure 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'],
        ];
    }

    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();
        }
        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
    {
        $aTagParams = $contentObjectRenderer->stdWrapValue('ATagParams', $linkConfiguration);
        // Add the global config.ATagParams
        $globalParams = $contentObjectRenderer->getTypoScriptFrontendController() ? trim($contentObjectRenderer->getTypoScriptFrontendController()->config['config']['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): LinkResultInterface
    {
        $target = (string)($linkResult->getTarget() ?: $linkResult->getAttribute('data-window-target'));
        if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->isInternalUrl($linkResult->getUrl())) {
            return $linkResult;
        }
        $relAttributeValue = 'noreferrer';
        if ($linkResult->getAttribute('rel') !== null) {
            $existingAttributeValue = $linkResult->getAttribute('rel');
            $relAttributeValue = implode(' ', array_unique(array_merge(
                [$relAttributeValue],
                GeneralUtility::trimExplode(' ', $existingAttributeValue)
            )));
        }
        return $linkResult->withAttribute('rel', $relAttributeValue);
    }

    /**
     * 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 to move away from GeneralUtility::isOnCurrentHost
     */
    protected function isInternalUrl(string $url): 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)) {
                    ++$foundDomains;
                    break;
                }
            }
            $this->runtimeCache->set($cacheIdentifier, $foundDomains > 0);
        }

        return (bool)$this->runtimeCache->get($cacheIdentifier);
    }
}