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