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

FluidTYPO3 / vhs / 13566190336

27 Feb 2025 12:18PM UTC coverage: 72.127% (-0.6%) from 72.746%
13566190336

push

github

NamelessCoder
[TER] 7.1.0

5649 of 7832 relevant lines covered (72.13%)

20.01 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 TYPO3\CMS\Core\Context\Context;
18
use TYPO3\CMS\Core\Context\LanguageAspect;
19
use TYPO3\CMS\Core\Database\ConnectionPool;
20
use TYPO3\CMS\Core\Imaging\Icon;
21
use TYPO3\CMS\Core\Imaging\IconFactory;
22
use TYPO3\CMS\Core\Site\Site;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
25
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
26
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
27
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
28
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
29

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

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

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

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

52
    protected ConfigurationManagerInterface $configurationManager;
53

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

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

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

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

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

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

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

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

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

233
        $imgType = trim($flagImageType);
×
234
        $conf = [
×
235
            'file' => $path . strtoupper($iso) . '.' . $imgType,
×
236
            'altText' => $label,
×
237
            'titleText' => $label
×
238
        ];
×
239

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

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

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

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

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

320
        // reorder languageMenu
321
        $languageMenu = [];
×
322
        if (!empty($order)) {
×
323
            foreach ($order as $value) {
×
324
                if (isset($tempArray[$value])) {
×
325
                    $languageMenu[$value] = $tempArray[$value];
×
326
                }
327
            }
328
        } else {
329
            $languageMenu = $tempArray;
×
330
        }
331

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

341
        // get the languages actually available on this page
342
        $languageUids = $this->getSystemLanguageUids();
×
343

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

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

382
        return $languageMenu;
×
383
    }
384

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

396
        $select = 'uid,title,flag';
×
397
        $from = 'sys_language';
×
398

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

411
        foreach ($sysLanguage as $value) {
×
412
            $result[$value['uid']] = [
×
413
                'label' => $value['title'],
×
414
                'flag' => $value['flag'],
×
415
            ];
×
416
        }
417

418
        return $result;
×
419
    }
420

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

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

455
        return $result;
×
456
    }
457

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

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

496
        return (integer) $pageUid;
×
497
    }
498

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

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

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

535
        return array_column($rows, 'sys_language_uid');
×
536
    }
537
}
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