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

FluidTYPO3 / vhs / 28587673031

02 Jul 2026 11:48AM UTC coverage: 69.886% (+0.1%) from 69.743%
28587673031

Pull #1986

github

web-flow
Merge d34275774 into 01283ce2e
Pull Request #1986: [TASK] Update test base class

0 of 2 new or added lines in 2 files covered. (0.0%)

79 existing lines in 5 files now uncovered.

4827 of 6907 relevant lines covered (69.89%)

4.33 hits per line

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

86.05
/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 Psr\Http\Message\ServerRequestInterface;
13
use TYPO3\CMS\Core\Context\Context;
14
use TYPO3\CMS\Core\Context\LanguageAspect;
15
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
16
use TYPO3\CMS\Core\SingletonInterface;
17
use TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility;
18
use TYPO3\CMS\Core\Utility\GeneralUtility;
19
use TYPO3\CMS\Core\Utility\RootlineUtility;
20
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
21

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

35
    protected static array $cachedPages = [];
36
    protected static array $cachedMenus = [];
37

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

58
        return static::$cachedMenus[$cacheKey];
2✔
59
    }
60

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

68
        return static::$cachedPages[$cacheKey];
2✔
69
    }
70

71
    public function getRootLine(
72
        ?int $pageUid = null,
73
        bool $reverse = false
74
    ): array {
75
        if (null === $pageUid) {
8✔
76
            if (isset($GLOBALS['TSFE'])) {
4✔
77
                $pageUid = $GLOBALS['TSFE']->id;
4✔
78
            } else {
UNCOV
79
                $pageUid = $this->getRequest()->getQueryParams()['id'] ?? null;
×
80
            }
81
        }
82

83
        if (!$pageUid) {
8✔
UNCOV
84
            throw new \UnexpectedValueException('PageService::getRootLine requires a page UID', 1774448248);
×
85
        }
86

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

96
    protected function getPageConstraints(
97
        array $excludePages = [],
98
        bool $includeNotInMenu = false,
99
        bool $includeMenuSeparator = false
100
    ): string {
101
        $constraints = [];
2✔
102

103
        $types = [
2✔
104
            PageRepository::DOKTYPE_BE_USER_SECTION,
2✔
105
            PageRepository::DOKTYPE_SYSFOLDER
2✔
106
        ];
2✔
107

108
        if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '<=')) {
2✔
UNCOV
109
            $types[] = PageRepository::DOKTYPE_RECYCLER;
×
110
        }
111

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

114
        if ($includeNotInMenu === false) {
2✔
115
            $constraints[] = 'nav_hide = 0';
2✔
116
        }
117

118
        if ($includeMenuSeparator === false) {
2✔
119
            $constraints[] = 'doktype != ' . PageRepository::DOKTYPE_SPACER;
2✔
120
        }
121

122
        if (0 < count($excludePages)) {
2✔
UNCOV
123
            $constraints[] = 'uid NOT IN (' . implode(',', $excludePages) . ')';
×
124
        }
125

126
        return 'AND ' . implode(' AND ', $constraints);
2✔
127
    }
128

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

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

167
        $pageOverlay = [];
2✔
168
        if (0 !== $languageUid) {
2✔
169
            $pageOverlay = $this->getPageRepository()->getPageOverlay($pageUid, $languageUid);
×
170
        }
171
        $translationAvailable = (0 !== count($pageOverlay));
2✔
172

173
        return
2✔
174
            ($hideIfNotTranslated && (0 !== $languageUid) && !$translationAvailable) ||
2✔
175
            ($hideIfDefaultLanguage && ((0 === $languageUid) || !$translationAvailable)) ||
2✔
176
            (!$normalWhenNoLanguage && (0 !== $languageUid) && !$translationAvailable);
2✔
177
    }
178

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

205
        return $GLOBALS['TSFE']->cObj->typoLink('', $config);
4✔
206
    }
207

208
    public function isAccessProtected(array $page): bool
209
    {
210
        return (0 !== (int) $page['fe_group']);
16✔
211
    }
212

213
    public function isAccessGranted(array $page): bool
214
    {
215
        if (!$this->isAccessProtected($page)) {
14✔
216
            return true;
2✔
217
        }
218

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

221
        $hide = (in_array(-1, $groups));
12✔
222
        $show = (in_array(-2, $groups));
12✔
223

224
        $userIsLoggedIn = (is_array($GLOBALS['TSFE']->fe_user->user));
12✔
225
        $userGroups = $GLOBALS['TSFE']->fe_user->groupData['uid'];
12✔
226
        $userIsInGrantedGroups = (0 < count(array_intersect($userGroups, $groups)));
12✔
227

228
        return (!$userIsLoggedIn && $hide) || ($userIsLoggedIn && $show) || ($userIsLoggedIn && $userIsInGrantedGroups);
12✔
229
    }
230

231
    public function isCurrent(int $pageUid): bool
232
    {
233
        return ($pageUid === (int) $GLOBALS['TSFE']->id);
4✔
234
    }
235

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

245
        return false;
2✔
246
    }
247

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

255
        return $useShortcutTarget;
4✔
256
    }
257

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

265
        return $useShortcutUid;
4✔
266
    }
267

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

299
    /**
300
     * @return PageRepository
301
     * @codeCoverageIgnore
302
     */
303
    public function getPageRepository()
304
    {
305
        return clone ($GLOBALS['TSFE']->sys_page ?? $this->getPageRepositoryForBackendContext());
306
    }
307

308
    /**
309
     * @return PageRepository
310
     * @codeCoverageIgnore
311
     */
312
    protected function getPageRepositoryForBackendContext()
313
    {
314
        static $instance = null;
315
        if ($instance === null) {
316
            /** @var PageRepository $instance */
317
            $instance = GeneralUtility::makeInstance(PageRepository::class);
318
        }
319
        return $instance;
320
    }
321

322
    private function getRequest(): ServerRequestInterface
323
    {
UNCOV
324
        return $GLOBALS['TYPO3_REQUEST'];
×
325
    }
326
}
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