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

FluidTYPO3 / vhs / 28588802516

02 Jul 2026 12:08PM UTC coverage: 70.321% (+0.4%) from 69.971%
28588802516

push

github

NamelessCoder
[TASK] Remove most of the remaining TSFE usages

21 of 32 new or added lines in 7 files covered. (65.63%)

4836 of 6877 relevant lines covered (70.32%)

4.41 hits per line

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

87.5
/Classes/Service/PageService.php
1
<?php
2

3
namespace FluidTYPO3\Vhs\Service;
4

5
/*
6
 * This file is part of the FluidTYPO3/Vhs project under GPLv2 or later.
7
 *
8
 * For the full copyright and license information, please read the
9
 * LICENSE.md file that was distributed with this source code.
10
 */
11

12
use FluidTYPO3\Vhs\Utility\ContentObjectFetcher;
13
use FluidTYPO3\Vhs\Utility\ParameterUtility;
14
use FluidTYPO3\Vhs\Utility\RequestResolver;
15
use TYPO3\CMS\Core\Context\Context;
16
use TYPO3\CMS\Core\Context\LanguageAspect;
17
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
18
use TYPO3\CMS\Core\SingletonInterface;
19
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
21
use TYPO3\CMS\Core\Utility\RootlineUtility;
22
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
23

24
/**
25
 * Page Service
26
 *
27
 * Wrapper service for \TYPO3\CMS\Frontend\Page\PageRepository including static caches for
28
 * menus, rootlines, pages and page overlays to be implemented in
29
 * viewhelpers by replacing calls to \TYPO3\CMS\Frontend\Page\PageRepository::getMenu()
30
 * and the like.
31
 */
32
class PageService implements SingletonInterface
33
{
34
    public const int DOKTYPE_MOVE_TO_PLACEHOLDER = 0;
35
    public const int SHORTCUT_MODE_RANDOM_SUBPAGE = 2;
36

37
    protected static array $cachedPages = [];
38
    protected static array $cachedMenus = [];
39

40
    public function getMenu(
41
        int $pageUid,
42
        array $excludePages = [],
43
        bool $includeNotInMenu = false,
44
        bool $includeMenuSeparator = false,
45
        bool $disableGroupAccessCheck = false
46
    ): array {
47
        $pageRepository = $this->getPageRepository();
2✔
48
        $pageConstraints = $this->getPageConstraints($excludePages, $includeNotInMenu, $includeMenuSeparator);
2✔
49
        $cacheKey = md5($pageUid . $pageConstraints . (int) $disableGroupAccessCheck);
2✔
50
        if (!isset(static::$cachedMenus[$cacheKey])) {
2✔
51
            static::$cachedMenus[$cacheKey] = array_filter(
2✔
52
                $pageRepository->getMenu($pageUid, '*', 'sorting', $pageConstraints, true, $disableGroupAccessCheck),
2✔
53
                function ($page) use ($includeNotInMenu) {
2✔
54
                    return (!($page['nav_hide'] ?? false) || $includeNotInMenu)
2✔
55
                        && !$this->hidePageForLanguageUid($page);
2✔
56
                }
2✔
57
            );
2✔
58
        }
59

60
        return static::$cachedMenus[$cacheKey];
2✔
61
    }
62

63
    public function getPage(int $pageUid, bool $disableGroupAccessCheck = false): array
64
    {
65
        $cacheKey = md5($pageUid . (int) $disableGroupAccessCheck);
2✔
66
        if (!isset(static::$cachedPages[$cacheKey])) {
2✔
67
            static::$cachedPages[$cacheKey] = $this->getPageRepository()->getPage($pageUid, $disableGroupAccessCheck);
2✔
68
        }
69

70
        return static::$cachedPages[$cacheKey];
2✔
71
    }
72

73
    public function getRootLine(
74
        ?int $pageUid = null,
75
        bool $reverse = false
76
    ): array {
77
        $pageUid ??= RequestResolver::getPageUid();
8✔
78

79
        if (!$pageUid) {
8✔
80
            throw new \UnexpectedValueException('PageService::getRootLine requires a page UID', 1774448248);
×
81
        }
82

83
        /** @var RootlineUtility $rootLineUtility */
84
        $rootLineUtility = GeneralUtility::makeInstance(RootlineUtility::class, $pageUid);
8✔
85
        $rootline = $rootLineUtility->get();
8✔
86
        if ($reverse) {
8✔
87
            $rootline = array_reverse($rootline);
4✔
88
        }
89
        return $rootline;
8✔
90
    }
91

92
    protected function getPageConstraints(
93
        array $excludePages = [],
94
        bool $includeNotInMenu = false,
95
        bool $includeMenuSeparator = false
96
    ): string {
97
        $constraints = [];
2✔
98

99
        $types = [
2✔
100
            PageRepository::DOKTYPE_BE_USER_SECTION,
2✔
101
            PageRepository::DOKTYPE_SYSFOLDER
2✔
102
        ];
2✔
103

104
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '<=')) {
2✔
105
            $types[] = PageRepository::DOKTYPE_RECYCLER;
×
106
        }
107

108
        $constraints[] = 'doktype NOT IN (' . implode(',', $types) . ')';
2✔
109

110
        if ($includeNotInMenu === false) {
2✔
111
            $constraints[] = 'nav_hide = 0';
2✔
112
        }
113

114
        if ($includeMenuSeparator === false) {
2✔
115
            $constraints[] = 'doktype != ' . PageRepository::DOKTYPE_SPACER;
2✔
116
        }
117

118
        if (0 < count($excludePages)) {
2✔
119
            $constraints[] = 'uid NOT IN (' . implode(',', $excludePages) . ')';
×
120
        }
121

122
        return 'AND ' . implode(' AND ', $constraints);
2✔
123
    }
124

125
    /**
126
     * @param array|integer|null $page
127
     */
128
    public function hidePageForLanguageUid($page = null, int $languageUid = -1, bool $normalWhenNoLanguage = true): bool
129
    {
130
        if (is_array($page)) {
2✔
131
            $pageUid = $page['uid'];
2✔
132
            $pageRecord = $page;
2✔
133
        } else {
NEW
134
            $pageUid = (0 === (int) $page) ? RequestResolver::getPageUid() : (int) $page;
×
135
            $pageRecord = $this->getPage($pageUid);
×
136
        }
137
        if (-1 === $languageUid) {
2✔
138
            if (class_exists(LanguageAspect::class)) {
2✔
139
                /** @var Context $context */
140
                $context = GeneralUtility::makeInstance(Context::class);
2✔
141
                /** @var LanguageAspect $languageAspect */
142
                $languageAspect = $context->getAspect('language');
2✔
143
                $languageUid = $languageAspect->getId();
2✔
144
            } else {
NEW
145
                $languageUid = RequestResolver::getLanguage()->getLanguageId();
×
146
            }
147
        }
148

149
        $l18nCfg = $pageRecord['l18n_cfg'] ?? 0;
2✔
150
        if (class_exists(PageTranslationVisibility::class)) {
2✔
151
            /** @var PageTranslationVisibility $visibilityBitSet */
152
            $visibilityBitSet = GeneralUtility::makeInstance(
2✔
153
                PageTranslationVisibility::class,
2✔
154
                $l18nCfg
2✔
155
            );
2✔
156
            $hideIfNotTranslated = $visibilityBitSet->shouldHideTranslationIfNoTranslatedRecordExists();
2✔
157
            $hideIfDefaultLanguage = $visibilityBitSet->shouldBeHiddenInDefaultLanguage();
2✔
158
        } else {
159
            $hideIfNotTranslated = (bool) GeneralUtility::hideIfNotTranslated($l18nCfg);
×
160
            $hideIfDefaultLanguage = (bool) GeneralUtility::hideIfDefaultLanguage($l18nCfg);
×
161
        }
162

163
        $pageOverlay = [];
2✔
164
        if (0 !== $languageUid) {
2✔
165
            $pageOverlay = $this->getPageRepository()->getPageOverlay($pageUid, $languageUid);
×
166
        }
167
        $translationAvailable = (0 !== count($pageOverlay));
2✔
168

169
        return
2✔
170
            ($hideIfNotTranslated && (0 !== $languageUid) && !$translationAvailable) ||
2✔
171
            ($hideIfDefaultLanguage && ((0 === $languageUid) || !$translationAvailable)) ||
2✔
172
            (!$normalWhenNoLanguage && (0 !== $languageUid) && !$translationAvailable);
2✔
173
    }
174

175
    public function getItemLink(array $page, bool $forceAbsoluteUrl = false): string
176
    {
177
        $parameter = $page['uid'];
4✔
178
        if ((int) $page['doktype'] === PageRepository::DOKTYPE_LINK) {
4✔
179
            $redirectTo = $page['url'] ?? '';
2✔
180
            if (!empty($redirectTo)) {
2✔
181
                $uI = parse_url($redirectTo);
×
182
                // If relative path, prefix Site URL
183
                // If it's a valid email without protocol, add "mailto:"
184
                if (!($uI['scheme'] ?? false)) {
×
185
                    if (GeneralUtility::validEmail($redirectTo)) {
×
186
                        $redirectTo = 'mailto:' . $redirectTo;
×
187
                    } elseif ($redirectTo[0] !== '/') {
×
NEW
188
                        $redirectTo = ParameterUtility::resolveParameterValue('TYPO3_SITE_URL') . $redirectTo;
×
189
                    }
190
                }
191
                $parameter = $redirectTo;
×
192
            }
193
        }
194
        $config = [
4✔
195
            'parameter' => $parameter,
4✔
196
            'returnLast' => 'url',
4✔
197
            'additionalParams' => '',
4✔
198
            'forceAbsoluteUrl' => $forceAbsoluteUrl,
4✔
199
        ];
4✔
200

201
        return ContentObjectFetcher::resolve()->typoLink_URL($config);
4✔
202
    }
203

204
    public function isAccessProtected(array $page): bool
205
    {
206
        return (0 !== (int) $page['fe_group']);
16✔
207
    }
208

209
    public function isAccessGranted(array $page): bool
210
    {
211
        if (!$this->isAccessProtected($page)) {
14✔
212
            return true;
2✔
213
        }
214

215
        $groups = GeneralUtility::intExplode(',', (string) $page['fe_group']);
12✔
216

217
        $hide = (in_array(-1, $groups));
12✔
218
        $show = (in_array(-2, $groups));
12✔
219

220
        $userIsLoggedIn = RequestResolver::isFrontendUserLoggedIn();
12✔
221
        if (!$userIsLoggedIn) {
12✔
222
            return $hide;
6✔
223
        }
224

225
        $frontendUser = RequestResolver::getFrontendUser();
6✔
226
        $userGroups = $frontendUser?->groupData['uid'] ?? [];
6✔
227
        $userIsInGrantedGroups = (0 < count(array_intersect($userGroups, $groups)));
6✔
228

229
        return $show || $userIsInGrantedGroups;
6✔
230
    }
231

232
    public function isCurrent(int $pageUid): bool
233
    {
234
        return $pageUid === RequestResolver::getPageUid();
4✔
235
    }
236

237
    public function isActive(int $pageUid): bool
238
    {
239
        $rootLineData = $this->getRootLine();
8✔
240
        foreach ($rootLineData as $page) {
8✔
241
            if ((int) $page['uid'] === $pageUid) {
8✔
242
                return true;
8✔
243
            }
244
        }
245

246
        return false;
2✔
247
    }
248

249
    public function shouldUseShortcutTarget(array $arguments): bool
250
    {
251
        $useShortcutTarget = (bool) $arguments['useShortcutData'];
4✔
252
        if (array_key_exists('useShortcutTarget', $arguments)) {
4✔
253
            $useShortcutTarget = (bool) $arguments['useShortcutTarget'];
4✔
254
        }
255

256
        return $useShortcutTarget;
4✔
257
    }
258

259
    public function shouldUseShortcutUid(array $arguments): bool
260
    {
261
        $useShortcutUid = (bool) $arguments['useShortcutData'];
4✔
262
        if (array_key_exists('useShortcutUid', $arguments)) {
4✔
263
            $useShortcutUid = (bool) $arguments['useShortcutUid'];
4✔
264
        }
265

266
        return $useShortcutUid;
4✔
267
    }
268

269
    /**
270
     * Determines the target page record for the provided page record
271
     * if it is configured as a shortcut in any of the possible modes.
272
     * Returns NULL otherwise.
273
     */
274
    public function getShortcutTargetPage(array $page): ?array
275
    {
276
        $dokType = (int) ($page['doktype'] ?? PageRepository::DOKTYPE_DEFAULT);
18✔
277
        if ($dokType !== PageRepository::DOKTYPE_SHORTCUT) {
18✔
278
            return null;
6✔
279
        }
280
        $originalPageUid = $page['uid'];
12✔
281
        switch ($page['shortcut_mode']) {
12✔
282
            case PageRepository::SHORTCUT_MODE_PARENT_PAGE:
283
                $targetPage = $this->getPage($page['pid']);
2✔
284
                break;
2✔
285
            case self::SHORTCUT_MODE_RANDOM_SUBPAGE:
286
                $menu = $this->getMenu($page['shortcut'] > 0 ? $page['shortcut'] : $originalPageUid);
4✔
287
                $targetPage = (0 < count($menu)) ? $menu[array_rand($menu)] : $page;
4✔
288
                break;
4✔
289
            case PageRepository::SHORTCUT_MODE_FIRST_SUBPAGE:
290
                $menu = $this->getMenu($page['shortcut'] > 0 ? $page['shortcut'] : $originalPageUid);
4✔
291
                $targetPage = (0 < count($menu)) ? reset($menu) : $page;
4✔
292
                break;
4✔
293
            case PageRepository::SHORTCUT_MODE_NONE:
294
            default:
295
                $targetPage = $this->getPage($page['shortcut']);
2✔
296
        }
297
        return $targetPage;
12✔
298
    }
299

300
    /**
301
     * @return PageRepository
302
     * @codeCoverageIgnore
303
     */
304
    public function getPageRepository()
305
    {
306
        static $instance = null;
307
        if ($instance === null) {
308
            /** @var PageRepository $instance */
309
            $instance = GeneralUtility::makeInstance(PageRepository::class);
310
        }
311
        return $instance;
312
    }
313
}
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