1<?php
2declare(strict_types = 1);
3namespace TYPO3\CMS\Frontend\Typolink;
4
5/*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18use TYPO3\CMS\Core\Service\DependencyOrderingService;
19use TYPO3\CMS\Core\TypoScript\TemplateService;
20use TYPO3\CMS\Core\Utility\GeneralUtility;
21use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
22use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
23use TYPO3\CMS\Frontend\Http\UrlProcessorInterface;
24use TYPO3\CMS\Frontend\Page\PageRepository;
25
26/**
27 * Abstract class to provide proper helper for most types necessary
28 * Hands in the ContentObject and TSFE which are needed here for all the stdWrap magic.
29 */
30abstract class AbstractTypolinkBuilder
31{
32    /**
33     * @var ContentObjectRenderer
34     */
35    protected $contentObjectRenderer;
36
37    /**
38     * @var TypoScriptFrontendController
39     */
40    protected $typoScriptFrontendController;
41
42    /**
43     * AbstractTypolinkBuilder constructor.
44     *
45     * @param ContentObjectRenderer $contentObjectRenderer
46     * @param TypoScriptFrontendController $typoScriptFrontendController
47     */
48    public function __construct(ContentObjectRenderer $contentObjectRenderer, TypoScriptFrontendController $typoScriptFrontendController = null)
49    {
50        $this->contentObjectRenderer = $contentObjectRenderer;
51        $this->typoScriptFrontendController = $typoScriptFrontendController ?? $GLOBALS['TSFE'];
52    }
53
54    /**
55     * Should be implemented by all subclasses to return an array with three parts:
56     * - URL
57     * - Link Text (can be modified)
58     * - Target (can be modified)
59     *
60     * @param array $linkDetails parsed link details by the LinkService
61     * @param string $linkText the link text
62     * @param string $target the target to point to
63     * @param array $conf the TypoLink configuration array
64     * @return array an array with three parts (URL, Link Text, Target)
65     */
66    abstract public function build(array &$linkDetails, string $linkText, string $target, array $conf): array;
67
68    /**
69     * Forces a given URL to be absolute.
70     *
71     * @param string $url The URL to be forced to be absolute
72     * @param array $configuration TypoScript configuration of typolink
73     * @return string The absolute URL
74     */
75    protected function forceAbsoluteUrl(string $url, array $configuration): string
76    {
77        if (!empty($url) && !empty($configuration['forceAbsoluteUrl']) && preg_match('#^(?:([a-z]+)(://)([^/]*)/?)?(.*)$#', $url, $matches)) {
78            $urlParts = [
79                'scheme' => $matches[1],
80                'delimiter' => '://',
81                'host' => $matches[3],
82                'path' => $matches[4]
83            ];
84            $isUrlModified = false;
85            // Set scheme and host if not yet part of the URL:
86            if (empty($urlParts['host'])) {
87                $urlParts['scheme'] = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
88                $urlParts['host'] = GeneralUtility::getIndpEnv('HTTP_HOST');
89                $urlParts['path'] = '/' . ltrim($urlParts['path'], '/');
90                // absRefPrefix has been prepended to $url beforehand
91                // so we only modify the path if no absRefPrefix has been set
92                // otherwise we would destroy the path
93                if ($this->getTypoScriptFrontendController()->absRefPrefix === '') {
94                    $urlParts['path'] = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . ltrim($urlParts['path'], '/');
95                }
96                $isUrlModified = true;
97            }
98            // Override scheme:
99            $forceAbsoluteUrl = &$configuration['forceAbsoluteUrl.']['scheme'];
100            if (!empty($forceAbsoluteUrl) && $urlParts['scheme'] !== $forceAbsoluteUrl) {
101                $urlParts['scheme'] = $forceAbsoluteUrl;
102                $isUrlModified = true;
103            }
104            // Recreate the absolute URL:
105            if ($isUrlModified) {
106                $url = implode('', $urlParts);
107            }
108        }
109        return $url;
110    }
111
112    /**
113     * Determines whether lib.parseFunc is defined.
114     *
115     * @return bool
116     */
117    protected function isLibParseFuncDefined(): bool
118    {
119        $configuration = $this->contentObjectRenderer->mergeTSRef(
120            ['parseFunc' => '< lib.parseFunc'],
121            'parseFunc'
122        );
123        return !empty($configuration['parseFunc.']) && is_array($configuration['parseFunc.']);
124    }
125
126    /**
127     * Helper method to a fallback method parsing HTML out of it
128     *
129     * @param string $originalLinkText the original string, if empty, the fallback link text
130     * @param string $fallbackLinkText the string to be used.
131     * @return string the final text
132     */
133    protected function parseFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
134    {
135        if ($originalLinkText !== '') {
136            return $originalLinkText;
137        }
138        if ($this->isLibParseFuncDefined()) {
139            return $this->contentObjectRenderer->parseFunc($fallbackLinkText, ['makelinks' => 0], '< lib.parseFunc');
140        }
141        // encode in case `lib.parseFunc` is not configured
142        return $this->encodeFallbackLinkTextIfLinkTextIsEmpty($originalLinkText, $fallbackLinkText);
143    }
144
145    /**
146     * Helper method to a fallback method properly encoding HTML.
147     *
148     * @param string $originalLinkText the original string, if empty, the fallback link text
149     * @param string $fallbackLinkText the string to be used.
150     * @return string the final text
151     */
152    protected function encodeFallbackLinkTextIfLinkTextIsEmpty(string $originalLinkText, string $fallbackLinkText): string
153    {
154        if ($originalLinkText !== '') {
155            return $originalLinkText;
156        }
157        return htmlspecialchars($fallbackLinkText, ENT_QUOTES);
158    }
159
160    /**
161     * Creates the value for target="..." in a typolink configuration
162     *
163     * @param array $conf the typolink configuration
164     * @param string $name the key, usually "target", "extTarget" or "fileTarget"
165     * @param bool $respectFrameSetOption if set, then the fallback is only used as target if the doctype allows it
166     * @param string $fallbackTarget the string to be used when no target is found in the configuration
167     * @return string the value of the target attribute, if there is one
168     */
169    protected function resolveTargetAttribute(array $conf, string $name, bool $respectFrameSetOption = false, string $fallbackTarget = ''): string
170    {
171        $tsfe = $this->getTypoScriptFrontendController();
172        $targetAttributeAllowed = !$respectFrameSetOption
173            || (!isset($tsfe->config['config']['doctype']) || !$tsfe->config['config']['doctype'])
174            || in_array((string)$tsfe->config['config']['doctype'], ['xhtml_trans', 'xhtml_basic', 'html5'], true);
175
176        $target = '';
177        if (isset($conf[$name])) {
178            $target = $conf[$name];
179        } elseif ($targetAttributeAllowed && !$conf['directImageLink']) {
180            $target = $fallbackTarget;
181        }
182        if (isset($conf[$name . '.']) && $conf[$name . '.']) {
183            $target = (string)$this->contentObjectRenderer->stdWrap($target, $conf[$name . '.'] ?? []);
184        }
185        return $target;
186    }
187
188    /**
189     * Loops over all configured URL modifier hooks (if available) and returns the generated URL or NULL if no URL was generated.
190     *
191     * @param string $context The context in which the method is called (e.g. typoLink).
192     * @param string $url The URL that should be processed.
193     * @param array $typolinkConfiguration The current link configuration array.
194     * @return string|null Returns NULL if URL was not processed or the processed URL as a string.
195     * @throws \RuntimeException if a hook was registered but did not fulfill the correct parameters.
196     */
197    protected function processUrl(string $context, string $url, array $typolinkConfiguration = [])
198    {
199        $urlProcessors = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['urlProcessing']['urlProcessors'] ?? false;
200        if (!$urlProcessors) {
201            return $url;
202        }
203
204        foreach ($urlProcessors as $identifier => $configuration) {
205            if (empty($configuration) || !is_array($configuration)) {
206                throw new \RuntimeException('Missing configuration for URI processor "' . $identifier . '".', 1491130459);
207            }
208            if (!is_string($configuration['processor']) || empty($configuration['processor']) || !class_exists($configuration['processor']) || !is_subclass_of($configuration['processor'], UrlProcessorInterface::class)) {
209                throw new \RuntimeException('The URI processor "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . UrlProcessorInterface::class . '".', 1491130460);
210            }
211        }
212
213        $orderedProcessors = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($urlProcessors);
214        $keepProcessing = true;
215
216        foreach ($orderedProcessors as $configuration) {
217            /** @var UrlProcessorInterface $urlProcessor */
218            $urlProcessor = GeneralUtility::makeInstance($configuration['processor']);
219            $url = $urlProcessor->process($context, $url, $typolinkConfiguration, $this->contentObjectRenderer, $keepProcessing);
220            if (!$keepProcessing) {
221                break;
222            }
223        }
224
225        return $url;
226    }
227
228    /**
229     * @return TypoScriptFrontendController
230     */
231    public function getTypoScriptFrontendController(): TypoScriptFrontendController
232    {
233        if ($this->typoScriptFrontendController instanceof TypoScriptFrontendController) {
234            return $this->typoScriptFrontendController;
235        }
236
237        // This usually happens when typolink is created by the TYPO3 Backend, where no TSFE object
238        // is there. This functionality is currently completely internal, as these links cannot be
239        // created properly from the Backend.
240        // However, this is added to avoid any exceptions when trying to create a link
241        $this->typoScriptFrontendController = GeneralUtility::makeInstance(
242            TypoScriptFrontendController::class,
243            null,
244            GeneralUtility::_GP('id'),
245            (int)GeneralUtility::_GP('type')
246        );
247        $this->typoScriptFrontendController->sys_page = GeneralUtility::makeInstance(PageRepository::class);
248        $this->typoScriptFrontendController->tmpl = GeneralUtility::makeInstance(TemplateService::class);
249        return $this->typoScriptFrontendController;
250    }
251}
252