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