• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TYPO3-Headless / headless / 25552700505

08 May 2026 11:18AM UTC coverage: 74.7% (+1.7%) from 73.04%
25552700505

Pull #881

github

web-flow
Merge cf3b72b38 into c36c6b6bf
Pull Request #881: [WIP] TYPO3 v14 support - ongoing work - not stable

38 of 58 new or added lines in 10 files covered. (65.52%)

23 existing lines in 2 files now uncovered.

1181 of 1581 relevant lines covered (74.7%)

6.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.0
/Classes/Event/Listener/AfterLinkIsGeneratedListener.php
1
<?php
2

3
/*
4
 * This file is part of the "headless" Extension for TYPO3 CMS.
5
 *
6
 * For the full copyright and license information, please read the
7
 * LICENSE.md file that was distributed with this source code.
8
 */
9

10
declare(strict_types=1);
11

12
namespace FriendsOfTYPO3\Headless\Event\Listener;
13

14
use FriendsOfTYPO3\Headless\Utility\HeadlessFrontendUrlInterface;
15
use Psr\Log\LoggerInterface;
16
use Throwable;
17
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
18
use TYPO3\CMS\Core\LinkHandling\LinkService;
19
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
20
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
21
use TYPO3\CMS\Core\Site\Entity\Site;
22
use TYPO3\CMS\Core\Site\SiteFinder;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
25
use TYPO3\CMS\Frontend\Event\AfterLinkIsGeneratedEvent;
26
use TYPO3\CMS\Frontend\Typolink\UnableToLinkException;
27

28
use function is_numeric;
29
use function is_string;
30
use function str_starts_with;
31

32
final class AfterLinkIsGeneratedListener
33
{
34
    public function __construct(
35
        private readonly LoggerInterface $logger,
36
        private readonly HeadlessFrontendUrlInterface $urlUtility,
37
        private readonly LinkService $linkService,
38
        private readonly TypoLinkCodecService $typoLinkCodecService,
39
        private readonly SiteFinder $siteFinder
40
    ) {}
32✔
41

42
    public function __invoke(AfterLinkIsGeneratedEvent $event): void
43
    {
44
        $result = $event->getLinkResult();
31✔
45

46
        if ($result->getType() !== 'page') {
31✔
47
            return;
26✔
48
        }
49

50
        $pageId = $result->getLinkConfiguration()['parameter'] ?? 0;
6✔
51

52
        if ((int)($result->getLinkConfiguration()['page']['doktype'] ?? 1) === 4) {
6✔
53
            $pageId = (int)$result->getLinkConfiguration()['page']['shortcut'];
1✔
54
        } elseif (isset($result->getLinkConfiguration()['parameter.'])) {
5✔
55
            $pageId = (int)($this->linkService->resolve($event->getContentObjectRenderer()->parameters['href'] ?? '')['pageuid'] ?? 0);
2✔
56
        }
57

58
        $urlUtility = $this->urlUtility->withRequest($event->getContentObjectRenderer()->getRequest());
6✔
59

60
        if (is_numeric($pageId) && ((int)$pageId) > 0) {
6✔
61
            $href = $urlUtility->getFrontendUrlForPage(
3✔
62
                $event->getLinkResult()->getUrl(),
3✔
63
                (int)$pageId
3✔
64
            );
3✔
65
        } else {
66
            try {
67
                $site = $this->getTargetSite($event);
3✔
68
                $key = 'frontendBase';
1✔
69

70
                if (is_string($pageId) && str_starts_with(
1✔
71
                    $pageId,
1✔
72
                    't3://page?uid=current&type=' . $site->getSettings()->get(
1✔
73
                        'headless.sitemap.type',
1✔
74
                        '1533906435'
1✔
75
                    )
1✔
76
                )) {
1✔
77
                    $key = $site->getSettings()->get('headless.sitemap.key', 'frontendApiProxy');
1✔
78
                }
79

80
                $href = $urlUtility->getFrontendUrlWithSite($event->getLinkResult()->getUrl(), $site, $key);
1✔
81
            } catch (Throwable $e) {
2✔
82
                $this->logger->error($e->getMessage());
2✔
83
            }
84
        }
85

86
        if (isset($href)) {
6✔
87
            $result = $result->withAttribute('href', $href);
4✔
88
            $event->setLinkResult($result);
4✔
89
        }
90
    }
91

92
    private function getTargetSite(AfterLinkIsGeneratedEvent $event): Site
93
    {
94
        $linkConfiguration = $event->getLinkResult()->getLinkConfiguration();
3✔
95

96
        if (isset($linkConfiguration['parameter.'])) {
3✔
97
            // Evaluate "parameter." stdWrap but keep additional information (like target, class and title)
98
            $linkParameterParts = $this->typoLinkCodecService->decode($linkConfiguration['parameter'] ?? '');
1✔
UNCOV
99
            $modifiedLinkParameterString = $event->getContentObjectRenderer()->stdWrap(
×
UNCOV
100
                $linkParameterParts['url'],
×
UNCOV
101
                $linkConfiguration['parameter.']
×
UNCOV
102
            );
×
103
            // As the stdWrap result might contain target etc. as well again (".field = header_link")
104
            // the result is then taken from the stdWrap and overridden if the value is not empty.
UNCOV
105
            $modifiedLinkParameterParts = $this->typoLinkCodecService->decode((string)($modifiedLinkParameterString ?? ''));
×
UNCOV
106
            $linkParameterParts = array_replace(
×
UNCOV
107
                $linkParameterParts,
×
UNCOV
108
                array_filter($modifiedLinkParameterParts, static fn($value) => trim((string)$value) !== '')
×
UNCOV
109
            );
×
UNCOV
110
            $linkParameter = $this->typoLinkCodecService->encode($linkParameterParts);
×
111
        } else {
112
            $linkParameter = trim((string)($linkConfiguration['parameter'] ?? ''));
2✔
113
        }
114

115
        try {
116
            [$linkParameter] = $this->resolveTypolinkParameterString($linkParameter, $linkConfiguration);
2✔
117
        } catch (UnableToLinkException $e) {
1✔
118
            $this->logger->warning($e->getMessage(), ['linkConfiguration' => $linkConfiguration]);
×
119
            throw $e;
×
120
        }
121
        $linkDetails = $this->resolveLinkDetails(
1✔
122
            $linkParameter,
1✔
123
            $linkConfiguration,
1✔
124
            $event->getContentObjectRenderer()
1✔
125
        );
1✔
126
        if ($linkDetails === null) {
1✔
UNCOV
127
            throw new UnableToLinkException(
×
UNCOV
128
                'Could not resolve link details from ' . $linkParameter,
×
UNCOV
129
                1642001442,
×
UNCOV
130
                null,
×
UNCOV
131
                $event->getLinkResult()->getLinkText()
×
UNCOV
132
            );
×
133
        }
134

135
        if (($linkDetails['pageuid'] ?? 'current') === 'current') {
1✔
136
            return $event->getContentObjectRenderer()->getRequest()->getAttribute('site');
×
137
        }
138

139
        return $this->siteFinder->getSiteByPageId((int)$linkDetails['pageuid']);
1✔
140
    }
141

142
    protected function resolveLinkDetails(
143
        string $linkParameter,
144
        array $linkConfiguration,
145
        ContentObjectRenderer $contentObjectRenderer
146
    ): ?array {
147
        $linkDetails = null;
1✔
148
        if (!$linkParameter) {
1✔
149
            // Support anchors without href value if id or name attribute is present.
UNCOV
150
            $aTagParams = (string)$contentObjectRenderer->stdWrapValue('ATagParams', $linkConfiguration);
×
UNCOV
151
            $aTagParams = GeneralUtility::get_tag_attributes($aTagParams);
×
152
            // If it looks like an anchor tag, render it anyway
UNCOV
153
            if (isset($aTagParams['id']) || isset($aTagParams['name'])) {
×
154
                $linkDetails = [
×
155
                    'type' => LinkService::TYPE_INPAGE,
×
156
                    'url' => '',
×
157
                ];
×
158
            }
159
        } else {
160
            // Detecting kind of link and resolve all necessary parameters
161
            try {
162
                $linkDetails = $this->linkService->resolve($linkParameter);
1✔
163
            } catch (UnknownLinkHandlerException|InvalidPathException $exception) {
×
164
                $this->logger->warning('The link could not be generated', ['exception' => $exception]);
×
165
                return null;
×
166
            }
167
        }
168
        if (is_array($linkDetails)) {
1✔
169
            $linkDetails['typoLinkParameter'] = $linkParameter;
1✔
170
        }
171
        return $linkDetails;
1✔
172
    }
173

174
    private function resolveTypolinkParameterString(string $mixedLinkParameter, array &$linkConfiguration = []): array
175
    {
176
        $linkParameterParts = $this->typoLinkCodecService->decode($mixedLinkParameter);
2✔
177
        [$linkHandlerKeyword] = explode(':', $linkParameterParts['url'] ?? '', 2);
1✔
178
        if (in_array(
1✔
179
            strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', (string)$linkHandlerKeyword)),
1✔
180
            ['javascript', 'data'],
1✔
181
            true
1✔
182
        )) {
1✔
183
            // Disallow insecure scheme's like javascript: or data:
184
            throw new UnableToLinkException(
×
185
                'Insuecure scheme for linking detected with "' . $mixedLinkParameter . "'",
×
186
                1641986533
×
187
            );
×
188
        }
189

190
        // additional parameters that need to be set
191
        if (($linkParameterParts['additionalParams'] ?? '') !== '') {
1✔
UNCOV
192
            $forceParams = $linkParameterParts['additionalParams'];
×
193
            // params value
UNCOV
194
            $linkConfiguration['additionalParams'] = ($linkConfiguration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
×
195
        }
196

197
        return [
1✔
198
            $linkParameterParts['url'] ?? '',
1✔
199
            $linkParameterParts['target'] ?? '',
1✔
200
            $linkParameterParts['class'] ?? '',
1✔
201
            $linkParameterParts['title'] ?? '',
1✔
202
        ];
1✔
203
    }
204
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc