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