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

TYPO3-Headless / headless / 19539021351

20 Nov 2025 01:46PM UTC coverage: 73.272% (-0.2%) from 73.468%
19539021351

push

github

tmotyl
[BUGFIX] Fix workspace preview implementation

Resolves: #846

1 of 1 new or added line in 1 file covered. (100.0%)

4 existing lines in 1 file now uncovered.

1124 of 1534 relevant lines covered (73.27%)

8.36 hits per line

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

84.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
    ) {}
30✔
41

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

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

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

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

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

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

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

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

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

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

96
        if (isset($linkConfiguration['parameter.'])) {
22✔
97
            // Evaluate "parameter." stdWrap but keep additional information (like target, class and title)
98
            $linkParameterParts = $this->typoLinkCodecService->decode($linkConfiguration['parameter'] ?? '');
20✔
99
            $modifiedLinkParameterString = $event->getContentObjectRenderer()->stdWrap(
20✔
100
                $linkParameterParts['url'],
20✔
101
                $linkConfiguration['parameter.']
20✔
102
            );
20✔
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.
105
            $modifiedLinkParameterParts = $this->typoLinkCodecService->decode((string)($modifiedLinkParameterString ?? ''));
20✔
106
            $linkParameterParts = array_replace(
20✔
107
                $linkParameterParts,
20✔
108
                array_filter($modifiedLinkParameterParts, static fn($value) => trim((string)$value) !== '')
20✔
109
            );
20✔
110
            $linkParameter = $this->typoLinkCodecService->encode($linkParameterParts);
20✔
111
        } else {
112
            $linkParameter = trim((string)($linkConfiguration['parameter'] ?? ''));
2✔
113
        }
114

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

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

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

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

174
    private function resolveTypolinkParameterString(string $mixedLinkParameter, array &$linkConfiguration = []): array
175
    {
176
        $linkParameterParts = $this->typoLinkCodecService->decode($mixedLinkParameter);
22✔
177
        [$linkHandlerKeyword] = explode(':', $linkParameterParts['url'] ?? '', 2);
22✔
178
        if (in_array(
22✔
179
            strtolower((string)preg_replace('#\s|[[:cntrl:]]#', '', (string)$linkHandlerKeyword)),
22✔
180
            ['javascript', 'data'],
22✔
181
            true
22✔
182
        )) {
22✔
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'] ?? '') !== '') {
22✔
192
            $forceParams = $linkParameterParts['additionalParams'];
20✔
193
            // params value
194
            $linkConfiguration['additionalParams'] = ($linkConfiguration['additionalParams'] ?? '') . $forceParams[0] === '&' ? $forceParams : '&' . $forceParams;
20✔
195
        }
196

197
        return [
22✔
198
            $linkParameterParts['url'] ?? '',
22✔
199
            $linkParameterParts['target'] ?? '',
22✔
200
            $linkParameterParts['class'] ?? '',
22✔
201
            $linkParameterParts['title'] ?? '',
22✔
202
        ];
22✔
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