PageRenderer::renderInlineJavaScript()   B
last analyzed

Complexity

Conditions 9
Paths 16

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 22
dl 0
loc 31
rs 8.0555
c 0
b 0
f 0
cc 9
nc 16
nop 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Page;
17
18
use Psr\Http\Message\ServerRequestInterface;
19
use TYPO3\CMS\Backend\Routing\Router;
20
use TYPO3\CMS\Backend\Routing\UriBuilder;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
23
use TYPO3\CMS\Core\Core\Environment;
24
use TYPO3\CMS\Core\Http\ApplicationType;
25
use TYPO3\CMS\Core\Localization\Locales;
26
use TYPO3\CMS\Core\Localization\LocalizationFactory;
27
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
28
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
29
use TYPO3\CMS\Core\Package\PackageInterface;
30
use TYPO3\CMS\Core\Package\PackageManager;
31
use TYPO3\CMS\Core\Resource\RelativeCssPathFixer;
32
use TYPO3\CMS\Core\Resource\ResourceCompressor;
33
use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
34
use TYPO3\CMS\Core\SingletonInterface;
35
use TYPO3\CMS\Core\Utility\ArrayUtility;
36
use TYPO3\CMS\Core\Utility\GeneralUtility;
37
use TYPO3\CMS\Core\Utility\PathUtility;
38
39
/**
40
 * TYPO3 pageRender class
41
 * This class render the HTML of a webpage, usable for BE and FE
42
 */
43
class PageRenderer implements SingletonInterface
44
{
45
    // Constants for the part to be rendered
46
    protected const PART_COMPLETE = 0;
47
    protected const PART_HEADER = 1;
48
    protected const PART_FOOTER = 2;
49
50
    const REQUIREJS_SCOPE_CONFIG = 'config';
51
    const REQUIREJS_SCOPE_RESOLVE = 'resolve';
52
53
    /**
54
     * @var bool
55
     */
56
    protected $compressJavascript = false;
57
58
    /**
59
     * @var bool
60
     */
61
    protected $compressCss = false;
62
63
    /**
64
     * @var bool
65
     */
66
    protected $removeLineBreaksFromTemplate = false;
67
68
    /**
69
     * @var bool
70
     */
71
    protected $concatenateJavascript = false;
72
73
    /**
74
     * @var bool
75
     */
76
    protected $concatenateCss = false;
77
78
    /**
79
     * @var bool
80
     */
81
    protected $moveJsFromHeaderToFooter = false;
82
83
    /**
84
     * @var Locales
85
     */
86
    protected $locales;
87
88
    /**
89
     * The language key
90
     * Two character string or 'default'
91
     *
92
     * @var string
93
     */
94
    protected $lang;
95
96
    /**
97
     * List of language dependencies for actual language. This is used for local variants of a language
98
     * that depend on their "main" language, like Brazilian Portuguese or Canadian French.
99
     *
100
     * @var array
101
     */
102
    protected $languageDependencies = [];
103
104
    /**
105
     * @var ResourceCompressor
106
     */
107
    protected $compressor;
108
109
    // Arrays containing associative array for the included files
110
    /**
111
     * @var array<string, array>
112
     */
113
    protected $jsFiles = [];
114
115
    /**
116
     * @var array
117
     */
118
    protected $jsFooterFiles = [];
119
120
    /**
121
     * @var array
122
     */
123
    protected $jsLibs = [];
124
125
    /**
126
     * @var array
127
     */
128
    protected $jsFooterLibs = [];
129
130
    /**
131
     * @var array<string, array>
132
     */
133
    protected $cssFiles = [];
134
135
    /**
136
     * @var array<string, array>
137
     */
138
    protected $cssLibs = [];
139
140
    /**
141
     * The title of the page
142
     *
143
     * @var string
144
     */
145
    protected $title;
146
147
    /**
148
     * Charset for the rendering
149
     *
150
     * @var string
151
     */
152
    protected $charSet;
153
154
    /**
155
     * @var string
156
     */
157
    protected $favIcon;
158
159
    /**
160
     * @var string
161
     */
162
    protected $baseUrl;
163
164
    /**
165
     * @var bool
166
     */
167
    protected $renderXhtml = true;
168
169
    // Static header blocks
170
    /**
171
     * @var string
172
     */
173
    protected $xmlPrologAndDocType = '';
174
175
    /**
176
     * @var array
177
     */
178
    protected $metaTags = [];
179
180
    /**
181
     * @var array
182
     */
183
    protected $inlineComments = [];
184
185
    /**
186
     * @var array
187
     */
188
    protected $headerData = [];
189
190
    /**
191
     * @var array
192
     */
193
    protected $footerData = [];
194
195
    /**
196
     * @var string
197
     */
198
    protected $titleTag = '<title>|</title>';
199
200
    /**
201
     * @var string
202
     */
203
    protected $metaCharsetTag = '<meta http-equiv="Content-Type" content="text/html; charset=|" />';
204
205
    /**
206
     * @var string
207
     */
208
    protected $htmlTag = '<html>';
209
210
    /**
211
     * @var string
212
     */
213
    protected $headTag = '<head>';
214
215
    /**
216
     * @var string
217
     */
218
    protected $baseUrlTag = '<base href="|" />';
219
220
    /**
221
     * @var string
222
     */
223
    protected $iconMimeType = '';
224
225
    /**
226
     * @var string
227
     */
228
    protected $shortcutTag = '<link rel="icon" href="%1$s"%2$s />';
229
230
    // Static inline code blocks
231
    /**
232
     * @var array<string, array>
233
     */
234
    protected $jsInline = [];
235
236
    /**
237
     * @var array
238
     */
239
    protected $jsFooterInline = [];
240
241
    /**
242
     * @var array<string, array>
243
     */
244
    protected $cssInline = [];
245
246
    /**
247
     * @var string
248
     */
249
    protected $bodyContent;
250
251
    /**
252
     * @var string
253
     */
254
    protected $templateFile;
255
256
    // Paths to contributed libraries
257
258
    /**
259
     * default path to the requireJS library, relative to the typo3/ directory
260
     * @var string
261
     */
262
    protected $requireJsPath = 'EXT:core/Resources/Public/JavaScript/Contrib/';
263
264
    // Internal flags for JS-libraries
265
    /**
266
     * if set, the requireJS library is included
267
     * @var bool
268
     */
269
    protected $addRequireJs = false;
270
271
    /**
272
     * Inline configuration for requireJS (internal)
273
     * @var array
274
     */
275
    protected $requireJsConfig = [];
276
277
    /**
278
     * Inline configuration for requireJS from extensions
279
     *
280
     * @var array
281
     */
282
    protected $additionalRequireJsConfig = [];
283
284
    /**
285
     * Module names of internal requireJS 'paths'
286
     * @var array
287
     */
288
    protected $internalRequireJsPathModuleNames = [];
289
290
    /**
291
     * Inline configuration for requireJS (public)
292
     * @var array
293
     */
294
    protected $publicRequireJsConfig = [];
295
296
    /**
297
     * @var array
298
     */
299
    protected $inlineLanguageLabels = [];
300
301
    /**
302
     * @var array
303
     */
304
    protected $inlineLanguageLabelFiles = [];
305
306
    /**
307
     * @var array
308
     */
309
    protected $inlineSettings = [];
310
311
    /**
312
     * @var array{0: string, 1: string}
313
     */
314
    protected $inlineJavascriptWrap = [
315
        '<script type="text/javascript">' . LF . '/*<![CDATA[*/' . LF,
316
        '/*]]>*/' . LF . '</script>' . LF,
317
    ];
318
319
    /**
320
     * @var array
321
     */
322
    protected $inlineCssWrap = [
323
        '<style>' . LF . '/*<![CDATA[*/' . LF . '<!-- ' . LF,
324
        '-->' . LF . '/*]]>*/' . LF . '</style>' . LF,
325
    ];
326
327
    /**
328
     * Is empty string for HTML and ' /' for XHTML rendering
329
     *
330
     * @var string
331
     */
332
    protected $endingSlash = '';
333
334
    /**
335
     * @var MetaTagManagerRegistry
336
     */
337
    protected $metaTagRegistry;
338
339
    protected JavaScriptRenderer $javaScriptRenderer;
340
341
    /**
342
     * @var FrontendInterface
343
     */
344
    protected static $cache;
345
346
    /**
347
     * @param string $templateFile Declare the used template file. Omit this parameter will use default template
348
     */
349
    public function __construct($templateFile = '')
350
    {
351
        $this->reset();
352
        $this->locales = GeneralUtility::makeInstance(Locales::class);
353
        if ($templateFile !== '') {
354
            $this->templateFile = $templateFile;
355
        }
356
357
        $this->metaTagRegistry = GeneralUtility::makeInstance(MetaTagManagerRegistry::class);
358
        $this->javaScriptRenderer = JavaScriptRenderer::create();
359
        $this->setMetaTag('name', 'generator', 'TYPO3 CMS');
360
    }
361
362
    /**
363
     * @param array $newState
364
     * @internal
365
     */
366
    public function updateState(array $newState): void
367
    {
368
        foreach ($newState as $var => $value) {
369
            switch ($var) {
370
            case 'locales':
371
                break;
372
            case 'metaTagRegistry':
373
                $this->metaTagRegistry->updateState($value);
374
                break;
375
            default:
376
                $this->{$var} = $value;
377
                break;
378
            }
379
        }
380
    }
381
382
    /**
383
     * @return array
384
     * @internal
385
     */
386
    public function getState(): array
387
    {
388
        $state = [];
389
        foreach (get_object_vars($this) as $var => $value) {
390
            switch ($var) {
391
            case 'locales':
392
                break;
393
            case 'metaTagRegistry':
394
                $state[$var] = $this->metaTagRegistry->getState();
395
                break;
396
            default:
397
                $state[$var] = $value;
398
                break;
399
            }
400
        }
401
        return $state;
402
    }
403
404
    /**
405
     * @param FrontendInterface $cache
406
     */
407
    public static function setCache(FrontendInterface $cache)
408
    {
409
        static::$cache = $cache;
410
    }
411
412
    public function getJavaScriptRenderer(): JavaScriptRenderer
413
    {
414
        return $this->javaScriptRenderer;
415
    }
416
417
    /**
418
     * Reset all vars to initial values
419
     */
420
    protected function reset()
421
    {
422
        $this->templateFile = 'EXT:core/Resources/Private/Templates/PageRenderer.html';
423
        $this->jsFiles = [];
424
        $this->jsFooterFiles = [];
425
        $this->jsInline = [];
426
        $this->jsFooterInline = [];
427
        $this->jsLibs = [];
428
        $this->cssFiles = [];
429
        $this->cssInline = [];
430
        $this->metaTags = [];
431
        $this->inlineComments = [];
432
        $this->headerData = [];
433
        $this->footerData = [];
434
        $this->javaScriptRenderer = JavaScriptRenderer::create();
435
    }
436
437
    /*****************************************************/
438
    /*                                                   */
439
    /*  Public Setters                                   */
440
    /*                                                   */
441
    /*                                                   */
442
    /*****************************************************/
443
    /**
444
     * Sets the title
445
     *
446
     * @param string $title	title of webpage
447
     */
448
    public function setTitle($title)
449
    {
450
        $this->title = $title;
451
    }
452
453
    /**
454
     * Enables/disables rendering of XHTML code
455
     *
456
     * @param bool $enable Enable XHTML
457
     */
458
    public function setRenderXhtml($enable)
459
    {
460
        $this->renderXhtml = $enable;
461
462
        // Whenever XHTML gets disabled, remove the "text/javascript" type from the wrap
463
        // since this is not needed and may lead to validation errors in the future.
464
        $this->inlineJavascriptWrap = [
465
            '<script' . ($enable ? ' type="text/javascript" ' : '') . '>' . LF . '/*<![CDATA[*/' . LF,
466
            '/*]]>*/' . LF . '</script>' . LF,
467
        ];
468
    }
469
470
    /**
471
     * Sets xml prolog and docType
472
     *
473
     * @param string $xmlPrologAndDocType Complete tags for xml prolog and docType
474
     */
475
    public function setXmlPrologAndDocType($xmlPrologAndDocType)
476
    {
477
        $this->xmlPrologAndDocType = $xmlPrologAndDocType;
478
    }
479
480
    /**
481
     * Sets meta charset
482
     *
483
     * @param string $charSet Used charset
484
     */
485
    public function setCharSet($charSet)
486
    {
487
        $this->charSet = $charSet;
488
    }
489
490
    /**
491
     * Sets language
492
     *
493
     * @param string $lang Used language
494
     */
495
    public function setLanguage($lang)
496
    {
497
        $this->lang = $lang;
498
        $this->languageDependencies = [];
499
500
        // Language is found. Configure it:
501
        if (in_array($this->lang, $this->locales->getLocales())) {
502
            $this->languageDependencies[] = $this->lang;
503
            foreach ($this->locales->getLocaleDependencies($this->lang) as $language) {
504
                $this->languageDependencies[] = $language;
505
            }
506
        }
507
    }
508
509
    /**
510
     * Set the meta charset tag
511
     *
512
     * @param string $metaCharsetTag
513
     */
514
    public function setMetaCharsetTag($metaCharsetTag)
515
    {
516
        $this->metaCharsetTag = $metaCharsetTag;
517
    }
518
519
    /**
520
     * Sets html tag
521
     *
522
     * @param string $htmlTag Html tag
523
     */
524
    public function setHtmlTag($htmlTag)
525
    {
526
        $this->htmlTag = $htmlTag;
527
    }
528
529
    /**
530
     * Sets HTML head tag
531
     *
532
     * @param string $headTag HTML head tag
533
     */
534
    public function setHeadTag($headTag)
535
    {
536
        $this->headTag = $headTag;
537
    }
538
539
    /**
540
     * Sets favicon
541
     *
542
     * @param string $favIcon
543
     */
544
    public function setFavIcon($favIcon)
545
    {
546
        $this->favIcon = $favIcon;
547
    }
548
549
    /**
550
     * Sets icon mime type
551
     *
552
     * @param string $iconMimeType
553
     */
554
    public function setIconMimeType($iconMimeType)
555
    {
556
        $this->iconMimeType = $iconMimeType;
557
    }
558
559
    /**
560
     * Sets HTML base URL
561
     *
562
     * @param string $baseUrl HTML base URL
563
     */
564
    public function setBaseUrl($baseUrl)
565
    {
566
        $this->baseUrl = $baseUrl;
567
    }
568
569
    /**
570
     * Sets template file
571
     *
572
     * @param string $file
573
     */
574
    public function setTemplateFile($file)
575
    {
576
        $this->templateFile = $file;
577
    }
578
579
    /**
580
     * Sets Content for Body
581
     *
582
     * @param string $content
583
     */
584
    public function setBodyContent($content)
585
    {
586
        $this->bodyContent = $content;
587
    }
588
589
    /**
590
     * Sets path to requireJS library (relative to typo3 directory)
591
     *
592
     * @param string $path Path to requireJS library
593
     */
594
    public function setRequireJsPath($path)
595
    {
596
        $this->requireJsPath = $path;
597
    }
598
599
    /**
600
     * @param string $scope
601
     * @return array
602
     */
603
    public function getRequireJsConfig(string $scope = null): array
604
    {
605
        // return basic RequireJS configuration without shim, paths and packages
606
        if ($scope === static::REQUIREJS_SCOPE_CONFIG) {
607
            return array_replace_recursive(
608
                $this->publicRequireJsConfig,
609
                $this->filterArrayKeys(
610
                    $this->requireJsConfig,
611
                    ['shim', 'paths', 'packages'],
612
                    false
613
                )
614
            );
615
        }
616
        // return RequireJS configuration for resolving only shim, paths and packages
617
        if ($scope === static::REQUIREJS_SCOPE_RESOLVE) {
618
            return $this->filterArrayKeys(
619
                $this->requireJsConfig,
620
                ['shim', 'paths', 'packages'],
621
                true
622
            );
623
        }
624
        return [];
625
    }
626
627
    /*****************************************************/
628
    /*                                                   */
629
    /*  Public Enablers / Disablers                      */
630
    /*                                                   */
631
    /*                                                   */
632
    /*****************************************************/
633
    /**
634
     * Enables MoveJsFromHeaderToFooter
635
     */
636
    public function enableMoveJsFromHeaderToFooter()
637
    {
638
        $this->moveJsFromHeaderToFooter = true;
639
    }
640
641
    /**
642
     * Disables MoveJsFromHeaderToFooter
643
     */
644
    public function disableMoveJsFromHeaderToFooter()
645
    {
646
        $this->moveJsFromHeaderToFooter = false;
647
    }
648
649
    /**
650
     * Enables compression of javascript
651
     */
652
    public function enableCompressJavascript()
653
    {
654
        $this->compressJavascript = true;
655
    }
656
657
    /**
658
     * Disables compression of javascript
659
     */
660
    public function disableCompressJavascript()
661
    {
662
        $this->compressJavascript = false;
663
    }
664
665
    /**
666
     * Enables compression of css
667
     */
668
    public function enableCompressCss()
669
    {
670
        $this->compressCss = true;
671
    }
672
673
    /**
674
     * Disables compression of css
675
     */
676
    public function disableCompressCss()
677
    {
678
        $this->compressCss = false;
679
    }
680
681
    /**
682
     * Enables concatenation of js files
683
     */
684
    public function enableConcatenateJavascript()
685
    {
686
        $this->concatenateJavascript = true;
687
    }
688
689
    /**
690
     * Disables concatenation of js files
691
     */
692
    public function disableConcatenateJavascript()
693
    {
694
        $this->concatenateJavascript = false;
695
    }
696
697
    /**
698
     * Enables concatenation of css files
699
     */
700
    public function enableConcatenateCss()
701
    {
702
        $this->concatenateCss = true;
703
    }
704
705
    /**
706
     * Disables concatenation of css files
707
     */
708
    public function disableConcatenateCss()
709
    {
710
        $this->concatenateCss = false;
711
    }
712
713
    /**
714
     * Sets removal of all line breaks in template
715
     */
716
    public function enableRemoveLineBreaksFromTemplate()
717
    {
718
        $this->removeLineBreaksFromTemplate = true;
719
    }
720
721
    /**
722
     * Unsets removal of all line breaks in template
723
     */
724
    public function disableRemoveLineBreaksFromTemplate()
725
    {
726
        $this->removeLineBreaksFromTemplate = false;
727
    }
728
729
    /**
730
     * Enables Debug Mode
731
     * This is a shortcut to switch off all compress/concatenate features to enable easier debug
732
     */
733
    public function enableDebugMode()
734
    {
735
        $this->compressJavascript = false;
736
        $this->compressCss = false;
737
        $this->concatenateCss = false;
738
        $this->concatenateJavascript = false;
739
        $this->removeLineBreaksFromTemplate = false;
740
    }
741
742
    /*****************************************************/
743
    /*                                                   */
744
    /*  Public Getters                                   */
745
    /*                                                   */
746
    /*                                                   */
747
    /*****************************************************/
748
    /**
749
     * Gets the title
750
     *
751
     * @return string $title Title of webpage
752
     */
753
    public function getTitle()
754
    {
755
        return $this->title;
756
    }
757
758
    /**
759
     * Gets the charSet
760
     *
761
     * @return string $charSet
762
     */
763
    public function getCharSet()
764
    {
765
        return $this->charSet;
766
    }
767
768
    /**
769
     * Gets the language
770
     *
771
     * @return string $lang
772
     */
773
    public function getLanguage()
774
    {
775
        return $this->lang;
776
    }
777
778
    /**
779
     * Returns rendering mode XHTML or HTML
780
     *
781
     * @return bool TRUE if XHTML, FALSE if HTML
782
     */
783
    public function getRenderXhtml()
784
    {
785
        return $this->renderXhtml;
786
    }
787
788
    /**
789
     * Gets html tag
790
     *
791
     * @return string $htmlTag Html tag
792
     */
793
    public function getHtmlTag()
794
    {
795
        return $this->htmlTag;
796
    }
797
798
    /**
799
     * Get meta charset
800
     *
801
     * @return string
802
     */
803
    public function getMetaCharsetTag()
804
    {
805
        return $this->metaCharsetTag;
806
    }
807
808
    /**
809
     * Gets head tag
810
     *
811
     * @return string $tag Head tag
812
     */
813
    public function getHeadTag()
814
    {
815
        return $this->headTag;
816
    }
817
818
    /**
819
     * Gets favicon
820
     *
821
     * @return string $favIcon
822
     */
823
    public function getFavIcon()
824
    {
825
        return $this->favIcon;
826
    }
827
828
    /**
829
     * Gets icon mime type
830
     *
831
     * @return string $iconMimeType
832
     */
833
    public function getIconMimeType()
834
    {
835
        return $this->iconMimeType;
836
    }
837
838
    /**
839
     * Gets HTML base URL
840
     *
841
     * @return string $url
842
     */
843
    public function getBaseUrl()
844
    {
845
        return $this->baseUrl;
846
    }
847
848
    /**
849
     * Gets template file
850
     *
851
     * @return string
852
     */
853
    public function getTemplateFile()
854
    {
855
        return $this->templateFile;
856
    }
857
858
    /**
859
     * Gets MoveJsFromHeaderToFooter
860
     *
861
     * @return bool
862
     */
863
    public function getMoveJsFromHeaderToFooter()
864
    {
865
        return $this->moveJsFromHeaderToFooter;
866
    }
867
868
    /**
869
     * Gets compress of javascript
870
     *
871
     * @return bool
872
     */
873
    public function getCompressJavascript()
874
    {
875
        return $this->compressJavascript;
876
    }
877
878
    /**
879
     * Gets compress of css
880
     *
881
     * @return bool
882
     */
883
    public function getCompressCss()
884
    {
885
        return $this->compressCss;
886
    }
887
888
    /**
889
     * Gets concatenate of js files
890
     *
891
     * @return bool
892
     */
893
    public function getConcatenateJavascript()
894
    {
895
        return $this->concatenateJavascript;
896
    }
897
898
    /**
899
     * Gets concatenate of css files
900
     *
901
     * @return bool
902
     */
903
    public function getConcatenateCss()
904
    {
905
        return $this->concatenateCss;
906
    }
907
908
    /**
909
     * Gets remove of empty lines from template
910
     *
911
     * @return bool
912
     */
913
    public function getRemoveLineBreaksFromTemplate()
914
    {
915
        return $this->removeLineBreaksFromTemplate;
916
    }
917
918
    /**
919
     * Gets content for body
920
     *
921
     * @return string
922
     */
923
    public function getBodyContent()
924
    {
925
        return $this->bodyContent;
926
    }
927
928
    /**
929
     * Gets the inline language labels.
930
     *
931
     * @return array The inline language labels
932
     */
933
    public function getInlineLanguageLabels()
934
    {
935
        return $this->inlineLanguageLabels;
936
    }
937
938
    /**
939
     * Gets the inline language files
940
     *
941
     * @return array
942
     */
943
    public function getInlineLanguageLabelFiles()
944
    {
945
        return $this->inlineLanguageLabelFiles;
946
    }
947
948
    /*****************************************************/
949
    /*                                                   */
950
    /*  Public Functions to add Data                     */
951
    /*                                                   */
952
    /*                                                   */
953
    /*****************************************************/
954
955
    /**
956
     * Sets a given meta tag
957
     *
958
     * @param string $type The type of the meta tag. Allowed values are property, name or http-equiv
959
     * @param string $name The name of the property to add
960
     * @param string $content The content of the meta tag
961
     * @param array $subProperties Subproperties of the meta tag (like e.g. og:image:width)
962
     * @param bool $replace Replace earlier set meta tag
963
     * @throws \InvalidArgumentException
964
     */
965
    public function setMetaTag(string $type, string $name, string $content, array $subProperties = [], $replace = true)
966
    {
967
        // Lowercase all the things
968
        $type = strtolower($type);
969
        $name = strtolower($name);
970
        if (!in_array($type, ['property', 'name', 'http-equiv'], true)) {
971
            throw new \InvalidArgumentException(
972
                'When setting a meta tag the only types allowed are property, name or http-equiv. "' . $type . '" given.',
973
                1496402460
974
            );
975
        }
976
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
977
        $manager->addProperty($name, $content, $subProperties, $replace, $type);
978
    }
979
980
    /**
981
     * Returns the requested meta tag
982
     *
983
     * @param string $type
984
     * @param string $name
985
     *
986
     * @return array
987
     */
988
    public function getMetaTag(string $type, string $name): array
989
    {
990
        // Lowercase all the things
991
        $type = strtolower($type);
992
        $name = strtolower($name);
993
994
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
995
        $propertyContent = $manager->getProperty($name, $type);
996
997
        if (!empty($propertyContent[0])) {
998
            return [
999
                'type' => $type,
1000
                'name' => $name,
1001
                'content' => $propertyContent[0]['content'],
1002
            ];
1003
        }
1004
        return [];
1005
    }
1006
1007
    /**
1008
     * Unset the requested meta tag
1009
     *
1010
     * @param string $type
1011
     * @param string $name
1012
     */
1013
    public function removeMetaTag(string $type, string $name)
1014
    {
1015
        // Lowercase all the things
1016
        $type = strtolower($type);
1017
        $name = strtolower($name);
1018
1019
        $manager = $this->metaTagRegistry->getManagerForProperty($name);
1020
        $manager->removeProperty($name, $type);
1021
    }
1022
1023
    /**
1024
     * Adds inline HTML comment
1025
     *
1026
     * @param string $comment
1027
     */
1028
    public function addInlineComment($comment)
1029
    {
1030
        if (!in_array($comment, $this->inlineComments)) {
1031
            $this->inlineComments[] = $comment;
1032
        }
1033
    }
1034
1035
    /**
1036
     * Adds header data
1037
     *
1038
     * @param string $data Free header data for HTML header
1039
     */
1040
    public function addHeaderData($data)
1041
    {
1042
        if (!in_array($data, $this->headerData)) {
1043
            $this->headerData[] = $data;
1044
        }
1045
    }
1046
1047
    /**
1048
     * Adds footer data
1049
     *
1050
     * @param string $data Free header data for HTML header
1051
     */
1052
    public function addFooterData($data)
1053
    {
1054
        if (!in_array($data, $this->footerData)) {
1055
            $this->footerData[] = $data;
1056
        }
1057
    }
1058
1059
    /**
1060
     * Adds JS Library. JS Library block is rendered on top of the JS files.
1061
     *
1062
     * @param string $name Arbitrary identifier
1063
     * @param string $file File name
1064
     * @param string $type Content Type
1065
     * @param bool $compress Flag if library should be compressed
1066
     * @param bool $forceOnTop Flag if added library should be inserted at begin of this block
1067
     * @param string $allWrap
1068
     * @param bool $excludeFromConcatenation
1069
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1070
     * @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
1071
     * @param string $integrity Subresource Integrity (SRI)
1072
     * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
1073
     * @param string $crossorigin CORS settings attribute
1074
     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
1075
     */
1076
    public function addJsLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
1077
    {
1078
        if (!isset($this->jsLibs[strtolower($name)])) {
1079
            $this->jsLibs[strtolower($name)] = [
1080
                'file' => $file,
1081
                'type' => $type,
1082
                'section' => self::PART_HEADER,
1083
                'compress' => $compress,
1084
                'forceOnTop' => $forceOnTop,
1085
                'allWrap' => $allWrap,
1086
                'excludeFromConcatenation' => $excludeFromConcatenation,
1087
                'splitChar' => $splitChar,
1088
                'async' => $async,
1089
                'integrity' => $integrity,
1090
                'defer' => $defer,
1091
                'crossorigin' => $crossorigin,
1092
                'nomodule' => $nomodule,
1093
            ];
1094
        }
1095
    }
1096
1097
    /**
1098
     * Adds JS Library to Footer. JS Library block is rendered on top of the Footer JS files.
1099
     *
1100
     * @param string $name Arbitrary identifier
1101
     * @param string $file File name
1102
     * @param string $type Content Type
1103
     * @param bool $compress Flag if library should be compressed
1104
     * @param bool $forceOnTop Flag if added library should be inserted at begin of this block
1105
     * @param string $allWrap
1106
     * @param bool $excludeFromConcatenation
1107
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1108
     * @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
1109
     * @param string $integrity Subresource Integrity (SRI)
1110
     * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
1111
     * @param string $crossorigin CORS settings attribute
1112
     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
1113
     */
1114
    public function addJsFooterLibrary($name, $file, $type = 'text/javascript', $compress = false, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
1115
    {
1116
        $name .= '_jsFooterLibrary';
1117
        if (!isset($this->jsLibs[strtolower($name)])) {
1118
            $this->jsLibs[strtolower($name)] = [
1119
                'file' => $file,
1120
                'type' => $type,
1121
                'section' => self::PART_FOOTER,
1122
                'compress' => $compress,
1123
                'forceOnTop' => $forceOnTop,
1124
                'allWrap' => $allWrap,
1125
                'excludeFromConcatenation' => $excludeFromConcatenation,
1126
                'splitChar' => $splitChar,
1127
                'async' => $async,
1128
                'integrity' => $integrity,
1129
                'defer' => $defer,
1130
                'crossorigin' => $crossorigin,
1131
                'nomodule' => $nomodule,
1132
            ];
1133
        }
1134
    }
1135
1136
    /**
1137
     * Adds JS file
1138
     *
1139
     * @param string $file File name
1140
     * @param string $type Content Type
1141
     * @param bool $compress
1142
     * @param bool $forceOnTop
1143
     * @param string $allWrap
1144
     * @param bool $excludeFromConcatenation
1145
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1146
     * @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
1147
     * @param string $integrity Subresource Integrity (SRI)
1148
     * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
1149
     * @param string $crossorigin CORS settings attribute
1150
     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
1151
     */
1152
    public function addJsFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
1153
    {
1154
        if (!isset($this->jsFiles[$file])) {
1155
            $this->jsFiles[$file] = [
1156
                'file' => $file,
1157
                'type' => $type,
1158
                'section' => self::PART_HEADER,
1159
                'compress' => $compress,
1160
                'forceOnTop' => $forceOnTop,
1161
                'allWrap' => $allWrap,
1162
                'excludeFromConcatenation' => $excludeFromConcatenation,
1163
                'splitChar' => $splitChar,
1164
                'async' => $async,
1165
                'integrity' => $integrity,
1166
                'defer' => $defer,
1167
                'crossorigin' => $crossorigin,
1168
                'nomodule' => $nomodule,
1169
            ];
1170
        }
1171
    }
1172
1173
    /**
1174
     * Adds JS file to footer
1175
     *
1176
     * @param string $file File name
1177
     * @param string $type Content Type
1178
     * @param bool $compress
1179
     * @param bool $forceOnTop
1180
     * @param string $allWrap
1181
     * @param bool $excludeFromConcatenation
1182
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1183
     * @param bool $async Flag if property 'async="async"' should be added to JavaScript tags
1184
     * @param string $integrity Subresource Integrity (SRI)
1185
     * @param bool $defer Flag if property 'defer="defer"' should be added to JavaScript tags
1186
     * @param string $crossorigin CORS settings attribute
1187
     * @param bool $nomodule Flag if property 'nomodule="nomodule"' should be added to JavaScript tags
1188
     */
1189
    public function addJsFooterFile($file, $type = 'text/javascript', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $async = false, $integrity = '', $defer = false, $crossorigin = '', $nomodule = false)
1190
    {
1191
        if (!isset($this->jsFiles[$file])) {
1192
            $this->jsFiles[$file] = [
1193
                'file' => $file,
1194
                'type' => $type,
1195
                'section' => self::PART_FOOTER,
1196
                'compress' => $compress,
1197
                'forceOnTop' => $forceOnTop,
1198
                'allWrap' => $allWrap,
1199
                'excludeFromConcatenation' => $excludeFromConcatenation,
1200
                'splitChar' => $splitChar,
1201
                'async' => $async,
1202
                'integrity' => $integrity,
1203
                'defer' => $defer,
1204
                'crossorigin' => $crossorigin,
1205
                'nomodule' => $nomodule,
1206
            ];
1207
        }
1208
    }
1209
1210
    /**
1211
     * Adds JS inline code
1212
     *
1213
     * @param string $name
1214
     * @param string $block
1215
     * @param bool $compress
1216
     * @param bool $forceOnTop
1217
     */
1218
    public function addJsInlineCode($name, $block, $compress = true, $forceOnTop = false)
1219
    {
1220
        if (!isset($this->jsInline[$name]) && !empty($block)) {
1221
            $this->jsInline[$name] = [
1222
                'code' => $block . LF,
1223
                'section' => self::PART_HEADER,
1224
                'compress' => $compress,
1225
                'forceOnTop' => $forceOnTop,
1226
            ];
1227
        }
1228
    }
1229
1230
    /**
1231
     * Adds JS inline code to footer
1232
     *
1233
     * @param string $name
1234
     * @param string $block
1235
     * @param bool $compress
1236
     * @param bool $forceOnTop
1237
     */
1238
    public function addJsFooterInlineCode($name, $block, $compress = true, $forceOnTop = false)
1239
    {
1240
        if (!isset($this->jsInline[$name]) && !empty($block)) {
1241
            $this->jsInline[$name] = [
1242
                'code' => $block . LF,
1243
                'section' => self::PART_FOOTER,
1244
                'compress' => $compress,
1245
                'forceOnTop' => $forceOnTop,
1246
            ];
1247
        }
1248
    }
1249
1250
    /**
1251
     * Adds CSS file
1252
     *
1253
     * @param string $file
1254
     * @param string $rel
1255
     * @param string $media
1256
     * @param string $title
1257
     * @param bool $compress
1258
     * @param bool $forceOnTop
1259
     * @param string $allWrap
1260
     * @param bool $excludeFromConcatenation
1261
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1262
     * @param bool $inline
1263
     */
1264
    public function addCssFile($file, $rel = 'stylesheet', $media = 'all', $title = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $inline = false)
1265
    {
1266
        if (!isset($this->cssFiles[$file])) {
1267
            $this->cssFiles[$file] = [
1268
                'file' => $file,
1269
                'rel' => $rel,
1270
                'media' => $media,
1271
                'title' => $title,
1272
                'compress' => $compress,
1273
                'forceOnTop' => $forceOnTop,
1274
                'allWrap' => $allWrap,
1275
                'excludeFromConcatenation' => $excludeFromConcatenation,
1276
                'splitChar' => $splitChar,
1277
                'inline' => $inline,
1278
            ];
1279
        }
1280
    }
1281
1282
    /**
1283
     * Adds CSS file
1284
     *
1285
     * @param string $file
1286
     * @param string $rel
1287
     * @param string $media
1288
     * @param string $title
1289
     * @param bool $compress
1290
     * @param bool $forceOnTop
1291
     * @param string $allWrap
1292
     * @param bool $excludeFromConcatenation
1293
     * @param string $splitChar The char used to split the allWrap value, default is "|"
1294
     * @param bool $inline
1295
     */
1296
    public function addCssLibrary($file, $rel = 'stylesheet', $media = 'all', $title = '', $compress = true, $forceOnTop = false, $allWrap = '', $excludeFromConcatenation = false, $splitChar = '|', $inline = false)
1297
    {
1298
        if (!isset($this->cssLibs[$file])) {
1299
            $this->cssLibs[$file] = [
1300
                'file' => $file,
1301
                'rel' => $rel,
1302
                'media' => $media,
1303
                'title' => $title,
1304
                'compress' => $compress,
1305
                'forceOnTop' => $forceOnTop,
1306
                'allWrap' => $allWrap,
1307
                'excludeFromConcatenation' => $excludeFromConcatenation,
1308
                'splitChar' => $splitChar,
1309
                'inline' => $inline,
1310
            ];
1311
        }
1312
    }
1313
1314
    /**
1315
     * Adds CSS inline code
1316
     *
1317
     * @param string $name
1318
     * @param string $block
1319
     * @param bool $compress
1320
     * @param bool $forceOnTop
1321
     */
1322
    public function addCssInlineBlock($name, $block, $compress = false, $forceOnTop = false)
1323
    {
1324
        if (!isset($this->cssInline[$name]) && !empty($block)) {
1325
            $this->cssInline[$name] = [
1326
                'code' => $block,
1327
                'compress' => $compress,
1328
                'forceOnTop' => $forceOnTop,
1329
            ];
1330
        }
1331
    }
1332
1333
    /**
1334
     * Call function if you need the requireJS library
1335
     * this automatically adds the JavaScript path of all loaded extensions in the requireJS path option
1336
     * so it resolves names like TYPO3/CMS/MyExtension/MyJsFile to EXT:MyExtension/Resources/Public/JavaScript/MyJsFile.js
1337
     * when using requireJS
1338
     */
1339
    public function loadRequireJs()
1340
    {
1341
        $this->addRequireJs = true;
1342
        if (!empty($this->requireJsConfig) && !empty($this->publicRequireJsConfig)) {
1343
            return;
1344
        }
1345
1346
        $packageManager = GeneralUtility::makeInstance(PackageManager::class);
1347
        $packages = $packageManager->getActivePackages();
1348
        $isDevelopment = Environment::getContext()->isDevelopment();
1349
        $cacheIdentifier = (new PackageDependentCacheIdentifier($packageManager))
1350
              ->withPrefix('RequireJS')
1351
              ->withAdditionalHashedIdentifier(($isDevelopment ? ':dev' : '') . GeneralUtility::getIndpEnv('TYPO3_REQUEST_SCRIPT'))
1352
              ->toString();
1353
        /** @var FrontendInterface $cache */
1354
        $cache = static::$cache ?? GeneralUtility::makeInstance(CacheManager::class)->getCache('assets');
1355
        $requireJsConfig = $cache->get($cacheIdentifier);
1356
1357
        // if we did not get a configuration from the cache, compute and store it in the cache
1358
        if (!isset($requireJsConfig['internal']) || !isset($requireJsConfig['public'])) {
1359
            $requireJsConfig = $this->computeRequireJsConfig($isDevelopment, $packages);
1360
            $cache->set($cacheIdentifier, $requireJsConfig);
1361
        }
1362
1363
        $this->requireJsConfig = array_merge_recursive($this->additionalRequireJsConfig, $requireJsConfig['internal']);
1364
        $this->additionalRequireJsConfig = [];
1365
        $this->publicRequireJsConfig = $requireJsConfig['public'];
1366
        $this->internalRequireJsPathModuleNames = $requireJsConfig['internalNames'];
1367
    }
1368
1369
    /**
1370
     * Computes the RequireJS configuration, mainly consisting of the paths to the core and all extension JavaScript
1371
     * resource folders plus some additional generic configuration.
1372
     *
1373
     * @param bool $isDevelopment
1374
     * @param array<string, PackageInterface> $packages
1375
     * @return array The RequireJS configuration
1376
     */
1377
    protected function computeRequireJsConfig($isDevelopment, array $packages)
1378
    {
1379
        // load all paths to map to package names / namespaces
1380
        $requireJsConfig = [
1381
            'public' => [],
1382
            'internal' => [],
1383
            'internalNames' => [],
1384
        ];
1385
1386
        $corePath = $packages['core']->getPackagePath() . 'Resources/Public/JavaScript/Contrib/';
1387
        $corePath = PathUtility::getAbsoluteWebPath($corePath);
1388
        // first, load all paths for the namespaces, and configure contrib libs.
1389
        $requireJsConfig['public']['paths'] = [
1390
            'jquery' => $corePath . 'jquery/jquery',
1391
            'jquery-ui' => $corePath . 'jquery-ui',
1392
            'nprogress' => $corePath . 'nprogress',
1393
            'moment' => $corePath . 'moment',
1394
            'cropperjs' => $corePath . 'cropper.min',
1395
            'imagesloaded' => $corePath . 'imagesloaded.pkgd.min',
1396
            'bootstrap' => $corePath . 'bootstrap/bootstrap',
1397
            'autosize' => $corePath . 'autosize',
1398
            'taboverride' => $corePath . 'taboverride.min',
1399
            'jquery/autocomplete' => $corePath . 'jquery.autocomplete',
1400
            'jquery/minicolors' => $corePath . 'jquery.minicolors',
1401
            'd3-selection' => $corePath . 'd3-selection',
1402
            'd3-drag' => $corePath . 'd3-drag',
1403
            'd3-dispatch' => $corePath . 'd3-dispatch',
1404
            'Sortable' => $corePath . 'Sortable.min', // @deprecated since v11, will be removed in v12
1405
            'sortablejs' => $corePath . 'Sortable.min',
1406
            'tablesort' => $corePath . 'tablesort',
1407
            'tablesort.dotsep' => $corePath . 'tablesort.dotsep',
1408
            'broadcastchannel' => $corePath . 'broadcastchannel-polyfill',
1409
            'flatpickr' => $corePath . 'flatpickr',
1410
        ];
1411
        $requireJsConfig['public']['shim'] = [
1412
            'tablesort.dotsep' => ['deps' => ['tablesort']],
1413
        ];
1414
        $requireJsConfig['public']['packages'] = [
1415
            [
1416
                'name' => 'lit-html',
1417
                'location' => $corePath . 'lit-html',
1418
                'main' => 'lit-html',
1419
            ],
1420
            [
1421
                'name' => '@lit/reactive-element',
1422
                'location' => $corePath . '@lit/reactive-element',
1423
                'main' => 'reactive-element',
1424
            ],
1425
            [
1426
                'name' => 'lit-element',
1427
                'location' => $corePath . 'lit-element',
1428
                'main' => 'index',
1429
            ],
1430
            [
1431
                'name' => 'lit',
1432
                'location' => $corePath . 'lit',
1433
                'main' => 'index',
1434
            ],
1435
        ];
1436
        $requireJsConfig['public']['waitSeconds'] = 30;
1437
        $requireJsConfig['public']['typo3BaseUrl'] = false;
1438
        $publicPackageNames = ['core', 'frontend', 'backend'];
1439
        $requireJsExtensionVersions = [];
1440
        foreach ($packages as $packageName => $package) {
1441
            $absoluteJsPath = $package->getPackagePath() . 'Resources/Public/JavaScript/';
1442
            $fullJsPath = PathUtility::getAbsoluteWebPath($absoluteJsPath);
1443
            $fullJsPath = rtrim($fullJsPath, '/');
1444
            if (!empty($fullJsPath) && file_exists($absoluteJsPath)) {
1445
                $type = in_array($packageName, $publicPackageNames, true) ? 'public' : 'internal';
1446
                $requireJsConfig[$type]['paths']['TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($packageName)] = $fullJsPath;
1447
                $requireJsExtensionVersions[] = $package->getPackageKey() . ':' . $package->getPackageMetadata()->getVersion();
1448
            }
1449
        }
1450
        // sanitize module names in internal 'paths'
1451
        $internalPathModuleNames = array_keys($requireJsConfig['internal']['paths'] ?? []);
1452
        $sanitizedInternalPathModuleNames = array_map(
1453
            static function ($moduleName) {
1454
                // trim spaces and slashes & add ending slash
1455
                return trim($moduleName, ' /') . '/';
1456
            },
1457
            $internalPathModuleNames
1458
        );
1459
        $requireJsConfig['internalNames'] = array_combine(
1460
            $sanitizedInternalPathModuleNames,
1461
            $internalPathModuleNames
1462
        );
1463
1464
        // Add a GET parameter to the files loaded via requireJS in order to avoid browser caching of JS files
1465
        if ($isDevelopment) {
1466
            $requireJsConfig['public']['urlArgs'] = 'bust=' . $GLOBALS['EXEC_TIME'];
1467
        } else {
1468
            $requireJsConfig['public']['urlArgs'] = 'bust=' . GeneralUtility::hmac(
1469
                Environment::getProjectPath() . implode('|', $requireJsExtensionVersions)
1470
            );
1471
        }
1472
1473
        // check if additional AMD modules need to be loaded if a single AMD module is initialized
1474
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules'] ?? false)) {
1475
            $this->addInlineSettingArray(
1476
                'RequireJS.PostInitializationModules',
1477
                $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules']
1478
            );
1479
        }
1480
1481
        return $requireJsConfig;
1482
    }
1483
1484
    /**
1485
     * Add additional configuration to require js.
1486
     *
1487
     * Configuration will be merged recursive with overrule.
1488
     *
1489
     * To add another path mapping deliver the following configuration:
1490
     * 		'paths' => array(
1491
     *			'EXTERN/mybootstrapjs' => 'sysext/.../twbs/bootstrap.min',
1492
     *      ),
1493
     *
1494
     * @param array $configuration The configuration that will be merged with existing one.
1495
     */
1496
    public function addRequireJsConfiguration(array $configuration)
1497
    {
1498
        if ($this->addRequireJs === true) {
1499
            $this->requireJsConfig = array_merge_recursive($this->requireJsConfig, $configuration);
1500
        } else {
1501
            // Delay merge until RequireJS base configuration is loaded
1502
            $this->additionalRequireJsConfig = array_merge_recursive($this->additionalRequireJsConfig, $configuration);
1503
        }
1504
    }
1505
1506
    /**
1507
     * Generates RequireJS loader HTML markup.
1508
     *
1509
     * @return string
1510
     * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
1511
     */
1512
    protected function getRequireJsLoader(): string
1513
    {
1514
        $html = '';
1515
        $backendUserLoggedIn = !empty($GLOBALS['BE_USER']->user['uid']);
1516
1517
        if (!($GLOBALS['TYPO3_REQUEST']) instanceof ServerRequestInterface
1518
            || !ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isBackend()
1519
        ) {
1520
            // no backend request - basically frontend
1521
            $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
1522
            $requireJsConfig['typo3BaseUrl'] = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . '?eID=requirejs';
1523
        } elseif (!$backendUserLoggedIn) {
1524
            // backend request, but no backend user logged in
1525
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1526
            $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
1527
            $requireJsConfig['typo3BaseUrl'] = (string)$uriBuilder->buildUriFromRoute('ajax_core_requirejs');
1528
        } else {
1529
            // Backend request, having backend user logged in.
1530
            // Merge public and private require js configuration.
1531
            // Use array_merge for 'packages' definitions (scalar array indexes) and
1532
            // merge+replace for other, string array based configuration (like 'path' and 'shim').
1533
            $requireJsConfig = ArrayUtility::replaceAndAppendScalarValuesRecursive(
1534
                $this->publicRequireJsConfig,
1535
                $this->requireJsConfig
1536
            );
1537
        }
1538
        $requireJS = RequireJS::create(
1539
            $this->processJsFile($this->requireJsPath . 'require.js'),
1540
            $requireJsConfig
1541
        );
1542
        // add (probably filtered) RequireJS configuration
1543
        if ($this->getApplicationType() === 'BE') {
1544
            $html .= sprintf(
1545
                '<script src="%s"></script>' . "\n",
1546
                htmlspecialchars($requireJS->getUri())
1547
            );
1548
            // (using dedicated instance of JavaScriptRenderer)
1549
            $javaScriptRenderer = JavaScriptRenderer::create();
1550
            $javaScriptRenderer->loadRequireJS($requireJS);
1551
            $html .= $javaScriptRenderer->render();
1552
        } else {
1553
            $html .= GeneralUtility::wrapJS('var require = ' . json_encode($requireJS->getConfig())) . LF;
1554
            // directly after that, include the require.js file
1555
            $html .= sprintf(
1556
                '<script src="%s"></script>' . "\n",
1557
                htmlspecialchars($requireJS->getUri())
1558
            );
1559
        }
1560
        // use (anonymous require.js loader), e.g. used when not having a valid TYP3 backend user session
1561
        if (!empty($requireJsConfig['typo3BaseUrl'])) {
1562
            $html .= '<script src="'
1563
                . $this->processJsFile(
1564
                    'EXT:core/Resources/Public/JavaScript/requirejs-loader.js'
1565
                )
1566
                . '"></script>' . LF;
1567
        }
1568
1569
        return $html;
1570
    }
1571
1572
    /**
1573
     * @param array $array
1574
     * @param string[] $keys
1575
     * @param bool $keep
1576
     * @return array
1577
     */
1578
    protected function filterArrayKeys(array $array, array $keys, bool $keep = true): array
1579
    {
1580
        return array_filter(
1581
            $array,
1582
            static function (string $key) use ($keys, $keep) {
1583
                return in_array($key, $keys, true) === $keep;
1584
            },
1585
            ARRAY_FILTER_USE_KEY
1586
        );
1587
    }
1588
1589
    /**
1590
     * includes an AMD-compatible JS file by resolving the ModuleName, and then requires the file via a requireJS request,
1591
     * additionally allowing to execute JavaScript code afterwards
1592
     *
1593
     * this function only works for AMD-ready JS modules, used like "define('TYPO3/CMS/Backend/FormEngine..."
1594
     * in the JS file
1595
     *
1596
     *	TYPO3/CMS/Backend/FormEngine =>
1597
     * 		"TYPO3": Vendor Name
1598
     * 		"CMS": Product Name
1599
     *		"Backend": Extension Name
1600
     *		"FormEngine": FileName in the Resources/Public/JavaScript folder
1601
     *
1602
     * @param string $mainModuleName Must be in the form of "TYPO3/CMS/PackageName/ModuleName" e.g. "TYPO3/CMS/Backend/FormEngine"
1603
     * @param string $callBackFunction loaded right after the requireJS loading, must be wrapped in function() {}
1604
     */
1605
    public function loadRequireJsModule($mainModuleName, $callBackFunction = null)
1606
    {
1607
        $inlineCodeKey = $mainModuleName;
0 ignored issues
show
Unused Code introduced by
The assignment to $inlineCodeKey is dead and can be removed.
Loading history...
1608
        // make sure requireJS is initialized
1609
        $this->loadRequireJs();
1610
        // move internal module path definition to public module definition
1611
        // (since loading a module ends up disclosing the existence anyway)
1612
        $baseModuleName = $this->findRequireJsBaseModuleName($mainModuleName);
1613
        if ($baseModuleName !== null && isset($this->requireJsConfig['paths'][$baseModuleName])) {
1614
            $this->publicRequireJsConfig['paths'][$baseModuleName] = $this->requireJsConfig['paths'][$baseModuleName];
1615
            unset($this->requireJsConfig['paths'][$baseModuleName]);
1616
        }
1617
        if ($callBackFunction === null && $this->getApplicationType() === 'BE') {
1618
            $this->javaScriptRenderer->addJavaScriptModuleInstruction(
1619
                JavaScriptModuleInstruction::forRequireJS($mainModuleName)
1620
            );
1621
            return;
1622
        }
1623
        // processing frontend application or having callback function
1624
        // @todo deprecate callback function for backend application in TYPO3 v12.0
1625
        if ($callBackFunction === null) {
1626
            // just load the main module
1627
            $inlineCodeKey = $mainModuleName;
1628
            $javaScriptCode = sprintf('require([%s]);', GeneralUtility::quoteJSvalue($mainModuleName));
1629
        } else {
1630
            // load main module and execute possible callback function
1631
            $inlineCodeKey = $mainModuleName . sha1($callBackFunction);
1632
            $javaScriptCode = sprintf('require([%s], %s);', GeneralUtility::quoteJSvalue($mainModuleName), $callBackFunction);
1633
        }
1634
        $this->addJsInlineCode('RequireJS-Module-' . $inlineCodeKey, $javaScriptCode);
1635
    }
1636
1637
    /**
1638
     * Determines requireJS base module name (if defined).
1639
     *
1640
     * @param string $moduleName
1641
     * @return string|null
1642
     */
1643
    protected function findRequireJsBaseModuleName(string $moduleName)
1644
    {
1645
        // trim spaces and slashes & add ending slash
1646
        $sanitizedModuleName = trim($moduleName, ' /') . '/';
1647
        foreach ($this->internalRequireJsPathModuleNames as $sanitizedBaseModuleName => $baseModuleName) {
1648
            if (strpos($sanitizedModuleName, $sanitizedBaseModuleName) === 0) {
1649
                return $baseModuleName;
1650
            }
1651
        }
1652
        return null;
1653
    }
1654
1655
    /**
1656
     * Adds Javascript Inline Label. This will occur in TYPO3.lang - object
1657
     * The label can be used in scripts with TYPO3.lang.<key>
1658
     *
1659
     * @param string $key
1660
     * @param string $value
1661
     */
1662
    public function addInlineLanguageLabel($key, $value)
1663
    {
1664
        $this->inlineLanguageLabels[$key] = $value;
1665
    }
1666
1667
    /**
1668
     * Adds Javascript Inline Label Array. This will occur in TYPO3.lang - object
1669
     * The label can be used in scripts with TYPO3.lang.<key>
1670
     * Array will be merged with existing array.
1671
     *
1672
     * @param array $array
1673
     */
1674
    public function addInlineLanguageLabelArray(array $array)
1675
    {
1676
        $this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $array);
1677
    }
1678
1679
    /**
1680
     * Gets labels to be used in JavaScript fetched from a locallang file.
1681
     *
1682
     * @param string $fileRef Input is a file-reference (see GeneralUtility::getFileAbsFileName). That file is expected to be a 'locallang.xlf' file containing a valid XML TYPO3 language structure.
1683
     * @param string $selectionPrefix Prefix to select the correct labels (default: '')
1684
     * @param string $stripFromSelectionName String to be removed from the label names in the output. (default: '')
1685
     */
1686
    public function addInlineLanguageLabelFile($fileRef, $selectionPrefix = '', $stripFromSelectionName = '')
1687
    {
1688
        $index = md5($fileRef . $selectionPrefix . $stripFromSelectionName);
1689
        if ($fileRef && !isset($this->inlineLanguageLabelFiles[$index])) {
1690
            $this->inlineLanguageLabelFiles[$index] = [
1691
                'fileRef' => $fileRef,
1692
                'selectionPrefix' => $selectionPrefix,
1693
                'stripFromSelectionName' => $stripFromSelectionName,
1694
            ];
1695
        }
1696
    }
1697
1698
    /**
1699
     * Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
1700
     * The label can be used in scripts with TYPO3.setting.<key>
1701
     *
1702
     * @param string $namespace
1703
     * @param string $key
1704
     * @param mixed $value
1705
     */
1706
    public function addInlineSetting($namespace, $key, $value)
1707
    {
1708
        if ($namespace) {
1709
            if (strpos($namespace, '.')) {
1710
                $parts = explode('.', $namespace);
1711
                $a = &$this->inlineSettings;
1712
                foreach ($parts as $part) {
1713
                    $a = &$a[$part];
1714
                }
1715
                $a[$key] = $value;
1716
            } else {
1717
                $this->inlineSettings[$namespace][$key] = $value;
1718
            }
1719
        } else {
1720
            $this->inlineSettings[$key] = $value;
1721
        }
1722
    }
1723
1724
    /**
1725
     * Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
1726
     * The label can be used in scripts with TYPO3.setting.<key>
1727
     * Array will be merged with existing array.
1728
     *
1729
     * @param string $namespace
1730
     * @param array $array
1731
     */
1732
    public function addInlineSettingArray($namespace, array $array)
1733
    {
1734
        if ($namespace) {
1735
            if (strpos($namespace, '.')) {
1736
                $parts = explode('.', $namespace);
1737
                $a = &$this->inlineSettings;
1738
                foreach ($parts as $part) {
1739
                    $a = &$a[$part];
1740
                }
1741
                $a = array_merge((array)$a, $array);
1742
            } else {
1743
                $this->inlineSettings[$namespace] = array_merge((array)($this->inlineSettings[$namespace] ?? []), $array);
1744
            }
1745
        } else {
1746
            $this->inlineSettings = array_merge($this->inlineSettings, $array);
1747
        }
1748
    }
1749
1750
    /**
1751
     * Adds content to body content
1752
     *
1753
     * @param string $content
1754
     */
1755
    public function addBodyContent($content)
1756
    {
1757
        $this->bodyContent .= $content;
1758
    }
1759
1760
    /*****************************************************/
1761
    /*                                                   */
1762
    /*  Render Functions                                 */
1763
    /*                                                   */
1764
    /*****************************************************/
1765
    /**
1766
     * Render the page
1767
     *
1768
     * @return string Content of rendered page
1769
     */
1770
    public function render()
1771
    {
1772
        $this->prepareRendering();
1773
        [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs] = $this->renderJavaScriptAndCss();
1774
        $metaTags = implode(LF, array_merge($this->metaTags, $this->renderMetaTagsFromAPI()));
1775
        $markerArray = $this->getPreparedMarkerArray($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs, $metaTags);
1776
        $template = $this->getTemplate();
1777
1778
        // The page renderer needs a full reset when the page was rendered
1779
        $this->reset();
1780
        $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1781
        return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
1782
    }
1783
1784
    /**
1785
     * Renders metaTags based on tags added via the API
1786
     *
1787
     * @return array
1788
     */
1789
    protected function renderMetaTagsFromAPI()
1790
    {
1791
        $metaTags = [];
1792
        $metaTagManagers = $this->metaTagRegistry->getAllManagers();
1793
1794
        foreach ($metaTagManagers as $manager => $managerObject) {
1795
            $properties = $managerObject->renderAllProperties();
1796
            if (!empty($properties)) {
1797
                $metaTags[] = $properties;
1798
            }
1799
        }
1800
        return $metaTags;
1801
    }
1802
1803
    /**
1804
     * Render the page but not the JavaScript and CSS Files
1805
     *
1806
     * @param string $substituteHash The hash that is used for the placeholder markers
1807
     * @internal
1808
     * @return string Content of rendered page
1809
     */
1810
    public function renderPageWithUncachedObjects($substituteHash)
1811
    {
1812
        $this->prepareRendering();
1813
        $markerArray = $this->getPreparedMarkerArrayForPageWithUncachedObjects($substituteHash);
1814
        $template = $this->getTemplate();
1815
        $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1816
        return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
1817
    }
1818
1819
    /**
1820
     * Renders the JavaScript and CSS files that have been added during processing
1821
     * of uncached content objects (USER_INT, COA_INT)
1822
     *
1823
     * @param string $cachedPageContent
1824
     * @param string $substituteHash The hash that is used for the variables
1825
     * @internal
1826
     * @return string
1827
     */
1828
    public function renderJavaScriptAndCssForProcessingOfUncachedContentObjects($cachedPageContent, $substituteHash)
1829
    {
1830
        $this->prepareRendering();
1831
        [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs] = $this->renderJavaScriptAndCss();
1832
        $title = $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '';
1833
        $markerArray = [
1834
            '<!-- ###TITLE' . $substituteHash . '### -->' => $title,
1835
            '<!-- ###CSS_LIBS' . $substituteHash . '### -->' => $cssLibs,
1836
            '<!-- ###CSS_INCLUDE' . $substituteHash . '### -->' => $cssFiles,
1837
            '<!-- ###CSS_INLINE' . $substituteHash . '### -->' => $cssInline,
1838
            '<!-- ###JS_INLINE' . $substituteHash . '### -->' => $jsInline,
1839
            '<!-- ###JS_INCLUDE' . $substituteHash . '### -->' => $jsFiles,
1840
            '<!-- ###JS_LIBS' . $substituteHash . '### -->' => $jsLibs,
1841
            '<!-- ###META' . $substituteHash . '### -->' => implode(LF, array_merge($this->metaTags, $this->renderMetaTagsFromAPI())),
1842
            '<!-- ###HEADERDATA' . $substituteHash . '### -->' => implode(LF, $this->headerData),
1843
            '<!-- ###FOOTERDATA' . $substituteHash . '### -->' => implode(LF, $this->footerData),
1844
            '<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->' => $jsFooterLibs,
1845
            '<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->' => $jsFooterFiles,
1846
            '<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->' => $jsFooterInline,
1847
        ];
1848
        foreach ($markerArray as $placeHolder => $content) {
1849
            $cachedPageContent = str_replace($placeHolder, $content, $cachedPageContent);
1850
        }
1851
        $this->reset();
1852
        return $cachedPageContent;
1853
    }
1854
1855
    /**
1856
     * Remove ending slashes from static header block
1857
     * if the page is being rendered as html (not xhtml)
1858
     * and define property $this->endingSlash for further use
1859
     */
1860
    protected function prepareRendering()
1861
    {
1862
        if ($this->getRenderXhtml()) {
1863
            $this->endingSlash = ' /';
1864
        } else {
1865
            $this->metaCharsetTag = str_replace(' />', '>', $this->metaCharsetTag);
1866
            $this->baseUrlTag = str_replace(' />', '>', $this->baseUrlTag);
1867
            $this->shortcutTag = str_replace(' />', '>', $this->shortcutTag);
1868
            $this->endingSlash = '';
1869
        }
1870
    }
1871
1872
    /**
1873
     * Renders all JavaScript and CSS
1874
     *
1875
     * @return array|string[]
1876
     */
1877
    protected function renderJavaScriptAndCss()
1878
    {
1879
        $this->executePreRenderHook();
1880
        $mainJsLibs = $this->renderMainJavaScriptLibraries();
1881
        if ($this->concatenateJavascript || $this->concatenateCss) {
1882
            // Do the file concatenation
1883
            $this->doConcatenate();
1884
        }
1885
        if ($this->compressCss || $this->compressJavascript) {
1886
            // Do the file compression
1887
            $this->doCompress();
1888
        }
1889
        $this->executeRenderPostTransformHook();
1890
        $cssLibs = $this->renderCssLibraries();
1891
        $cssFiles = $this->renderCssFiles();
1892
        $cssInline = $this->renderCssInline();
1893
        [$jsLibs, $jsFooterLibs] = $this->renderAdditionalJavaScriptLibraries();
1894
        [$jsFiles, $jsFooterFiles] = $this->renderJavaScriptFiles();
1895
        [$jsInline, $jsFooterInline] = $this->renderInlineJavaScript();
1896
        $jsLibs = $mainJsLibs . $jsLibs;
1897
        if ($this->moveJsFromHeaderToFooter) {
1898
            $jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
1899
            $jsLibs = '';
1900
            $jsFooterFiles = $jsFiles . LF . $jsFooterFiles;
1901
            $jsFiles = '';
1902
            $jsFooterInline = $jsInline . LF . $jsFooterInline;
1903
            $jsInline = '';
1904
        }
1905
        // Use AssetRenderer to inject all JavaScripts and CSS files
1906
        $assetRenderer = GeneralUtility::makeInstance(AssetRenderer::class);
1907
        $jsInline .= $assetRenderer->renderInlineJavaScript(true);
1908
        $jsFooterInline .= $assetRenderer->renderInlineJavaScript();
1909
        $jsFiles .= $assetRenderer->renderJavaScript(true);
1910
        $jsFooterFiles .= $assetRenderer->renderJavaScript();
1911
        $cssInline .= $assetRenderer->renderInlineStyleSheets(true);
1912
        // append inline CSS to footer (as there is no cssFooterInline)
1913
        $jsFooterFiles .= $assetRenderer->renderInlineStyleSheets();
1914
        $cssLibs .= $assetRenderer->renderStyleSheets(true, $this->endingSlash);
1915
        $cssFiles .= $assetRenderer->renderStyleSheets(false, $this->endingSlash);
1916
1917
        $this->executePostRenderHook($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs);
1918
        return [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs];
1919
    }
1920
1921
    /**
1922
     * Fills the marker array with the given strings and trims each value
1923
     *
1924
     * @param string $jsLibs
1925
     * @param string $jsFiles
1926
     * @param string $jsFooterFiles
1927
     * @param string $cssLibs
1928
     * @param string $cssFiles
1929
     * @param string $jsInline
1930
     * @param string $cssInline
1931
     * @param string $jsFooterInline
1932
     * @param string $jsFooterLibs
1933
     * @param string $metaTags
1934
     * @return array Marker array
1935
     */
1936
    protected function getPreparedMarkerArray($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs, $metaTags)
1937
    {
1938
        $markerArray = [
1939
            'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
1940
            'HTMLTAG' => $this->htmlTag,
1941
            'HEADTAG' => $this->headTag,
1942
            'METACHARSET' => $this->charSet ? str_replace('|', htmlspecialchars($this->charSet), $this->metaCharsetTag) : '',
1943
            'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
1944
            'BASEURL' => $this->baseUrl ? str_replace('|', $this->baseUrl, $this->baseUrlTag) : '',
1945
            'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
1946
            'CSS_LIBS' => $cssLibs,
1947
            'CSS_INCLUDE' => $cssFiles,
1948
            'CSS_INLINE' => $cssInline,
1949
            'JS_INLINE' => $jsInline,
1950
            'JS_INCLUDE' => $jsFiles,
1951
            'JS_LIBS' => $jsLibs,
1952
            'TITLE' => $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '',
1953
            'META' => $metaTags,
1954
            'HEADERDATA' => $this->headerData ? implode(LF, $this->headerData) : '',
1955
            'FOOTERDATA' => $this->footerData ? implode(LF, $this->footerData) : '',
1956
            'JS_LIBS_FOOTER' => $jsFooterLibs,
1957
            'JS_INCLUDE_FOOTER' => $jsFooterFiles,
1958
            'JS_INLINE_FOOTER' => $jsFooterInline,
1959
            'BODY' => $this->bodyContent,
1960
        ];
1961
        $markerArray = array_map(static fn ($item) => (trim((string)$item)), $markerArray);
1962
        return $markerArray;
1963
    }
1964
1965
    /**
1966
     * Fills the marker array with the given strings and trims each value
1967
     *
1968
     * @param string $substituteHash The hash that is used for the placeholder markers
1969
     * @return array Marker array
1970
     */
1971
    protected function getPreparedMarkerArrayForPageWithUncachedObjects($substituteHash)
1972
    {
1973
        $markerArray = [
1974
            'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
1975
            'HTMLTAG' => $this->htmlTag,
1976
            'HEADTAG' => $this->headTag,
1977
            'METACHARSET' => $this->charSet ? str_replace('|', htmlspecialchars($this->charSet), $this->metaCharsetTag) : '',
1978
            'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
1979
            'BASEURL' => $this->baseUrl ? str_replace('|', $this->baseUrl, $this->baseUrlTag) : '',
1980
            'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
1981
            'META' => '<!-- ###META' . $substituteHash . '### -->',
1982
            'BODY' => $this->bodyContent,
1983
            'TITLE' => '<!-- ###TITLE' . $substituteHash . '### -->',
1984
            'CSS_LIBS' => '<!-- ###CSS_LIBS' . $substituteHash . '### -->',
1985
            'CSS_INCLUDE' => '<!-- ###CSS_INCLUDE' . $substituteHash . '### -->',
1986
            'CSS_INLINE' => '<!-- ###CSS_INLINE' . $substituteHash . '### -->',
1987
            'JS_INLINE' => '<!-- ###JS_INLINE' . $substituteHash . '### -->',
1988
            'JS_INCLUDE' => '<!-- ###JS_INCLUDE' . $substituteHash . '### -->',
1989
            'JS_LIBS' => '<!-- ###JS_LIBS' . $substituteHash . '### -->',
1990
            'HEADERDATA' => '<!-- ###HEADERDATA' . $substituteHash . '### -->',
1991
            'FOOTERDATA' => '<!-- ###FOOTERDATA' . $substituteHash . '### -->',
1992
            'JS_LIBS_FOOTER' => '<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->',
1993
            'JS_INCLUDE_FOOTER' => '<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->',
1994
            'JS_INLINE_FOOTER' => '<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->',
1995
        ];
1996
        $markerArray = array_map(static fn ($item) => (trim((string)$item)), $markerArray);
1997
        return $markerArray;
1998
    }
1999
2000
    /**
2001
     * Reads the template file and returns the requested part as string
2002
     *
2003
     * @return string
2004
     */
2005
    protected function getTemplate(): string
2006
    {
2007
        $templateFile = GeneralUtility::getFileAbsFileName($this->templateFile);
2008
        if (is_file($templateFile)) {
2009
            $template = (string)file_get_contents($templateFile);
2010
            if ($this->removeLineBreaksFromTemplate) {
2011
                $template = strtr($template, [LF => '', CR => '']);
2012
            }
2013
        } else {
2014
            $template = '';
2015
        }
2016
        return $template;
2017
    }
2018
2019
    /**
2020
     * Helper function for render the main JavaScript libraries,
2021
     * currently: RequireJS
2022
     *
2023
     * @return string Content with JavaScript libraries
2024
     */
2025
    protected function renderMainJavaScriptLibraries()
2026
    {
2027
        $out = '';
2028
2029
        // Include RequireJS
2030
        if ($this->addRequireJs) {
2031
            $out .= $this->getRequireJsLoader();
2032
        }
2033
2034
        $this->loadJavaScriptLanguageStrings();
2035
        if ($this->getApplicationType() === 'BE') {
2036
            $noBackendUserLoggedIn = empty($GLOBALS['BE_USER']->user['uid']);
2037
            $this->addAjaxUrlsToInlineSettings($noBackendUserLoggedIn);
2038
        }
2039
        $assignments = array_filter([
2040
            'settings' => $this->inlineSettings,
2041
            'lang' => $this->parseLanguageLabelsForJavaScript(),
2042
        ]);
2043
        if ($assignments === []) {
2044
            return '';
2045
        }
2046
        if ($this->getApplicationType() === 'BE') {
2047
            $this->javaScriptRenderer->addGlobalAssignment(['TYPO3' => $assignments]);
2048
            $out .= $this->javaScriptRenderer->render();
2049
        } else {
2050
            $out .= sprintf(
2051
                "%svar TYPO3 = Object.assign(TYPO3 || {}, %s);\r\n%s",
2052
                $this->inlineJavascriptWrap[0],
2053
                // filter potential prototype pollution
2054
                sprintf(
2055
                    'Object.fromEntries(Object.entries(%s).filter((entry) => '
2056
                        . "!['__proto__', 'prototype', 'constructor'].includes(entry[0])))",
2057
                    json_encode($assignments)
2058
                ),
2059
                $this->inlineJavascriptWrap[1],
2060
            );
2061
        }
2062
        return $out;
2063
    }
2064
2065
    /**
2066
     * Converts the language labels for usage in JavaScript
2067
     *
2068
     * @return array
2069
     */
2070
    protected function parseLanguageLabelsForJavaScript(): array
2071
    {
2072
        if (empty($this->inlineLanguageLabels)) {
2073
            return [];
2074
        }
2075
2076
        $labels = [];
2077
        foreach ($this->inlineLanguageLabels as $key => $translationUnit) {
2078
            if (is_array($translationUnit)) {
2079
                $translationUnit = current($translationUnit);
2080
                $labels[$key] = $translationUnit['target'] ?? $translationUnit['source'];
2081
            } else {
2082
                $labels[$key] = $translationUnit;
2083
            }
2084
        }
2085
2086
        return $labels;
2087
    }
2088
2089
    /**
2090
     * Load the language strings into JavaScript
2091
     */
2092
    protected function loadJavaScriptLanguageStrings()
2093
    {
2094
        if (!empty($this->inlineLanguageLabelFiles)) {
2095
            foreach ($this->inlineLanguageLabelFiles as $languageLabelFile) {
2096
                $this->includeLanguageFileForInline($languageLabelFile['fileRef'], $languageLabelFile['selectionPrefix'], $languageLabelFile['stripFromSelectionName']);
2097
            }
2098
        }
2099
        $this->inlineLanguageLabelFiles = [];
2100
        // Convert settings back to UTF-8 since json_encode() only works with UTF-8:
2101
        if ($this->getCharSet() && $this->getCharSet() !== 'utf-8' && is_array($this->inlineSettings)) {
2102
            $this->convertCharsetRecursivelyToUtf8($this->inlineSettings, $this->getCharSet());
2103
        }
2104
    }
2105
2106
    /**
2107
     * Small helper function to convert charsets for arrays into utf-8
2108
     *
2109
     * @param mixed $data given by reference (string/array usually)
2110
     * @param string $fromCharset convert FROM this charset
2111
     */
2112
    protected function convertCharsetRecursivelyToUtf8(&$data, string $fromCharset)
2113
    {
2114
        foreach ($data as $key => $value) {
2115
            if (is_array($data[$key])) {
2116
                $this->convertCharsetRecursivelyToUtf8($data[$key], $fromCharset);
2117
            } elseif (is_string($data[$key])) {
2118
                $data[$key] = mb_convert_encoding($data[$key], 'utf-8', $fromCharset);
2119
            }
2120
        }
2121
    }
2122
2123
    /**
2124
     * Make URLs to all backend ajax handlers available as inline setting.
2125
     *
2126
     * @param bool $publicRoutesOnly
2127
     */
2128
    protected function addAjaxUrlsToInlineSettings(bool $publicRoutesOnly = false)
2129
    {
2130
        $ajaxUrls = [];
2131
        // Add the ajax-based routes
2132
        /** @var UriBuilder $uriBuilder */
2133
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2134
        /** @var Router $router */
2135
        $router = GeneralUtility::makeInstance(Router::class);
2136
        foreach ($router->getRoutes() as $routeIdentifier => $route) {
2137
            if ($publicRoutesOnly && $route->getOption('access') !== 'public') {
2138
                continue;
2139
            }
2140
            if ($route->getOption('ajax')) {
2141
                $uri = (string)$uriBuilder->buildUriFromRoute($routeIdentifier);
2142
                // use the shortened value in order to use this in JavaScript
2143
                $routeIdentifier = str_replace('ajax_', '', $routeIdentifier);
2144
                $ajaxUrls[$routeIdentifier] = $uri;
2145
            }
2146
        }
2147
2148
        $this->inlineSettings['ajaxUrls'] = $ajaxUrls;
2149
    }
2150
2151
    /**
2152
     * Render CSS library files
2153
     *
2154
     * @return string
2155
     */
2156
    protected function renderCssLibraries()
2157
    {
2158
        $cssFiles = '';
2159
        if (!empty($this->cssLibs)) {
2160
            foreach ($this->cssLibs as $file => $properties) {
2161
                $tag = $this->createCssTag($properties, $file);
2162
                if ($properties['forceOnTop']) {
2163
                    $cssFiles = $tag . $cssFiles;
2164
                } else {
2165
                    $cssFiles .= $tag;
2166
                }
2167
            }
2168
        }
2169
        return $cssFiles;
2170
    }
2171
2172
    /**
2173
     * Render CSS files
2174
     *
2175
     * @return string
2176
     */
2177
    protected function renderCssFiles()
2178
    {
2179
        $cssFiles = '';
2180
        if (!empty($this->cssFiles)) {
2181
            foreach ($this->cssFiles as $file => $properties) {
2182
                $tag = $this->createCssTag($properties, $file);
2183
                if ($properties['forceOnTop']) {
2184
                    $cssFiles = $tag . $cssFiles;
2185
                } else {
2186
                    $cssFiles .= $tag;
2187
                }
2188
            }
2189
        }
2190
        return $cssFiles;
2191
    }
2192
2193
    /**
2194
     * Create link (inline=0) or style (inline=1) tag
2195
     *
2196
     * @param array $properties
2197
     * @param string $file
2198
     * @return string
2199
     */
2200
    private function createCssTag(array $properties, string $file): string
2201
    {
2202
        $includeInline = $properties['inline'] ?? false;
2203
        $file = $this->getStreamlinedFileName($file, !$includeInline);
2204
        if ($includeInline && @is_file($file)) {
2205
            $tag = $this->createInlineCssTagFromFile($file, $properties);
2206
        } else {
2207
            $tag = '<link rel="' . htmlspecialchars($properties['rel'])
2208
                . '" href="' . htmlspecialchars($file)
2209
                . '" media="' . htmlspecialchars($properties['media']) . '"'
2210
                . (($properties['title'] ?? false) ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
2211
                . $this->endingSlash . '>';
2212
        }
2213
        if ($properties['allWrap']) {
2214
            $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
2215
            $tag = $wrapArr[0] . $tag . $wrapArr[1];
2216
        }
2217
        $tag .= LF;
2218
2219
        return $tag;
2220
    }
2221
2222
    /**
2223
     * Render inline CSS
2224
     *
2225
     * @return string
2226
     */
2227
    protected function renderCssInline()
2228
    {
2229
        $cssInline = '';
2230
        if (!empty($this->cssInline)) {
2231
            foreach ($this->cssInline as $name => $properties) {
2232
                $cssCode = '/*' . htmlspecialchars($name) . '*/' . LF . $properties['code'] . LF;
2233
                if ($properties['forceOnTop']) {
2234
                    $cssInline = $cssCode . $cssInline;
2235
                } else {
2236
                    $cssInline .= $cssCode;
2237
                }
2238
            }
2239
            $cssInline = $this->inlineCssWrap[0] . $cssInline . $this->inlineCssWrap[1];
2240
        }
2241
        return $cssInline;
2242
    }
2243
2244
    /**
2245
     * Render JavaScript libraries
2246
     *
2247
     * @return array|string[] jsLibs and jsFooterLibs strings
2248
     */
2249
    protected function renderAdditionalJavaScriptLibraries()
2250
    {
2251
        $jsLibs = '';
2252
        $jsFooterLibs = '';
2253
        if (!empty($this->jsLibs)) {
2254
            foreach ($this->jsLibs as $properties) {
2255
                $properties['file'] = $this->getStreamlinedFileName($properties['file']);
2256
                $type = ($properties['type'] ?? false) ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
2257
                $async = ($properties['async'] ?? false) ? ' async="async"' : '';
2258
                $defer = ($properties['defer'] ?? false) ? ' defer="defer"' : '';
2259
                $nomodule = ($properties['nomodule'] ?? false) ? ' nomodule="nomodule"' : '';
2260
                $integrity = ($properties['integrity'] ?? false) ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
2261
                $crossorigin = ($properties['crossorigin'] ?? false) ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
2262
                $tag = '<script src="' . htmlspecialchars($properties['file']) . '"' . $type . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
2263
                if ($properties['allWrap'] ?? false) {
2264
                    $wrapArr = explode(($properties['splitChar'] ?? false) ?: '|', $properties['allWrap'], 2);
2265
                    $tag = $wrapArr[0] . $tag . $wrapArr[1];
2266
                }
2267
                $tag .= LF;
2268
                if ($properties['forceOnTop'] ?? false) {
2269
                    if (($properties['section'] ?? 0) === self::PART_HEADER) {
2270
                        $jsLibs = $tag . $jsLibs;
2271
                    } else {
2272
                        $jsFooterLibs = $tag . $jsFooterLibs;
2273
                    }
2274
                } elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
2275
                    $jsLibs .= $tag;
2276
                } else {
2277
                    $jsFooterLibs .= $tag;
2278
                }
2279
            }
2280
        }
2281
        if ($this->moveJsFromHeaderToFooter) {
2282
            $jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
2283
            $jsLibs = '';
2284
        }
2285
        return [$jsLibs, $jsFooterLibs];
2286
    }
2287
2288
    /**
2289
     * Render JavaScript files
2290
     *
2291
     * @return array|string[] jsFiles and jsFooterFiles strings
2292
     */
2293
    protected function renderJavaScriptFiles()
2294
    {
2295
        $jsFiles = '';
2296
        $jsFooterFiles = '';
2297
        if (!empty($this->jsFiles)) {
2298
            foreach ($this->jsFiles as $file => $properties) {
2299
                $file = $this->getStreamlinedFileName($file);
2300
                $type = ($properties['type'] ?? false) ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
2301
                $async = ($properties['async'] ?? false) ? ' async="async"' : '';
2302
                $defer = ($properties['defer'] ?? false) ? ' defer="defer"' : '';
2303
                $nomodule = ($properties['nomodule'] ?? false) ? ' nomodule="nomodule"' : '';
2304
                $integrity = ($properties['integrity'] ?? false) ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
2305
                $crossorigin = ($properties['crossorigin'] ?? false) ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
2306
                $tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . $nomodule . '></script>';
2307
                if ($properties['allWrap'] ?? false) {
2308
                    $wrapArr = explode(($properties['splitChar'] ?? false) ?: '|', $properties['allWrap'], 2);
2309
                    $tag = $wrapArr[0] . $tag . $wrapArr[1];
2310
                }
2311
                $tag .= LF;
2312
                if ($properties['forceOnTop'] ?? false) {
2313
                    if (($properties['section'] ?? 0) === self::PART_HEADER) {
2314
                        $jsFiles = $tag . $jsFiles;
2315
                    } else {
2316
                        $jsFooterFiles = $tag . $jsFooterFiles;
2317
                    }
2318
                } elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
2319
                    $jsFiles .= $tag;
2320
                } else {
2321
                    $jsFooterFiles .= $tag;
2322
                }
2323
            }
2324
        }
2325
        if ($this->moveJsFromHeaderToFooter) {
2326
            $jsFooterFiles = $jsFiles . $jsFooterFiles;
2327
            $jsFiles = '';
2328
        }
2329
        return [$jsFiles, $jsFooterFiles];
2330
    }
2331
2332
    /**
2333
     * Render inline JavaScript
2334
     *
2335
     * @return array|string[] jsInline and jsFooterInline string
2336
     */
2337
    protected function renderInlineJavaScript()
2338
    {
2339
        $jsInline = '';
2340
        $jsFooterInline = '';
2341
        if (!empty($this->jsInline)) {
2342
            foreach ($this->jsInline as $name => $properties) {
2343
                $jsCode = '/*' . htmlspecialchars($name) . '*/' . LF . $properties['code'] . LF;
2344
                if ($properties['forceOnTop'] ?? false) {
2345
                    if (($properties['section'] ?? 0) === self::PART_HEADER) {
2346
                        $jsInline = $jsCode . $jsInline;
2347
                    } else {
2348
                        $jsFooterInline = $jsCode . $jsFooterInline;
2349
                    }
2350
                } elseif (($properties['section'] ?? 0) === self::PART_HEADER) {
2351
                    $jsInline .= $jsCode;
2352
                } else {
2353
                    $jsFooterInline .= $jsCode;
2354
                }
2355
            }
2356
        }
2357
        if ($jsInline) {
2358
            $jsInline = $this->inlineJavascriptWrap[0] . $jsInline . $this->inlineJavascriptWrap[1];
2359
        }
2360
        if ($jsFooterInline) {
2361
            $jsFooterInline = $this->inlineJavascriptWrap[0] . $jsFooterInline . $this->inlineJavascriptWrap[1];
2362
        }
2363
        if ($this->moveJsFromHeaderToFooter) {
2364
            $jsFooterInline = $jsInline . $jsFooterInline;
2365
            $jsInline = '';
2366
        }
2367
        return [$jsInline, $jsFooterInline];
2368
    }
2369
2370
    /**
2371
     * Include language file for inline usage
2372
     *
2373
     * @param string $fileRef
2374
     * @param string $selectionPrefix
2375
     * @param string $stripFromSelectionName
2376
     * @throws \RuntimeException
2377
     */
2378
    protected function includeLanguageFileForInline($fileRef, $selectionPrefix = '', $stripFromSelectionName = '')
2379
    {
2380
        if (!isset($this->lang) || !isset($this->charSet)) {
2381
            throw new \RuntimeException('Language and character encoding are not set.', 1284906026);
2382
        }
2383
        $labelsFromFile = [];
2384
        $allLabels = $this->readLLfile($fileRef);
2385
        if ($allLabels !== false) {
0 ignored issues
show
introduced by
The condition $allLabels !== false is always true.
Loading history...
2386
            // Merge language specific translations:
2387
            if ($this->lang !== 'default' && isset($allLabels[$this->lang])) {
2388
                $labels = array_merge($allLabels['default'], $allLabels[$this->lang]);
2389
            } else {
2390
                $labels = $allLabels['default'];
2391
            }
2392
            // Iterate through all locallang labels:
2393
            foreach ($labels as $label => $value) {
2394
                // If $selectionPrefix is set, only respect labels that start with $selectionPrefix
2395
                if ($selectionPrefix === '' || strpos($label, $selectionPrefix) === 0) {
2396
                    // Remove substring $stripFromSelectionName from label
2397
                    $label = str_replace($stripFromSelectionName, '', $label);
2398
                    $labelsFromFile[$label] = $value;
2399
                }
2400
            }
2401
            $this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $labelsFromFile);
2402
        }
2403
    }
2404
2405
    /**
2406
     * Reads a locallang file.
2407
     *
2408
     * @param string $fileRef Reference to a relative filename to include.
2409
     * @return array Returns the $LOCAL_LANG array found in the file. If no array found, returns empty array.
2410
     */
2411
    protected function readLLfile($fileRef)
2412
    {
2413
        /** @var LocalizationFactory $languageFactory */
2414
        $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
2415
2416
        if ($this->lang !== 'default') {
2417
            $languages = array_reverse($this->languageDependencies);
2418
            // At least we need to have English
2419
            if (empty($languages)) {
2420
                $languages[] = 'default';
2421
            }
2422
        } else {
2423
            $languages = ['default'];
2424
        }
2425
2426
        $localLanguage = [];
2427
        foreach ($languages as $language) {
2428
            $tempLL = $languageFactory->getParsedData($fileRef, $language);
2429
2430
            $localLanguage['default'] = $tempLL['default'];
2431
            if (!isset($localLanguage[$this->lang])) {
2432
                $localLanguage[$this->lang] = $localLanguage['default'];
2433
            }
2434
            if ($this->lang !== 'default' && isset($tempLL[$language])) {
2435
                // Merge current language labels onto labels from previous language
2436
                // This way we have a labels with fall back applied
2437
                ArrayUtility::mergeRecursiveWithOverrule($localLanguage[$this->lang], $tempLL[$language], true, false);
2438
            }
2439
        }
2440
2441
        return $localLanguage;
2442
    }
2443
2444
    /*****************************************************/
2445
    /*                                                   */
2446
    /*  Tools                                            */
2447
    /*                                                   */
2448
    /*****************************************************/
2449
    /**
2450
     * Concatenate files into one file
2451
     * registered handler
2452
     */
2453
    protected function doConcatenate()
2454
    {
2455
        $this->doConcatenateCss();
2456
        $this->doConcatenateJavaScript();
2457
    }
2458
2459
    /**
2460
     * Concatenate JavaScript files according to the configuration.
2461
     */
2462
    protected function doConcatenateJavaScript()
2463
    {
2464
        if ($this->concatenateJavascript) {
2465
            if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['jsConcatenateHandler'])) {
2466
                // use external concatenation routine
2467
                $params = [
2468
                    'jsLibs' => &$this->jsLibs,
2469
                    'jsFiles' => &$this->jsFiles,
2470
                    'jsFooterFiles' => &$this->jsFooterFiles,
2471
                    'headerData' => &$this->headerData,
2472
                    'footerData' => &$this->footerData,
2473
                ];
2474
                GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['jsConcatenateHandler'], $params, $this);
2475
            } else {
2476
                $this->jsLibs = $this->getCompressor()->concatenateJsFiles($this->jsLibs);
2477
                $this->jsFiles = $this->getCompressor()->concatenateJsFiles($this->jsFiles);
2478
                $this->jsFooterFiles = $this->getCompressor()->concatenateJsFiles($this->jsFooterFiles);
2479
            }
2480
        }
2481
    }
2482
2483
    /**
2484
     * Concatenate CSS files according to configuration.
2485
     */
2486
    protected function doConcatenateCss()
2487
    {
2488
        if ($this->concatenateCss) {
2489
            if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssConcatenateHandler'])) {
2490
                // use external concatenation routine
2491
                $params = [
2492
                    'cssFiles' => &$this->cssFiles,
2493
                    'cssLibs' => &$this->cssLibs,
2494
                    'headerData' => &$this->headerData,
2495
                    'footerData' => &$this->footerData,
2496
                ];
2497
                GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssConcatenateHandler'], $params, $this);
2498
            } else {
2499
                $this->cssLibs = $this->getCompressor()->concatenateCssFiles($this->cssLibs);
2500
                $this->cssFiles = $this->getCompressor()->concatenateCssFiles($this->cssFiles);
2501
            }
2502
        }
2503
    }
2504
2505
    /**
2506
     * Compresses inline code
2507
     */
2508
    protected function doCompress()
2509
    {
2510
        $this->doCompressJavaScript();
2511
        $this->doCompressCss();
2512
    }
2513
2514
    /**
2515
     * Compresses CSS according to configuration.
2516
     */
2517
    protected function doCompressCss()
2518
    {
2519
        if ($this->compressCss) {
2520
            if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssCompressHandler'])) {
2521
                // Use external compression routine
2522
                $params = [
2523
                    'cssInline' => &$this->cssInline,
2524
                    'cssFiles' => &$this->cssFiles,
2525
                    'cssLibs' => &$this->cssLibs,
2526
                    'headerData' => &$this->headerData,
2527
                    'footerData' => &$this->footerData,
2528
                ];
2529
                GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['cssCompressHandler'], $params, $this);
2530
            } else {
2531
                $this->cssLibs = $this->getCompressor()->compressCssFiles($this->cssLibs);
2532
                $this->cssFiles = $this->getCompressor()->compressCssFiles($this->cssFiles);
2533
            }
2534
        }
2535
    }
2536
2537
    /**
2538
     * Compresses JavaScript according to configuration.
2539
     */
2540
    protected function doCompressJavaScript()
2541
    {
2542
        if ($this->compressJavascript) {
2543
            if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['jsCompressHandler'])) {
2544
                // Use external compression routine
2545
                $params = [
2546
                    'jsInline' => &$this->jsInline,
2547
                    'jsFooterInline' => &$this->jsFooterInline,
2548
                    'jsLibs' => &$this->jsLibs,
2549
                    'jsFiles' => &$this->jsFiles,
2550
                    'jsFooterFiles' => &$this->jsFooterFiles,
2551
                    'headerData' => &$this->headerData,
2552
                    'footerData' => &$this->footerData,
2553
                ];
2554
                GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][$this->getApplicationType()]['jsCompressHandler'], $params, $this);
2555
            } else {
2556
                // Traverse the arrays, compress files
2557
                foreach ($this->jsInline ?? [] as $name => $properties) {
2558
                    if ($properties['compress'] ?? false) {
2559
                        $this->jsInline[$name]['code'] = $this->getCompressor()->compressJavaScriptSource($properties['code']);
2560
                    }
2561
                }
2562
                $this->jsLibs = $this->getCompressor()->compressJsFiles($this->jsLibs);
2563
                $this->jsFiles = $this->getCompressor()->compressJsFiles($this->jsFiles);
2564
                $this->jsFooterFiles = $this->getCompressor()->compressJsFiles($this->jsFooterFiles);
2565
            }
2566
        }
2567
    }
2568
2569
    /**
2570
     * Returns instance of \TYPO3\CMS\Core\Resource\ResourceCompressor
2571
     *
2572
     * @return ResourceCompressor
2573
     */
2574
    protected function getCompressor()
2575
    {
2576
        if ($this->compressor === null) {
2577
            $this->compressor = GeneralUtility::makeInstance(ResourceCompressor::class);
2578
        }
2579
        return $this->compressor;
2580
    }
2581
2582
    /**
2583
     * Processes a Javascript file dependent on the current context
2584
     *
2585
     * Adds the version number for Frontend, compresses the file for Backend
2586
     *
2587
     * @param string $filename Filename
2588
     * @return string New filename
2589
     */
2590
    protected function processJsFile($filename)
2591
    {
2592
        $filename = $this->getStreamlinedFileName($filename, false);
2593
        if ($this->compressJavascript) {
2594
            $filename = $this->getCompressor()->compressJsFile($filename);
2595
        } elseif ($this->getApplicationType() === 'FE') {
2596
            $filename = GeneralUtility::createVersionNumberedFilename($filename);
2597
        }
2598
        return $this->getAbsoluteWebPath($filename);
2599
    }
2600
2601
    /**
2602
     * This function acts as a wrapper to allow relative and paths starting with EXT: to be dealt with
2603
     * in this very case to always return the absolute web path to be included directly before output.
2604
     *
2605
     * This is mainly added so the EXT: syntax can be resolved for PageRenderer in one central place,
2606
     * and hopefully removed in the future by one standard API call.
2607
     *
2608
     * @param string $file the filename to process
2609
     * @param bool $prepareForOutput whether the file should be prepared as version numbered file and prefixed as absolute webpath
2610
     * @return string
2611
     * @internal
2612
     */
2613
    protected function getStreamlinedFileName($file, $prepareForOutput = true)
2614
    {
2615
        if (PathUtility::isExtensionPath($file)) {
2616
            $file = Environment::getPublicPath() . '/' . PathUtility::getPublicResourceWebPath($file, false);
2617
            // as the path is now absolute, make it "relative" to the current script to stay compatible
2618
            $file = PathUtility::getRelativePathTo($file) ?? '';
2619
            $file = rtrim($file, '/');
2620
        } else {
2621
            $file = GeneralUtility::resolveBackPath($file);
2622
        }
2623
        if ($prepareForOutput) {
2624
            $file = GeneralUtility::createVersionNumberedFilename($file);
2625
            $file = $this->getAbsoluteWebPath($file);
2626
        }
2627
        return $file;
2628
    }
2629
2630
    /**
2631
     * Gets absolute web path of filename for backend disposal.
2632
     * Resolving the absolute path in the frontend with conflict with
2633
     * applying config.absRefPrefix in frontend rendering process.
2634
     *
2635
     * @param string $file
2636
     * @return string
2637
     * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::setAbsRefPrefix()
2638
     */
2639
    protected function getAbsoluteWebPath(string $file): string
2640
    {
2641
        if ($this->getApplicationType() === 'FE') {
2642
            return $file;
2643
        }
2644
        return PathUtility::getAbsoluteWebPath($file);
2645
    }
2646
2647
    /*****************************************************/
2648
    /*                                                   */
2649
    /*  Hooks                                            */
2650
    /*                                                   */
2651
    /*****************************************************/
2652
    /**
2653
     * Execute PreRenderHook for possible manipulation
2654
     */
2655
    protected function executePreRenderHook()
2656
    {
2657
        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-preProcess'] ?? false;
2658
        if (!$hooks) {
2659
            return;
2660
        }
2661
        $params = [
2662
            'jsLibs' => &$this->jsLibs,
2663
            'jsFooterLibs' => &$this->jsFooterLibs,
2664
            'jsFiles' => &$this->jsFiles,
2665
            'jsFooterFiles' => &$this->jsFooterFiles,
2666
            'cssLibs' => &$this->cssLibs,
2667
            'cssFiles' => &$this->cssFiles,
2668
            'headerData' => &$this->headerData,
2669
            'footerData' => &$this->footerData,
2670
            'jsInline' => &$this->jsInline,
2671
            'jsFooterInline' => &$this->jsFooterInline,
2672
            'cssInline' => &$this->cssInline,
2673
        ];
2674
        foreach ($hooks as $hook) {
2675
            GeneralUtility::callUserFunction($hook, $params, $this);
2676
        }
2677
    }
2678
2679
    /**
2680
     * PostTransform for possible manipulation of concatenated and compressed files
2681
     */
2682
    protected function executeRenderPostTransformHook()
2683
    {
2684
        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postTransform'] ?? false;
2685
        if (!$hooks) {
2686
            return;
2687
        }
2688
        $params = [
2689
            'jsLibs' => &$this->jsLibs,
2690
            'jsFooterLibs' => &$this->jsFooterLibs,
2691
            'jsFiles' => &$this->jsFiles,
2692
            'jsFooterFiles' => &$this->jsFooterFiles,
2693
            'cssLibs' => &$this->cssLibs,
2694
            'cssFiles' => &$this->cssFiles,
2695
            'headerData' => &$this->headerData,
2696
            'footerData' => &$this->footerData,
2697
            'jsInline' => &$this->jsInline,
2698
            'jsFooterInline' => &$this->jsFooterInline,
2699
            'cssInline' => &$this->cssInline,
2700
        ];
2701
        foreach ($hooks as $hook) {
2702
            GeneralUtility::callUserFunction($hook, $params, $this);
2703
        }
2704
    }
2705
2706
    /**
2707
     * Execute postRenderHook for possible manipulation
2708
     *
2709
     * @param string $jsLibs
2710
     * @param string $jsFiles
2711
     * @param string $jsFooterFiles
2712
     * @param string $cssLibs
2713
     * @param string $cssFiles
2714
     * @param string $jsInline
2715
     * @param string $cssInline
2716
     * @param string $jsFooterInline
2717
     * @param string $jsFooterLibs
2718
     */
2719
    protected function executePostRenderHook(&$jsLibs, &$jsFiles, &$jsFooterFiles, &$cssLibs, &$cssFiles, &$jsInline, &$cssInline, &$jsFooterInline, &$jsFooterLibs)
2720
    {
2721
        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postProcess'] ?? false;
2722
        if (!$hooks) {
2723
            return;
2724
        }
2725
        $params = [
2726
            'jsLibs' => &$jsLibs,
2727
            'jsFiles' => &$jsFiles,
2728
            'jsFooterFiles' => &$jsFooterFiles,
2729
            'cssLibs' => &$cssLibs,
2730
            'cssFiles' => &$cssFiles,
2731
            'headerData' => &$this->headerData,
2732
            'footerData' => &$this->footerData,
2733
            'jsInline' => &$jsInline,
2734
            'cssInline' => &$cssInline,
2735
            'xmlPrologAndDocType' => &$this->xmlPrologAndDocType,
2736
            'htmlTag' => &$this->htmlTag,
2737
            'headTag' => &$this->headTag,
2738
            'charSet' => &$this->charSet,
2739
            'metaCharsetTag' => &$this->metaCharsetTag,
2740
            'shortcutTag' => &$this->shortcutTag,
2741
            'inlineComments' => &$this->inlineComments,
2742
            'baseUrl' => &$this->baseUrl,
2743
            'baseUrlTag' => &$this->baseUrlTag,
2744
            'favIcon' => &$this->favIcon,
2745
            'iconMimeType' => &$this->iconMimeType,
2746
            'titleTag' => &$this->titleTag,
2747
            'title' => &$this->title,
2748
            'metaTags' => &$this->metaTags,
2749
            'jsFooterInline' => &$jsFooterInline,
2750
            'jsFooterLibs' => &$jsFooterLibs,
2751
            'bodyContent' => &$this->bodyContent,
2752
        ];
2753
        foreach ($hooks as $hook) {
2754
            GeneralUtility::callUserFunction($hook, $params, $this);
2755
        }
2756
    }
2757
2758
    /**
2759
     * Creates a CSS inline tag
2760
     *
2761
     * @param string $file the filename to process
2762
     * @param array $properties
2763
     * @return string
2764
     */
2765
    protected function createInlineCssTagFromFile(string $file, array $properties): string
2766
    {
2767
        $cssInline = file_get_contents($file);
2768
        if ($cssInline === false) {
2769
            return '';
2770
        }
2771
        $cssInlineFix = $this->getPathFixer()->fixRelativeUrlPaths($cssInline, '/' . PathUtility::dirname($file) . '/');
2772
        return '<style'
2773
            . ' media="' . htmlspecialchars($properties['media']) . '"'
2774
            . ($properties['title'] ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
2775
            . '>' . LF
2776
            . '/*<![CDATA[*/' . LF . '<!-- ' . LF
2777
            . $cssInlineFix
2778
            . '-->' . LF . '/*]]>*/' . LF . '</style>' . LF;
2779
    }
2780
2781
    protected function getPathFixer(): RelativeCssPathFixer
2782
    {
2783
        return GeneralUtility::makeInstance(RelativeCssPathFixer::class);
2784
    }
2785
2786
    /**
2787
     * String 'FE' if in FrontendApplication, 'BE' otherwise (also in CLI without request object)
2788
     *
2789
     * @internal
2790
     */
2791
    public function getApplicationType(): string
2792
    {
2793
        if (
2794
            ($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface &&
2795
            ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend()
2796
        ) {
2797
            return 'FE';
2798
        }
2799
2800
        return 'BE';
2801
    }
2802
}
2803