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

FluidTYPO3 / vhs / 28589645504

02 Jul 2026 12:22PM UTC coverage: 70.574%. Remained the same
28589645504

push

github

NamelessCoder
[TASK] Migrate a couple of additional cases to RequestResolver

3 of 4 new or added lines in 2 files covered. (75.0%)

1 existing line in 1 file now uncovered.

4847 of 6868 relevant lines covered (70.57%)

4.45 hits per line

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

24.82
/Classes/ViewHelpers/Page/LanguageMenuViewHelper.php
1
<?php
2
namespace FluidTYPO3\Vhs\ViewHelpers\Page;
3

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

11
use FluidTYPO3\Vhs\Proxy\DoctrineQueryProxy;
12
use FluidTYPO3\Vhs\Proxy\SiteFinderProxy;
13
use FluidTYPO3\Vhs\Traits\ArrayConsumingViewHelperTrait;
14
use FluidTYPO3\Vhs\Traits\TagViewHelperCompatibility;
15
use FluidTYPO3\Vhs\Utility\ContentObjectFetcher;
16
use FluidTYPO3\Vhs\Utility\CoreUtility;
17
use FluidTYPO3\Vhs\Utility\RequestResolver;
18
use TYPO3\CMS\Core\Context\Context;
19
use TYPO3\CMS\Core\Context\LanguageAspect;
20
use TYPO3\CMS\Core\Database\ConnectionPool;
21
use TYPO3\CMS\Core\Imaging\Icon;
22
use TYPO3\CMS\Core\Imaging\IconFactory;
23
use TYPO3\CMS\Core\Site\Site;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
26
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
27
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
28
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
29
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
30

31
/**
32
 * ViewHelper for rendering TYPO3 menus in Fluid
33
 * Require the extension static_info_table.
34
 */
35
class LanguageMenuViewHelper extends AbstractTagBasedViewHelper
36
{
37
    use ArrayConsumingViewHelperTrait;
38
    use TagViewHelperCompatibility;
39

40
    protected array $languageMenu = [];
41
    protected int $defaultLangUid = 0;
42

43
    /**
44
     * @var string
45
     */
46
    protected $tagName = 'ul';
47

48
    /**
49
     * @var ContentObjectRenderer
50
     */
51
    protected $cObj;
52

53
    protected ConfigurationManagerInterface $configurationManager;
54

55
    /**
56
     * @var Site|\TYPO3\CMS\Core\Site\Entity\Site
57
     */
58
    protected $site;
59

60
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
61
    {
62
        $this->configurationManager = $configurationManager;
4✔
63
    }
64

65
    public function initializeArguments(): void
66
    {
67
        parent::initializeArguments();
2✔
68
        $this->registerUniversalTagAttributes();
2✔
69
        $this->registerArgument(
2✔
70
            'tagName',
2✔
71
            'string',
2✔
72
            'Tag name to use for enclosing container, list and flags (not finished) only',
2✔
73
            false,
2✔
74
            'ul'
2✔
75
        );
2✔
76
        $this->registerArgument(
2✔
77
            'tagNameChildren',
2✔
78
            'string',
2✔
79
            'Tag name to use for child nodes surrounding links, list and flags only',
2✔
80
            false,
2✔
81
            'li'
2✔
82
        );
2✔
83
        $this->registerArgument('defaultIsoFlag', 'string', 'ISO code of the default flag');
2✔
84
        $this->registerArgument('defaultLanguageLabel', 'string', 'Label for the default language');
2✔
85
        $this->registerArgument('order', 'mixed', 'Orders the languageIds after this list', false, '');
2✔
86
        $this->registerArgument('labelOverwrite', 'mixed', 'Overrides language labels');
2✔
87
        $this->registerArgument(
2✔
88
            'hideNotTranslated',
2✔
89
            'boolean',
2✔
90
            'Hides languageIDs which are not translated',
2✔
91
            false,
2✔
92
            false
2✔
93
        );
2✔
94
        $this->registerArgument(
2✔
95
            'layout',
2✔
96
            'string',
2✔
97
            'How to render links when using autorendering. Possible selections: name,flag - use fx "name" or ' .
2✔
98
            '"flag,name" or "name,flag"',
2✔
99
            false,
2✔
100
            'flag,name'
2✔
101
        );
2✔
102
        $this->registerArgument(
2✔
103
            'useCHash',
2✔
104
            'boolean',
2✔
105
            'Use cHash for typolink. Has no effect on TYPO3 v9.5+',
2✔
106
            false,
2✔
107
            true
2✔
108
        );
2✔
109
        $this->registerArgument('flagPath', 'string', 'Overwrites the path to the flag folder', false, '');
2✔
110
        $this->registerArgument('flagImageType', 'string', 'Sets type of flag image: png, gif, jpeg', false, 'svg');
2✔
111
        $this->registerArgument('linkCurrent', 'boolean', 'Sets flag to link current language or not', false, true);
2✔
112
        $this->registerArgument(
2✔
113
            'classCurrent',
2✔
114
            'string',
2✔
115
            'Sets the class, by which the current language will be marked',
2✔
116
            false,
2✔
117
            'current'
2✔
118
        );
2✔
119
        $this->registerArgument(
2✔
120
            'as',
2✔
121
            'string',
2✔
122
            'If used, stores the menu pages as an array in a variable named according to this value and renders ' .
2✔
123
            'the tag content - which means automatic rendering is disabled if this attribute is used',
2✔
124
            false,
2✔
125
            'languageMenu'
2✔
126
        );
2✔
127
        $this->registerArgument('pageUid', 'integer', 'Optional page uid to use.', false, 0);
2✔
128
        $this->registerArgument('configuration', 'array', 'Additional typoLink configuration', false, []);
2✔
129
        $this->registerArgument('excludeQueryVars', 'string', 'Comma-separate list of variables to exclude', false, '');
2✔
130
        $this->registerArgument(
2✔
131
            'languages',
2✔
132
            'mixed',
2✔
133
            'Array, CSV or Traversable containing UIDs of languages to render'
2✔
134
        );
2✔
135
    }
136

137
    public function render(): string
138
    {
139
        if (!is_object($GLOBALS['TSFE']->sys_page)) {
×
140
            return '';
×
141
        }
142
        /** @var ContentObjectRenderer|null $contentObject */
143
        $contentObject = ContentObjectFetcher::resolve($this->configurationManager);
×
144
        if ($contentObject === null) {
×
145
            throw new Exception('v:page.languageMenu requires a ContentObjectRenderer, none found', 1737807859);
×
146
        }
147

148
        $this->cObj = $contentObject;
×
149
        $this->tagName = is_scalar($this->arguments['tagName']) ? (string) $this->arguments['tagName'] : 'ul';
×
150
        $this->tag->setTagName($this->tagName);
×
151
        $this->site = $this->getSite();
×
152
        $this->defaultLangUid = $this->site->getDefaultLanguage()->getLanguageId();
×
153
        $this->languageMenu = $this->parseLanguageMenu();
×
154

155
        /** @var string $as */
156
        $as = $this->arguments['as'];
×
157
        $this->renderingContext->getVariableProvider()->add($as, $this->languageMenu);
×
158
        /** @var string|null $content */
159
        $content = $this->renderChildren();
×
160
        $content = is_scalar($content) ? (string) $content : '';
×
161
        $this->renderingContext->getVariableProvider()->remove($as);
×
162
        if (0 === mb_strlen(trim($content))) {
×
163
            $content = $this->autoRender();
×
164
        }
165
        return $content;
×
166
    }
167

168
    protected function autoRender(): string
169
    {
170
        $content = $this->getLanguageMenu();
×
171
        $content = trim($content);
×
172
        if (!empty($content)) {
×
173
            $this->tag->setContent($content);
×
174
            $content = $this->tag->render();
×
175
        }
176
        return $content;
×
177
    }
178

179
    /**
180
     * Get layout 0 (default): list
181
     */
182
    protected function getLanguageMenu(): string
183
    {
184
        $tagName = $this->arguments['tagNameChildren'];
×
185
        $html = [];
×
186
        $itemCount = count($this->languageMenu);
×
187
        foreach ($this->languageMenu as $index => $var) {
×
188
            $class = '';
×
189
            $classes = [];
×
190
            if ($var['inactive']) {
×
191
                $classes[] = 'inactive';
×
192
            }
193
            if ($var['current']) {
×
194
                $classes[] = $this->arguments['classCurrent'];
×
195
            }
196
            if (0 === $index) {
×
197
                $classes[] = 'first';
×
198
            } elseif (($itemCount - 1) === $index) {
×
199
                $classes[] = 'last';
×
200
            }
201
            if (0 < count($classes)) {
×
202
                $class = ' class="' . implode(' ', $classes) . '" ';
×
203
            }
204
            if ($var['current'] && !$this->arguments['linkCurrent']) {
×
205
                $html[] = '<' . $tagName . $class . '>' . $this->getLayout($var) . '</' . $tagName . '>';
×
206
            } else {
207
                $html[] = '<' . $tagName . $class . '><a href="' . htmlspecialchars($var['url']) . '">' .
×
208
                    $this->getLayout($var) . '</a></' . $tagName . '>';
×
209
            }
210
        }
211
        return implode(PHP_EOL, $html);
×
212
    }
213

214
    /**
215
     * Returns the flag source given the language ISO code.
216
     */
217
    protected function getLanguageFlag(string $iso, string $label): string
218
    {
219
        /** @var string $flagPath */
220
        $flagPath = $this->arguments['flagPath'];
×
221
        /** @var string $flagImageType */
222
        $flagImageType = $this->arguments['flagImageType'];
×
223
        if ('' !== $flagPath) {
×
224
            $path = trim($flagPath);
×
225
        } else {
226
            $path = CoreUtility::getLanguageFlagIconPath();
×
227
        }
228

229
        $imgType = trim($flagImageType);
×
230
        $conf = [
×
231
            'file' => $path . strtoupper($iso) . '.' . $imgType,
×
232
            'altText' => $label,
×
233
            'titleText' => $label
×
234
        ];
×
235

236
        if (file_exists($conf['file'])) {
×
237
            $contentObjectDefinition = $this->cObj->getContentObject('IMAGE');
×
238
            if ($contentObjectDefinition === null) {
×
239
                return '';
×
240
            }
241
            return $this->cObj->render($contentObjectDefinition, $conf);
×
242
        }
243
        return '';
×
244
    }
245

246
    /**
247
     * Returns the flag source given a TYPO3 icon identifier.
248
     */
249
    protected function getLanguageFlagByIdentifier(string $identifier): string
250
    {
251
        /** @var IconFactory $iconFactory */
252
        $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
×
253
        $icon = $iconFactory->getIcon($identifier, Icon::SIZE_SMALL);
×
254
        return $icon->render();
×
255
    }
256

257
    /**
258
     * Return the layout: flag & text, flags only or text only.
259
     */
260
    protected function getLayout(array $language): string
261
    {
262
        /** @var string $layout */
263
        $layout = $this->arguments['layout'];
×
264
        /** @var string $flagCode */
265
        $flagCode = $language['flagCode'];
×
266
        $flagImage = false !== stripos($layout, 'flag') ? $flagCode : '';
×
267
        /** @var string $label */
268
        $label = $language['label'];
×
269
        switch ($this->arguments['layout']) {
×
270
            case 'flag':
×
271
                $html = $flagImage;
×
272
                break;
×
273
            case 'name':
×
274
                $html = $label;
×
275
                break;
×
276
            case 'name,flag':
×
277
                $html = $label;
×
278
                if ('' !== $flagImage) {
×
279
                    $html .= '&nbsp;' . $flagImage;
×
280
                }
281
                break;
×
282
            case 'flag,name':
×
283
            default:
284
                if ('' !== $flagImage) {
×
285
                    $html = $flagImage . '&nbsp;' . $label;
×
286
                } else {
287
                    $html = $label;
×
288
                }
289
        }
290
        return $html;
×
291
    }
292

293
    /**
294
     * Sets all parameter for langMenu.
295
     */
296
    protected function parseLanguageMenu(): array
297
    {
298
        /** @var array $languages */
299
        $languages = $this->arguments['languages'];
×
300
        /** @var string|null $orderArgument */
301
        $orderArgument = $this->arguments['order'];
×
302
        /** @var iterable $order */
303
        $order = $orderArgument ? GeneralUtility::trimExplode(',', $orderArgument) : '';
×
304
        /** @var string $labelOverwrite */
305
        $labelOverwrite = $this->arguments['labelOverwrite'];
×
306
        if (!empty($labelOverwrite)) {
×
307
            /** @var array $labelOverwrite */
308
            $labelOverwrite = GeneralUtility::trimExplode(',', $labelOverwrite);
×
309
        }
310

311
        // first gather languages into this array so we can reorder it later
312
        $limitLanguages = static::arrayFromArrayOrTraversableOrCSVStatic($languages ?? []);
×
313
        $limitLanguages = array_filter($limitLanguages);
×
314
        $tempArray = $this->getLanguagesFromSiteConfiguration($limitLanguages);
×
315

316
        // reorder languageMenu
317
        $languageMenu = [];
×
318
        if (!empty($order)) {
×
319
            foreach ($order as $value) {
×
320
                if (isset($tempArray[$value])) {
×
321
                    $languageMenu[$value] = $tempArray[$value];
×
322
                }
323
            }
324
        } else {
325
            $languageMenu = $tempArray;
×
326
        }
327

328
        // overwrite of label
329
        if (!empty($labelOverwrite)) {
×
330
            $i = 0;
×
331
            foreach ($languageMenu as $key => $value) {
×
332
                $languageMenu[$key]['label'] = $labelOverwrite[$i];
×
333
                $i++;
×
334
            }
335
        }
336

337
        // get the languages actually available on this page
338
        $languageUids = $this->getSystemLanguageUids();
×
339

340
        if (class_exists(LanguageAspect::class)) {
×
341
            /** @var Context $context */
342
            $context = GeneralUtility::makeInstance(Context::class);
×
343
            /** @var LanguageAspect $languageAspect */
344
            $languageAspect = $context->getAspect('language');
×
345
            $languageUid = $languageAspect->getId();
×
346
        } else {
347
            $languageUid = $GLOBALS['TSFE']->sys_language_uid;
×
348
        }
349

350
        foreach ($languageMenu as $key => $value) {
×
351
            $current = $languageUid === (int) $key ? 1 : 0;
×
352
            $inactive = in_array($key, $languageUids) || (int) $key === $this->defaultLangUid ? 0 : 1;
×
353
            $url = $this->getLanguageUrl($key);
×
354
            if (empty($url)) {
×
NEW
355
                $url = (string) RequestResolver::getRequest()->getUri();
×
356
            }
357
            $languageMenu[$key]['current'] = $current;
×
358
            $languageMenu[$key]['inactive'] = $inactive;
×
359
            $languageMenu[$key]['url'] = $url;
×
360
            $languageMenu[$key]['flagSrc'] = $this->getLanguageFlag($value['flag'] ?? $value['iso'], $value['label']);
×
361
            // if the user has set a flag path, always use that over the TYPO3 icon factory so the user
362
            // has the option to use custom flag images based on the ISO code of the language.
363
            // if the user has not set a flag path, prefer the TYPO3 icon factory when an icon
364
            // identifier is available (i.e., when using the site-based language lookup) .
365
            if (isset($value['flagIdentifier']) && empty($this->arguments['flagPath'])) {
×
366
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlagByIdentifier($value['flagIdentifier']);
×
367
            } else {
368
                $languageMenu[$key]['flagCode'] = $this->getLanguageFlag(
×
369
                    $value['flag'] ?? $value['iso'],
×
370
                    $value['label']
×
371
                );
×
372
            }
373
            if ($this->arguments['hideNotTranslated'] && $inactive) {
×
374
                unset($languageMenu[$key]);
×
375
            }
376
        }
377

378
        return $languageMenu;
×
379
    }
380

381
    /**
382
     * Get the list of languages from the sys_language table.
383
     */
384
    protected function getLanguagesFromSysLanguage(array $limitLanguages): array
385
    {
386
        // add default language
387
        $result[0] = [
×
388
            'label' => $this->arguments['defaultLanguageLabel'] ?? 'English',
×
389
            'flag' => $this->arguments['defaultIsoFlag'] ?? 'gb'
×
390
        ];
×
391

392
        $select = 'uid,title,flag';
×
393
        $from = 'sys_language';
×
394

395
        if (!empty($limitLanguages)) {
×
396
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
397
                $from,
×
398
                ['selectFields' => $select, 'pidInList' => 'root', 'uidInList' => implode(',', $limitLanguages)]
×
399
            );
×
400
        } else {
401
            $sysLanguage = $GLOBALS['TSFE']->cObj->getRecords(
×
402
                $from,
×
403
                ['selectFields' => $select, 'pidInList' => 'root']
×
404
            );
×
405
        }
406

407
        foreach ($sysLanguage as $value) {
×
408
            $result[$value['uid']] = [
×
409
                'label' => $value['title'],
×
410
                'flag' => $value['flag'],
×
411
            ];
×
412
        }
413

414
        return $result;
×
415
    }
416

417
    /**
418
     * Get the list of languages from the site configuration.
419
     */
420
    protected function getLanguagesFromSiteConfiguration(array $limitLanguages): array
421
    {
422
        $site = $this->getSite();
×
423
        // get only languages set as visible in frontend
424
        $languages = $site->getLanguages();
×
425
        $defaultLanguage = $site->getDefaultLanguage();
×
426

427
        $result = [];
×
428
        foreach ($languages as $language) {
×
429
            if (!empty($limitLanguages) && !in_array($language->getLanguageId(), $limitLanguages)) {
×
430
                continue;
×
431
            }
432
            $label = $language->getNavigationTitle();
×
433
            $flag = $language->getFlagIdentifier();
×
434
            if ($language->getLanguageId() == $defaultLanguage->getLanguageId()) {
×
435
                // override label/flag of default language if given
436
                $label = $this->arguments['defaultLanguageLabel'] ?? $label;
×
437
                $flag = $this->arguments['defaultIsoFlag'] ?? $flag;
×
438
            }
439
            if (version_compare(VersionNumberUtility::getCurrentTypo3Version(), '12.4', '>=')) {
×
440
                $isoCode = $language->getLocale()->getLanguageCode();
×
441
            } else {
442
                $isoCode = $language->getTwoLetterIsoCode();
×
443
            }
444
            $result[$language->getLanguageId()] = [
×
445
                'label' => $label,
×
446
                'iso' => $isoCode,
×
447
                'flagIdentifier' => $flag
×
448
            ];
×
449
        }
450

451
        return $result;
×
452
    }
453

454
    /**
455
     * Get link of language menu entry
456
     *
457
     * @param int|string $languageId
458
     */
459
    protected function getLanguageUrl($languageId): string
460
    {
461
        /** @var string $excludeVarsArgument */
462
        $excludeVarsArgument = $this->arguments['excludeQueryVars'];
×
463
        $excludedVars = trim((string) $excludeVarsArgument);
×
464
        $config = [
×
465
            'parameter' => $this->getPageUid(),
×
466
            'returnLast' => 'url',
×
467
            'additionalParams' => '&L=' . $languageId,
×
468
            'addQueryString' => 1,
×
469
            'addQueryString.' => [
×
470
                'method' => 'GET',
×
471
                'exclude' => 'id,L,cHash' . ($excludedVars ? ',' . $excludedVars : '')
×
472
            ]
×
473
        ];
×
474
        if (is_array($this->arguments['configuration'])) {
×
475
            $config = $this->mergeArrays($config, $this->arguments['configuration']);
×
476
        }
477
        return $this->cObj->typoLink('', $config);
×
478
    }
479

480
    /**
481
     * Get page via pageUid argument or current id
482
     */
483
    protected function getPageUid(): int
484
    {
485
        /** @var int $pageUid */
486
        $pageUid = $this->arguments['pageUid'];
×
487
        $pageUid = (int) $pageUid;
×
488
        if (0 === $pageUid) {
×
489
            $pageUid = $GLOBALS['TSFE']->id;
×
490
        }
491

492
        return (int) $pageUid;
×
493
    }
494

495
    /**
496
     * Find the site corresponding to the page that the menu is being rendered for
497
     *
498
     * @return Site|\TYPO3\CMS\Core\Site\Entity\Site
499
     */
500
    protected function getSite()
501
    {
502
        /** @var SiteFinderProxy $siteFinder */
503
        $siteFinder = GeneralUtility::makeInstance(SiteFinderProxy::class);
×
504
        return $siteFinder->getSiteByPageId($this->getPageUid());
×
505
    }
506

507
    /**
508
     * Fetches system languages available on the page depending on the TYPO3 version.
509
     *
510
     * @return int[]
511
     * @phpcsSuppress
512
     * @see https://docs.typo3.org/typo3cms/extensions/core/Changelog/9.0/Important-82445-MigratePagesLanguageOverlayIntoPages.html
513
     */
514
    protected function getSystemLanguageUids(): array
515
    {
516
        $table = 'pages';
×
517
        $parentField = 'l10n_parent';
×
518

519
        /** @var ConnectionPool $connectionPool */
520
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
×
521
        $connection = $connectionPool->getConnectionForTable($table);
×
522
        $queryBuilder = $connection->createQueryBuilder();
×
523
        $queryBuilder->select('sys_language_uid')
×
524
            ->from($table)
×
525
            ->where(
×
526
                $queryBuilder->expr()->eq($parentField, $this->getPageUid())
×
527
            );
×
528
        $result = DoctrineQueryProxy::executeQueryOnQueryBuilder($queryBuilder);
×
529
        $rows = DoctrineQueryProxy::fetchAllAssociative($result);
×
530

531
        return array_column($rows, 'sys_language_uid');
×
532
    }
533
}
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