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