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