‪TYPO3CMS  ‪main
LinkFactory.php
Go to the documentation of this file.
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 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerAwareTrait;
26 use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
34 
39 class ‪LinkFactory implements LoggerAwareInterface
40 {
42  use LoggerAwareTrait;
43 
44  public function ‪__construct(
45  protected readonly ‪LinkService $linkService,
46  protected readonly EventDispatcherInterface $eventDispatcher,
47  protected readonly TypoLinkCodecService $typoLinkCodecService,
48  protected readonly ‪FrontendInterface $runtimeCache,
49  protected readonly ‪SiteFinder $siteFinder,
50  ) {}
51 
55  public function ‪create(string $linkText, array $linkConfiguration, ‪ContentObjectRenderer $contentObjectRenderer): ‪LinkResultInterface
56  {
57  if (isset($linkConfiguration['parameter.'])) {
58  // Evaluate "parameter." stdWrap but keep additional information (like target, class and title)
59  $linkParameterParts = $this->typoLinkCodecService->decode((string)($linkConfiguration['parameter'] ?? ''));
60  $modifiedLinkParameterString = $contentObjectRenderer->‪stdWrap($linkParameterParts['url'], $linkConfiguration['parameter.']);
61  // As the stdWrap result might contain target etc. as well again (".field = header_link")
62  // the result is then taken from the stdWrap and overridden if the value is not empty.
63  $modifiedLinkParameterParts = $this->typoLinkCodecService->decode((string)($modifiedLinkParameterString ?? ''));
64  $linkParameterParts = array_replace($linkParameterParts, array_filter($modifiedLinkParameterParts, static fn($value) => trim((string)$value) !== ''));
65  $linkParameter = $this->typoLinkCodecService->encode($linkParameterParts);
66  } else {
67  $linkParameter = trim((string)($linkConfiguration['parameter'] ?? ''));
68  }
69  try {
70  [$linkParameter, $target, $classList, $title] = $this->‪resolveTypolinkParameterString($linkParameter, $linkConfiguration);
71  } catch (‪UnableToLinkException $e) {
72  $this->logger->warning($e->getMessage(), ['linkConfiguration' => $linkConfiguration]);
73  throw $e;
74  }
75  $linkDetails = $this->‪resolveLinkDetails($linkParameter, $linkConfiguration, $contentObjectRenderer);
76  if ($linkDetails === null) {
77  throw new ‪UnableToLinkException('Could not resolve link details from ' . $linkParameter, 1642001442, null, $linkText);
78  }
79 
80  $linkResult = $this->‪buildLinkResult($linkText, $linkDetails, $target, $linkConfiguration, $contentObjectRenderer);
81 
82  // Enrich the link result with resolved attributes and run post processing
83  $linkResult = $this->‪addAdditionalAnchorTagAttributes($linkResult, $linkConfiguration, $contentObjectRenderer);
84 
85  // Check, if the target is coded as a JS open window link:
86  $linkResult = $this->‪addJavaScriptOpenWindowInformationAttributes($linkResult, $linkConfiguration, $contentObjectRenderer);
87  $linkResult = $this->‪addSecurityRelValues($linkResult);
88  // Title attribute, will override any title attribute from ->addAdditionalAnchorTagAttributes()
89  $title = $title ?: trim((string)$contentObjectRenderer->‪stdWrapValue('title', $linkConfiguration));
90  if (!empty($title)) {
91  $linkResult = $linkResult->withAttribute('title', $title);
92  }
93  // Class attribute, will override any class attribute from ->addAdditionalAnchorTagAttributes()
94  if (!empty($classList)) {
95  $linkResult = $linkResult->withAttribute('class', $classList);
96  }
97 
98  if ($linkConfiguration['userFunc'] ?? false) {
99  $linkResult = $contentObjectRenderer->‪callUserFunction($linkConfiguration['userFunc'], $linkConfiguration['userFunc.'] ?? [], $linkResult);
100  if (!($linkResult instanceof ‪LinkResultInterface)) {
101  throw new ‪UnableToLinkException('Calling typolink.userFunc resulted in not returning a valid typolink', 1642171035, null, $linkText);
102  }
103  }
104 
105  $event = new ‪AfterLinkIsGeneratedEvent($linkResult, $contentObjectRenderer, $linkConfiguration);
106  $event = $this->eventDispatcher->dispatch($event);
107  return $event->getLinkResult();
108  }
109 
114  public function ‪createUri(string $urlParameter, ‪ContentObjectRenderer $contentObjectRenderer = null): ‪LinkResultInterface
115  {
116  if ($contentObjectRenderer === null) {
117  $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class);
118  // @todo: LinkFactory needs the request to determine fallback page uid when link config has none.
119  $contentObjectRenderer->setRequest(‪$GLOBALS['TYPO3_REQUEST']);
120  }
121  return $this->‪create('', ['parameter' => $urlParameter], $contentObjectRenderer);
122  }
123 
128  protected function ‪buildLinkResult(string $linkText, array $linkDetails, string $target, array $linkConfiguration, ‪ContentObjectRenderer $contentObjectRenderer): ‪LinkResultInterface
129  {
130  if (isset($linkDetails['type']) && isset(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']])) {
132  $linkBuilder = GeneralUtility::makeInstance(
133  ‪$GLOBALS['TYPO3_CONF_VARS']['FE']['typolinkBuilder'][$linkDetails['type']],
134  $contentObjectRenderer,
135  // AbstractTypolinkBuilder type hints an optional dependency to TypoScriptFrontendController.
136  // Some core parts however "fake" $GLOBALS['TSFE'] to stdCLass() due to its long list of
137  // dependencies. f:html view helper is such a scenario. This of course crashes if given to typolink builder
138  // classes. For now, we check the instance and hand over 'null', giving the link builders the option
139  // to take care of tsfe themselves. This scenario is for instance triggered when in BE login when sys_news
140  // records set links.
141  $contentObjectRenderer->‪getTypoScriptFrontendController() instanceof ‪TypoScriptFrontendController ? $contentObjectRenderer->‪getTypoScriptFrontendController() : null
142  );
143  try {
144  return $linkBuilder->build($linkDetails, $linkText, $target, $linkConfiguration);
145  } catch (‪UnableToLinkException $e) {
146  $this->logger->debug('Unable to link "{text}"', [
147  'text' => $e->‪getLinkText(),
148  'exception' => $e,
149  ]);
150  // Only return the link text directly (done in cObj->typolink)
151  throw $e;
152  }
153  } elseif (isset($linkDetails['url'])) {
154  $linkResult = new LinkResult($linkDetails['type'], $linkDetails['url']);
155  return $linkResult
156  ->withTarget($target)
157  ->withLinkConfiguration($linkConfiguration)
158  ->withLinkText($linkText);
159  }
160  throw new ‪UnableToLinkException('No suitable link handler for resolving ' . $linkDetails['typoLinkParameter'], 1642000232, null, $linkText);
161  }
162 
166  protected function ‪resolveLinkDetails(string $linkParameter, array $linkConfiguration, ‪ContentObjectRenderer $contentObjectRenderer): ?array
167  {
168  $linkDetails = null;
169  if (!$linkParameter) {
170  // Support anchors without href value if id or name attribute is present.
171  $aTagParams = (string)$contentObjectRenderer->‪stdWrapValue('ATagParams', $linkConfiguration);
172  $aTagParams = GeneralUtility::get_tag_attributes($aTagParams);
173  // If it looks like an anchor tag, render it anyway
174  if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
175  $linkDetails = [
176  'type' => ‪LinkService::TYPE_INPAGE,
177  'url' => '',
178  ];
179  }
180  } else {
181  // Detecting kind of link and resolve all necessary parameters
182  try {
183  $linkDetails = $this->linkService->resolve($linkParameter);
185  $this->logger->warning('The link could not be generated', ['exception' => $exception]);
186  return null;
187  }
188  }
189  if (is_array($linkDetails)) {
190  $linkDetails['typoLinkParameter'] = $linkParameter;
191  }
192  return $linkDetails;
193  }
194 
201  protected function ‪resolveTypolinkParameterString(string $mixedLinkParameter, array &$linkConfiguration = []): array
202  {
203  $linkParameterParts = $this->typoLinkCodecService->decode($mixedLinkParameter);
204  [$linkHandlerKeyword] = explode(':', $linkParameterParts['url'], 2);
205  if (in_array(strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', (string)$linkHandlerKeyword)), ['javascript', 'data'], true)) {
206  // Disallow insecure scheme's like javascript: or data:
207  throw new ‪UnableToLinkException('Insuecure scheme for linking detected with "' . $mixedLinkParameter . "'", 1641986533);
208  }
209 
210  // additional parameters that need to be set
211  if ($linkParameterParts['additionalParams'] !== '') {
212  $forceParams = $linkParameterParts['additionalParams'];
213  // params value
214  $linkConfiguration['additionalParams'] = ($linkConfiguration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
215  }
216 
217  return [
218  $linkParameterParts['url'],
219  $linkParameterParts['target'],
220  $linkParameterParts['class'],
221  $linkParameterParts['title'],
222  ];
223  }
224 
225  protected function ‪addJavaScriptOpenWindowInformationAttributes(‪LinkResultInterface $linkResult, array $linkConfiguration, ‪ContentObjectRenderer $contentObjectRenderer): ‪LinkResultInterface
226  {
227  $JSwindowParts = [];
228  if ($linkResult->‪getTarget() && preg_match('/^([0-9]+)x([0-9]+)(:(.*)|.*)$/', $linkResult->‪getTarget(), $JSwindowParts)) {
229  // Take all pre-configured and inserted parameters and compile parameter list, including width+height:
230  $JSwindow_tempParamsArr = ‪GeneralUtility::trimExplode(',', strtolower(($linkConfiguration['JSwindow_params'] ?? '') . ',' . ($JSwindowParts[4] ?? '')), true);
231  $JSwindow_paramsArr = [];
232  $target = $linkConfiguration['target'] ?? 'FEopenLink';
233  foreach ($JSwindow_tempParamsArr as $JSv) {
234  [$JSp, $JSv] = explode('=', $JSv, 2);
235  // If the target is set as JS param, this is extracted
236  if ($JSp === 'target') {
237  $target = $JSv;
238  } else {
239  $JSwindow_paramsArr[$JSp] = $JSp . '=' . $JSv;
240  }
241  }
242  // Add width/height:
243  $JSwindow_paramsArr['width'] = 'width=' . $JSwindowParts[1];
244  $JSwindow_paramsArr['height'] = 'height=' . $JSwindowParts[2];
245 
246  $JSwindowAttrs = [
247  'data-window-url' => $linkResult->‪getUrl(),
248  'data-window-target' => $target,
249  'data-window-features' => implode(',', $JSwindow_paramsArr),
250  ];
251  $linkResult = $linkResult->‪withAttributes($JSwindowAttrs);
252  $linkResult = $linkResult->‪withAttribute('target', $target);
253  $this->addDefaultFrontendJavaScript($contentObjectRenderer->‪getRequest());
254  }
255  return $linkResult;
256  }
257 
262  protected function ‪addAdditionalAnchorTagAttributes(‪LinkResultInterface $linkResult, array $linkConfiguration, ‪ContentObjectRenderer $contentObjectRenderer): ‪LinkResultInterface
263  {
264  $request = $contentObjectRenderer->‪getRequest();
265  $frontendTypoScriptConfigArray = $request->getAttribute('frontend.typoscript')?->getConfigArray();
266  $aTagParams = $contentObjectRenderer->‪stdWrapValue('ATagParams', $linkConfiguration);
267  // Add the global config.ATagParams
268  $globalParams = $frontendTypoScriptConfigArray['ATagParams'] ?? '';
269  $aTagParams = trim($globalParams . ' ' . $aTagParams);
270  if (!empty($aTagParams)) {
271  // Decode entities here, as they are doubly escaped again when using HTML output
272  $aTagParams = GeneralUtility::get_tag_attributes($aTagParams, true);
273  // Ensure "href" is not in the list of aTagParams to avoid double tags, usually happens within buggy parseFunc settings
274  unset($aTagParams['href']);
275  $linkResult = $linkResult->‪withAttributes($aTagParams);
276  }
277  return $linkResult;
278  }
279 
281  {
282  $target = (string)($linkResult->‪getTarget() ?: $linkResult->‪getAttribute('data-window-target'));
283  if (in_array($target, ['', null, '_self', '_parent', '_top'], true) || $this->‪isInternalUrl($linkResult->‪getUrl())) {
284  return $linkResult;
285  }
286  $relAttributeValue = 'noreferrer';
287  if ($linkResult->‪getAttribute('rel') !== null) {
288  $existingAttributeValue = $linkResult->‪getAttribute('rel');
289  $relAttributeValue = implode(' ', array_unique(array_merge(
290  [$relAttributeValue],
291  ‪GeneralUtility::trimExplode(' ', $existingAttributeValue)
292  )));
293  }
294  return $linkResult->‪withAttribute('rel', $relAttributeValue);
295  }
296 
306  protected function ‪isInternalUrl(string ‪$url): bool
307  {
308  $parsedUrl = parse_url(‪$url);
309  $foundDomains = 0;
310  if (!isset($parsedUrl['host'])) {
311  return true;
312  }
313 
314  $cacheIdentifier = sha1('isInternalDomain' . $parsedUrl['host']);
315 
316  if ($this->runtimeCache->has($cacheIdentifier) === false) {
317  foreach ($this->siteFinder->getAllSites() as $site) {
318  if ($site->getBase()->getHost() === $parsedUrl['host']) {
319  ++$foundDomains;
320  break;
321  }
322  if ($site->getBase()->getHost() === '' && ‪GeneralUtility::isOnCurrentHost(‪$url)) {
323  ++$foundDomains;
324  break;
325  }
326  }
327  $this->runtimeCache->set($cacheIdentifier, $foundDomains > 0);
328  }
329 
330  return (bool)$this->runtimeCache->get($cacheIdentifier);
331  }
332 }
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\getTypoScriptFrontendController
‪getTypoScriptFrontendController()
Definition: ContentObjectRenderer.php:5351
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\stdWrapValue
‪string int bool null stdWrapValue($key, array $config, $defaultValue='')
Definition: ContentObjectRenderer.php:1139
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\stdWrap
‪string null stdWrap($content='', $conf=[])
Definition: ContentObjectRenderer.php:1039
‪TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait
Definition: DefaultJavaScriptAssetTrait.php:30
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\getRequest
‪getRequest()
Definition: ContentObjectRenderer.php:5430
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Webhooks\Message\$url
‪identifier readonly UriInterface $url
Definition: LoginErrorOccurredMessage.php:36
‪TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController
Definition: TypoScriptFrontendController.php:58
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
Definition: ContentObjectRenderer.php:102
‪TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer\callUserFunction
‪mixed callUserFunction($funcName, $conf, $content)
Definition: ContentObjectRenderer.php:4398
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Resource\Exception\InvalidPathException
Definition: InvalidPathException.php:23
‪TYPO3\CMS\Core\Utility\GeneralUtility\isOnCurrentHost
‪static bool isOnCurrentHost(string $url)
Definition: GeneralUtility.php:409
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822