[BUGFIX] Fix several typos in php comments
[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 'broadcastchannel' => $corePath . '/broadcastchannel-polyfill',
1340 ];
1341 $requireJsConfig['public']['waitSeconds'] = 30;
1342 $requireJsConfig['public']['typo3BaseUrl'] = false;
1343 $publicPackageNames = ['core', 'frontend', 'backend'];
1344 $requireJsExtensionVersions = [];
1345 foreach ($packages as $packageName => $package) {
1346 $absoluteJsPath = $package->getPackagePath() . 'Resources/Public/JavaScript/';
1347 $fullJsPath = PathUtility::getAbsoluteWebPath($absoluteJsPath);
1348 $fullJsPath = rtrim($fullJsPath, '/');
1349 if (!empty($fullJsPath) && file_exists($absoluteJsPath)) {
1350 $type = in_array($packageName, $publicPackageNames, true) ? 'public' : 'internal';
1351 $requireJsConfig[$type]['paths']['TYPO3/CMS/' . GeneralUtility::underscoredToUpperCamelCase($packageName)] = $fullJsPath;
1352 $requireJsExtensionVersions[] = $package->getPackageKey() . ':' . $package->getPackageMetadata()->getVersion();
1353 }
1354 }
1355 // sanitize module names in internal 'paths'
1356 $internalPathModuleNames = array_keys($requireJsConfig['internal']['paths'] ?? []);
1357 $sanitizedInternalPathModuleNames = array_map(
1358 function ($moduleName) {
1359 // trim spaces and slashes & add ending slash
1360 return trim($moduleName, ' /') . '/';
1361 },
1362 $internalPathModuleNames
1363 );
1364 $requireJsConfig['internalNames'] = array_combine(
1365 $sanitizedInternalPathModuleNames,
1366 $internalPathModuleNames
1367 );
1368
1369 // Add a GET parameter to the files loaded via requireJS in order to avoid browser caching of JS files
1370 if ($isDevelopment) {
1371 $requireJsConfig['public']['urlArgs'] = 'bust=' . $GLOBALS['EXEC_TIME'];
1372 } else {
1373 $requireJsConfig['public']['urlArgs'] = 'bust=' . GeneralUtility::hmac(
1374 Environment::getProjectPath() . implode('|', $requireJsExtensionVersions)
1375 );
1376 }
1377
1378 // check if additional AMD modules need to be loaded if a single AMD module is initialized
1379 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules'] ?? false)) {
1380 $this->addInlineSettingArray(
1381 'RequireJS.PostInitializationModules',
1382 $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['RequireJS']['postInitializationModules']
1383 );
1384 }
1385
1386 return $requireJsConfig;
1387 }
1388
1389 /**
1390 * Add additional configuration to require js.
1391 *
1392 * Configuration will be merged recursive with overrule.
1393 *
1394 * To add another path mapping deliver the following configuration:
1395 * 'paths' => array(
1396 * 'EXTERN/mybootstrapjs' => 'sysext/.../twbs/bootstrap.min',
1397 * ),
1398 *
1399 * @param array $configuration The configuration that will be merged with existing one.
1400 */
1401 public function addRequireJsConfiguration(array $configuration)
1402 {
1403 if (TYPO3_MODE === 'BE') {
1404 // Load RequireJS in backend context at first. Doing this in FE could break the output
1405 $this->loadRequireJs();
1406 }
1407 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($this->requireJsConfig, $configuration);
1408 }
1409
1410 /**
1411 * Generates RequireJS loader HTML markup.
1412 *
1413 * @return string
1414 * @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
1415 */
1416 protected function getRequireJsLoader(): string
1417 {
1418 $html = '';
1419 $backendRequest = TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_BE;
1420 $backendUserLoggedIn = !empty($GLOBALS['BE_USER']->user['uid']);
1421
1422 // no backend request - basically frontend
1423 if (!$backendRequest) {
1424 $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
1425 $requireJsConfig['typo3BaseUrl'] = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH') . '?eID=requirejs';
1426 // backend request, but no backend user logged in
1427 } elseif (!$backendUserLoggedIn) {
1428 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1429 $requireJsConfig = $this->getRequireJsConfig(static::REQUIREJS_SCOPE_CONFIG);
1430 $requireJsConfig['typo3BaseUrl'] = (string)$uriBuilder->buildUriFromRoute('ajax_core_requirejs');
1431 // backend request, having backend user logged in
1432 } else {
1433 $requireJsConfig = array_replace_recursive(
1434 $this->publicRequireJsConfig,
1435 $this->requireJsConfig
1436 );
1437 }
1438
1439 // add (probably filtered) RequireJS configuration
1440 $html .= GeneralUtility::wrapJS('var require = ' . json_encode($requireJsConfig)) . LF;
1441 // directly after that, include the require.js file
1442 $html .= '<script src="'
1443 . $this->processJsFile($this->requireJsPath . 'require.js')
1444 . '"></script>' . LF;
1445
1446 if (!empty($requireJsConfig['typo3BaseUrl'])) {
1447 $html .= '<script src="'
1448 . $this->processJsFile(
1449 'EXT:core/Resources/Public/JavaScript/requirejs-loader.js'
1450 )
1451 . '"></script>' . LF;
1452 }
1453
1454 return $html;
1455 }
1456
1457 /**
1458 * @param array $array
1459 * @param string[] $keys
1460 * @param bool $keep
1461 * @return array
1462 */
1463 protected function filterArrayKeys(array $array, array $keys, bool $keep = true): array
1464 {
1465 return array_filter(
1466 $array,
1467 function (string $key) use ($keys, $keep) {
1468 return in_array($key, $keys, true) === $keep;
1469 },
1470 ARRAY_FILTER_USE_KEY
1471 );
1472 }
1473
1474 /**
1475 * includes an AMD-compatible JS file by resolving the ModuleName, and then requires the file via a requireJS request,
1476 * additionally allowing to execute JavaScript code afterwards
1477 *
1478 * this function only works for AMD-ready JS modules, used like "define('TYPO3/CMS/Backend/FormEngine..."
1479 * in the JS file
1480 *
1481 * TYPO3/CMS/Backend/FormEngine =>
1482 * "TYPO3": Vendor Name
1483 * "CMS": Product Name
1484 * "Backend": Extension Name
1485 * "FormEngine": FileName in the Resources/Public/JavaScript folder
1486 *
1487 * @param string $mainModuleName Must be in the form of "TYPO3/CMS/PackageName/ModuleName" e.g. "TYPO3/CMS/Backend/FormEngine"
1488 * @param string $callBackFunction loaded right after the requireJS loading, must be wrapped in function() {}
1489 */
1490 public function loadRequireJsModule($mainModuleName, $callBackFunction = null)
1491 {
1492 $inlineCodeKey = $mainModuleName;
1493 // make sure requireJS is initialized
1494 $this->loadRequireJs();
1495 // move internal module path definition to public module definition
1496 // (since loading a module ends up disclosing the existence anyway)
1497 $baseModuleName = $this->findRequireJsBaseModuleName($mainModuleName);
1498 if ($baseModuleName !== null && isset($this->requireJsConfig['paths'][$baseModuleName])) {
1499 $this->publicRequireJsConfig['paths'][$baseModuleName] = $this->requireJsConfig['paths'][$baseModuleName];
1500 unset($this->requireJsConfig['paths'][$baseModuleName]);
1501 }
1502 // execute the main module, and load a possible callback function
1503 $javaScriptCode = 'require(["' . $mainModuleName . '"]';
1504 if ($callBackFunction !== null) {
1505 $inlineCodeKey .= sha1($callBackFunction);
1506 $javaScriptCode .= ', ' . $callBackFunction;
1507 }
1508 $javaScriptCode .= ');';
1509 $this->addJsInlineCode('RequireJS-Module-' . $inlineCodeKey, $javaScriptCode);
1510 }
1511
1512 /**
1513 * Determines requireJS base module name (if defined).
1514 *
1515 * @param string $moduleName
1516 * @return string|null
1517 */
1518 protected function findRequireJsBaseModuleName(string $moduleName)
1519 {
1520 // trim spaces and slashes & add ending slash
1521 $sanitizedModuleName = trim($moduleName, ' /') . '/';
1522 foreach ($this->internalRequireJsPathModuleNames as $sanitizedBaseModuleName => $baseModuleName) {
1523 if (strpos($sanitizedModuleName, $sanitizedBaseModuleName) === 0) {
1524 return $baseModuleName;
1525 }
1526 }
1527 return null;
1528 }
1529
1530 /**
1531 * Adds Javascript Inline Label. This will occur in TYPO3.lang - object
1532 * The label can be used in scripts with TYPO3.lang.<key>
1533 *
1534 * @param string $key
1535 * @param string $value
1536 */
1537 public function addInlineLanguageLabel($key, $value)
1538 {
1539 $this->inlineLanguageLabels[$key] = $value;
1540 }
1541
1542 /**
1543 * Adds Javascript Inline Label Array. This will occur in TYPO3.lang - object
1544 * The label can be used in scripts with TYPO3.lang.<key>
1545 * Array will be merged with existing array.
1546 *
1547 * @param array $array
1548 */
1549 public function addInlineLanguageLabelArray(array $array)
1550 {
1551 $this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $array);
1552 }
1553
1554 /**
1555 * Gets labels to be used in JavaScript fetched from a locallang file.
1556 *
1557 * @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.
1558 * @param string $selectionPrefix Prefix to select the correct labels (default: '')
1559 * @param string $stripFromSelectionName String to be removed from the label names in the output. (default: '')
1560 */
1561 public function addInlineLanguageLabelFile($fileRef, $selectionPrefix = '', $stripFromSelectionName = '')
1562 {
1563 $index = md5($fileRef . $selectionPrefix . $stripFromSelectionName);
1564 if ($fileRef && !isset($this->inlineLanguageLabelFiles[$index])) {
1565 $this->inlineLanguageLabelFiles[$index] = [
1566 'fileRef' => $fileRef,
1567 'selectionPrefix' => $selectionPrefix,
1568 'stripFromSelectionName' => $stripFromSelectionName
1569 ];
1570 }
1571 }
1572
1573 /**
1574 * Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
1575 * The label can be used in scripts with TYPO3.setting.<key>
1576 *
1577 * @param string $namespace
1578 * @param string $key
1579 * @param mixed $value
1580 */
1581 public function addInlineSetting($namespace, $key, $value)
1582 {
1583 if ($namespace) {
1584 if (strpos($namespace, '.')) {
1585 $parts = explode('.', $namespace);
1586 $a = &$this->inlineSettings;
1587 foreach ($parts as $part) {
1588 $a = &$a[$part];
1589 }
1590 $a[$key] = $value;
1591 } else {
1592 $this->inlineSettings[$namespace][$key] = $value;
1593 }
1594 } else {
1595 $this->inlineSettings[$key] = $value;
1596 }
1597 }
1598
1599 /**
1600 * Adds Javascript Inline Setting. This will occur in TYPO3.settings - object
1601 * The label can be used in scripts with TYPO3.setting.<key>
1602 * Array will be merged with existing array.
1603 *
1604 * @param string $namespace
1605 * @param array $array
1606 */
1607 public function addInlineSettingArray($namespace, array $array)
1608 {
1609 if ($namespace) {
1610 if (strpos($namespace, '.')) {
1611 $parts = explode('.', $namespace);
1612 $a = &$this->inlineSettings;
1613 foreach ($parts as $part) {
1614 $a = &$a[$part];
1615 }
1616 $a = array_merge((array)$a, $array);
1617 } else {
1618 $this->inlineSettings[$namespace] = array_merge((array)$this->inlineSettings[$namespace], $array);
1619 }
1620 } else {
1621 $this->inlineSettings = array_merge($this->inlineSettings, $array);
1622 }
1623 }
1624
1625 /**
1626 * Adds content to body content
1627 *
1628 * @param string $content
1629 */
1630 public function addBodyContent($content)
1631 {
1632 $this->bodyContent .= $content;
1633 }
1634
1635 /*****************************************************/
1636 /* */
1637 /* Render Functions */
1638 /* */
1639 /*****************************************************/
1640 /**
1641 * Render the section (Header or Footer)
1642 *
1643 * @param int $part Section which should be rendered: self::PART_COMPLETE, self::PART_HEADER or self::PART_FOOTER
1644 * @return string Content of rendered section
1645 */
1646 public function render($part = self::PART_COMPLETE)
1647 {
1648 $this->prepareRendering();
1649 list($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs) = $this->renderJavaScriptAndCss();
1650 $metaTags = implode(LF, array_merge($this->metaTags, $this->renderMetaTagsFromAPI()));
1651 $markerArray = $this->getPreparedMarkerArray($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs, $metaTags);
1652 $template = $this->getTemplateForPart($part);
1653
1654 // The page renderer needs a full reset, even when only rendering one part of the page
1655 // This means that you can only register footer files *after* the header has been already rendered.
1656 // In case you render the footer part first, header files can only be added *after* the footer has been rendered
1657 $this->reset();
1658 $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1659 return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
1660 }
1661
1662 /**
1663 * Renders metaTags based on tags added via the API
1664 *
1665 * @return array
1666 */
1667 protected function renderMetaTagsFromAPI()
1668 {
1669 $metaTags = [];
1670 $metaTagManagers = $this->metaTagRegistry->getAllManagers();
1671
1672 foreach ($metaTagManagers as $manager => $managerObject) {
1673 $properties = $managerObject->renderAllProperties();
1674 if (!empty($properties)) {
1675 $metaTags[] = $properties;
1676 }
1677 }
1678 return $metaTags;
1679 }
1680
1681 /**
1682 * Render the page but not the JavaScript and CSS Files
1683 *
1684 * @param string $substituteHash The hash that is used for the placeholder markers
1685 * @internal
1686 * @return string Content of rendered section
1687 */
1688 public function renderPageWithUncachedObjects($substituteHash)
1689 {
1690 $this->prepareRendering();
1691 $markerArray = $this->getPreparedMarkerArrayForPageWithUncachedObjects($substituteHash);
1692 $template = $this->getTemplateForPart(self::PART_COMPLETE);
1693 $templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
1694 return trim($templateService->substituteMarkerArray($template, $markerArray, '###|###'));
1695 }
1696
1697 /**
1698 * Renders the JavaScript and CSS files that have been added during processing
1699 * of uncached content objects (USER_INT, COA_INT)
1700 *
1701 * @param string $cachedPageContent
1702 * @param string $substituteHash The hash that is used for the variables
1703 * @internal
1704 * @return string
1705 */
1706 public function renderJavaScriptAndCssForProcessingOfUncachedContentObjects($cachedPageContent, $substituteHash)
1707 {
1708 $this->prepareRendering();
1709 list($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs) = $this->renderJavaScriptAndCss();
1710 $title = $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '';
1711 $markerArray = [
1712 '<!-- ###TITLE' . $substituteHash . '### -->' => $title,
1713 '<!-- ###CSS_LIBS' . $substituteHash . '### -->' => $cssLibs,
1714 '<!-- ###CSS_INCLUDE' . $substituteHash . '### -->' => $cssFiles,
1715 '<!-- ###CSS_INLINE' . $substituteHash . '### -->' => $cssInline,
1716 '<!-- ###JS_INLINE' . $substituteHash . '### -->' => $jsInline,
1717 '<!-- ###JS_INCLUDE' . $substituteHash . '### -->' => $jsFiles,
1718 '<!-- ###JS_LIBS' . $substituteHash . '### -->' => $jsLibs,
1719 '<!-- ###META' . $substituteHash . '### -->' => implode(LF, array_merge($this->metaTags, $this->renderMetaTagsFromAPI())),
1720 '<!-- ###HEADERDATA' . $substituteHash . '### -->' => implode(LF, $this->headerData),
1721 '<!-- ###FOOTERDATA' . $substituteHash . '### -->' => implode(LF, $this->footerData),
1722 '<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->' => $jsFooterLibs,
1723 '<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->' => $jsFooterFiles,
1724 '<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->' => $jsFooterInline
1725 ];
1726 foreach ($markerArray as $placeHolder => $content) {
1727 $cachedPageContent = str_replace($placeHolder, $content, $cachedPageContent);
1728 }
1729 $this->reset();
1730 return $cachedPageContent;
1731 }
1732
1733 /**
1734 * Remove ending slashes from static header block
1735 * if the page is being rendered as html (not xhtml)
1736 * and define property $this->endingSlash for further use
1737 */
1738 protected function prepareRendering()
1739 {
1740 if ($this->getRenderXhtml()) {
1741 $this->endingSlash = ' /';
1742 } else {
1743 $this->metaCharsetTag = str_replace(' />', '>', $this->metaCharsetTag);
1744 $this->baseUrlTag = str_replace(' />', '>', $this->baseUrlTag);
1745 $this->shortcutTag = str_replace(' />', '>', $this->shortcutTag);
1746 $this->endingSlash = '';
1747 }
1748 }
1749
1750 /**
1751 * Renders all JavaScript and CSS
1752 *
1753 * @return array|string[]
1754 */
1755 protected function renderJavaScriptAndCss()
1756 {
1757 $this->executePreRenderHook();
1758 $mainJsLibs = $this->renderMainJavaScriptLibraries();
1759 if ($this->concatenateJavascript || $this->concatenateCss) {
1760 // Do the file concatenation
1761 $this->doConcatenate();
1762 }
1763 if ($this->compressCss || $this->compressJavascript) {
1764 // Do the file compression
1765 $this->doCompress();
1766 }
1767 $this->executeRenderPostTransformHook();
1768 $cssLibs = $this->renderCssLibraries();
1769 $cssFiles = $this->renderCssFiles();
1770 $cssInline = $this->renderCssInline();
1771 list($jsLibs, $jsFooterLibs) = $this->renderAdditionalJavaScriptLibraries();
1772 list($jsFiles, $jsFooterFiles) = $this->renderJavaScriptFiles();
1773 list($jsInline, $jsFooterInline) = $this->renderInlineJavaScript();
1774 $jsLibs = $mainJsLibs . $jsLibs;
1775 if ($this->moveJsFromHeaderToFooter) {
1776 $jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
1777 $jsLibs = '';
1778 $jsFooterFiles = $jsFiles . LF . $jsFooterFiles;
1779 $jsFiles = '';
1780 $jsFooterInline = $jsInline . LF . $jsFooterInline;
1781 $jsInline = '';
1782 }
1783 $this->executePostRenderHook($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs);
1784 return [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs];
1785 }
1786
1787 /**
1788 * Fills the marker array with the given strings and trims each value
1789 *
1790 * @param string $jsLibs
1791 * @param string $jsFiles
1792 * @param string $jsFooterFiles
1793 * @param string $cssLibs
1794 * @param string $cssFiles
1795 * @param string $jsInline
1796 * @param string $cssInline
1797 * @param string $jsFooterInline
1798 * @param string $jsFooterLibs
1799 * @param string $metaTags
1800 * @return array Marker array
1801 */
1802 protected function getPreparedMarkerArray($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs, $metaTags)
1803 {
1804 $markerArray = [
1805 'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
1806 'HTMLTAG' => $this->htmlTag,
1807 'HEADTAG' => $this->headTag,
1808 'METACHARSET' => $this->charSet ? str_replace('|', htmlspecialchars($this->charSet), $this->metaCharsetTag) : '',
1809 'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
1810 'BASEURL' => $this->baseUrl ? str_replace('|', $this->baseUrl, $this->baseUrlTag) : '',
1811 'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
1812 'CSS_LIBS' => $cssLibs,
1813 'CSS_INCLUDE' => $cssFiles,
1814 'CSS_INLINE' => $cssInline,
1815 'JS_INLINE' => $jsInline,
1816 'JS_INCLUDE' => $jsFiles,
1817 'JS_LIBS' => $jsLibs,
1818 'TITLE' => $this->title ? str_replace('|', htmlspecialchars($this->title), $this->titleTag) : '',
1819 'META' => $metaTags,
1820 'HEADERDATA' => $this->headerData ? implode(LF, $this->headerData) : '',
1821 'FOOTERDATA' => $this->footerData ? implode(LF, $this->footerData) : '',
1822 'JS_LIBS_FOOTER' => $jsFooterLibs,
1823 'JS_INCLUDE_FOOTER' => $jsFooterFiles,
1824 'JS_INLINE_FOOTER' => $jsFooterInline,
1825 'BODY' => $this->bodyContent
1826 ];
1827 $markerArray = array_map('trim', $markerArray);
1828 return $markerArray;
1829 }
1830
1831 /**
1832 * Fills the marker array with the given strings and trims each value
1833 *
1834 * @param string $substituteHash The hash that is used for the placeholder markers
1835 * @return array Marker array
1836 */
1837 protected function getPreparedMarkerArrayForPageWithUncachedObjects($substituteHash)
1838 {
1839 $markerArray = [
1840 'XMLPROLOG_DOCTYPE' => $this->xmlPrologAndDocType,
1841 'HTMLTAG' => $this->htmlTag,
1842 'HEADTAG' => $this->headTag,
1843 'METACHARSET' => $this->charSet ? str_replace('|', htmlspecialchars($this->charSet), $this->metaCharsetTag) : '',
1844 'INLINECOMMENT' => $this->inlineComments ? LF . LF . '<!-- ' . LF . implode(LF, $this->inlineComments) . '-->' . LF . LF : '',
1845 'BASEURL' => $this->baseUrl ? str_replace('|', $this->baseUrl, $this->baseUrlTag) : '',
1846 'SHORTCUT' => $this->favIcon ? sprintf($this->shortcutTag, htmlspecialchars($this->favIcon), $this->iconMimeType) : '',
1847 'META' => '<!-- ###META' . $substituteHash . '### -->',
1848 'BODY' => $this->bodyContent,
1849 'TITLE' => '<!-- ###TITLE' . $substituteHash . '### -->',
1850 'CSS_LIBS' => '<!-- ###CSS_LIBS' . $substituteHash . '### -->',
1851 'CSS_INCLUDE' => '<!-- ###CSS_INCLUDE' . $substituteHash . '### -->',
1852 'CSS_INLINE' => '<!-- ###CSS_INLINE' . $substituteHash . '### -->',
1853 'JS_INLINE' => '<!-- ###JS_INLINE' . $substituteHash . '### -->',
1854 'JS_INCLUDE' => '<!-- ###JS_INCLUDE' . $substituteHash . '### -->',
1855 'JS_LIBS' => '<!-- ###JS_LIBS' . $substituteHash . '### -->',
1856 'HEADERDATA' => '<!-- ###HEADERDATA' . $substituteHash . '### -->',
1857 'FOOTERDATA' => '<!-- ###FOOTERDATA' . $substituteHash . '### -->',
1858 'JS_LIBS_FOOTER' => '<!-- ###JS_LIBS_FOOTER' . $substituteHash . '### -->',
1859 'JS_INCLUDE_FOOTER' => '<!-- ###JS_INCLUDE_FOOTER' . $substituteHash . '### -->',
1860 'JS_INLINE_FOOTER' => '<!-- ###JS_INLINE_FOOTER' . $substituteHash . '### -->'
1861 ];
1862 $markerArray = array_map('trim', $markerArray);
1863 return $markerArray;
1864 }
1865
1866 /**
1867 * Reads the template file and returns the requested part as string
1868 *
1869 * @param int $part
1870 * @return string
1871 */
1872 protected function getTemplateForPart($part)
1873 {
1874 $templateFile = GeneralUtility::getFileAbsFileName($this->templateFile);
1875 if (is_file($templateFile)) {
1876 $template = file_get_contents($templateFile);
1877 if ($this->removeLineBreaksFromTemplate) {
1878 $template = strtr($template, [LF => '', CR => '']);
1879 }
1880 if ($part !== self::PART_COMPLETE) {
1881 $templatePart = explode('###BODY###', $template);
1882 $template = $templatePart[$part - 1];
1883 }
1884 } else {
1885 $template = '';
1886 }
1887 return $template;
1888 }
1889
1890 /**
1891 * Helper function for render the main JavaScript libraries,
1892 * currently: RequireJS
1893 *
1894 * @return string Content with JavaScript libraries
1895 */
1896 protected function renderMainJavaScriptLibraries()
1897 {
1898 $out = '';
1899
1900 // Include RequireJS
1901 if ($this->addRequireJs) {
1902 $out .= $this->getRequireJsLoader();
1903 }
1904
1905 $this->loadJavaScriptLanguageStrings();
1906 if (TYPO3_MODE === 'BE') {
1907 $noBackendUserLoggedIn = empty($GLOBALS['BE_USER']->user['uid']);
1908 $this->addAjaxUrlsToInlineSettings($noBackendUserLoggedIn);
1909 }
1910 $inlineSettings = '';
1911 $languageLabels = $this->parseLanguageLabelsForJavaScript();
1912 if (!empty($languageLabels)) {
1913 $inlineSettings .= 'TYPO3.lang = ' . json_encode($languageLabels) . ';';
1914 }
1915 $inlineSettings .= $this->inlineSettings ? 'TYPO3.settings = ' . json_encode($this->inlineSettings) . ';' : '';
1916
1917 if ($inlineSettings !== '') {
1918 // make sure the global TYPO3 is available
1919 $inlineSettings = 'var TYPO3 = TYPO3 || {};' . CRLF . $inlineSettings;
1920 $out .= $this->inlineJavascriptWrap[0] . $inlineSettings . $this->inlineJavascriptWrap[1];
1921 }
1922
1923 return $out;
1924 }
1925
1926 /**
1927 * Converts the language labels for usage in JavaScript
1928 *
1929 * @return array
1930 */
1931 protected function parseLanguageLabelsForJavaScript(): array
1932 {
1933 if (empty($this->inlineLanguageLabels)) {
1934 return [];
1935 }
1936
1937 $labels = [];
1938 foreach ($this->inlineLanguageLabels as $key => $translationUnit) {
1939 if (is_array($translationUnit)) {
1940 $translationUnit = current($translationUnit);
1941 $labels[$key] = $translationUnit['target'] ?? $translationUnit['source'];
1942 } else {
1943 $labels[$key] = $translationUnit;
1944 }
1945 }
1946
1947 return $labels;
1948 }
1949
1950 /**
1951 * Load the language strings into JavaScript
1952 */
1953 protected function loadJavaScriptLanguageStrings()
1954 {
1955 if (!empty($this->inlineLanguageLabelFiles)) {
1956 foreach ($this->inlineLanguageLabelFiles as $languageLabelFile) {
1957 $this->includeLanguageFileForInline($languageLabelFile['fileRef'], $languageLabelFile['selectionPrefix'], $languageLabelFile['stripFromSelectionName']);
1958 }
1959 }
1960 $this->inlineLanguageLabelFiles = [];
1961 // Convert settings back to UTF-8 since json_encode() only works with UTF-8:
1962 if ($this->getCharSet() && $this->getCharSet() !== 'utf-8' && is_array($this->inlineSettings)) {
1963 $this->convertCharsetRecursivelyToUtf8($this->inlineSettings, $this->getCharSet());
1964 }
1965 }
1966
1967 /**
1968 * Small helper function to convert charsets for arrays into utf-8
1969 *
1970 * @param mixed $data given by reference (string/array usually)
1971 * @param string $fromCharset convert FROM this charset
1972 */
1973 protected function convertCharsetRecursivelyToUtf8(&$data, string $fromCharset)
1974 {
1975 foreach ($data as $key => $value) {
1976 if (is_array($data[$key])) {
1977 $this->convertCharsetRecursivelyToUtf8($data[$key], $fromCharset);
1978 } elseif (is_string($data[$key])) {
1979 $data[$key] = mb_convert_encoding($data[$key], 'utf-8', $fromCharset);
1980 }
1981 }
1982 }
1983
1984 /**
1985 * Make URLs to all backend ajax handlers available as inline setting.
1986 *
1987 * @param bool $publicRoutesOnly
1988 */
1989 protected function addAjaxUrlsToInlineSettings(bool $publicRoutesOnly = false)
1990 {
1991 $ajaxUrls = [];
1992 // Add the ajax-based routes
1993 /** @var UriBuilder $uriBuilder */
1994 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
1995 /** @var Router $router */
1996 $router = GeneralUtility::makeInstance(Router::class);
1997 $routes = $router->getRoutes();
1998 foreach ($routes as $routeIdentifier => $route) {
1999 if ($publicRoutesOnly && $route->getOption('access') !== 'public') {
2000 continue;
2001 }
2002 if ($route->getOption('ajax')) {
2003 $uri = (string)$uriBuilder->buildUriFromRoute($routeIdentifier);
2004 // use the shortened value in order to use this in JavaScript
2005 $routeIdentifier = str_replace('ajax_', '', $routeIdentifier);
2006 $ajaxUrls[$routeIdentifier] = $uri;
2007 }
2008 }
2009
2010 $this->inlineSettings['ajaxUrls'] = $ajaxUrls;
2011 }
2012
2013 /**
2014 * Render CSS library files
2015 *
2016 * @return string
2017 */
2018 protected function renderCssLibraries()
2019 {
2020 $cssFiles = '';
2021 if (!empty($this->cssLibs)) {
2022 foreach ($this->cssLibs as $file => $properties) {
2023 $tag = $this->createCssTag($properties, $file);
2024 if ($properties['forceOnTop']) {
2025 $cssFiles = $tag . $cssFiles;
2026 } else {
2027 $cssFiles .= $tag;
2028 }
2029 }
2030 }
2031 return $cssFiles;
2032 }
2033
2034 /**
2035 * Render CSS files
2036 *
2037 * @return string
2038 */
2039 protected function renderCssFiles()
2040 {
2041 $cssFiles = '';
2042 if (!empty($this->cssFiles)) {
2043 foreach ($this->cssFiles as $file => $properties) {
2044 $tag = $this->createCssTag($properties, $file);
2045 if ($properties['forceOnTop']) {
2046 $cssFiles = $tag . $cssFiles;
2047 } else {
2048 $cssFiles .= $tag;
2049 }
2050 }
2051 }
2052 return $cssFiles;
2053 }
2054
2055 /**
2056 * Create link (inline=0) or style (inline=1) tag
2057 *
2058 * @param array $properties
2059 * @param string $file
2060 * @return string
2061 */
2062 private function createCssTag(array $properties, string $file): string
2063 {
2064 if ($properties['inline'] && @is_file($file)) {
2065 $tag = $this->createInlineCssTagFromFile($file, $properties);
2066 } else {
2067 $href = $this->getStreamlinedFileName($file);
2068 $tag = '<link rel="' . htmlspecialchars($properties['rel'])
2069 . '" type="text/css" href="' . htmlspecialchars($href)
2070 . '" media="' . htmlspecialchars($properties['media']) . '"'
2071 . ($properties['title'] ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
2072 . $this->endingSlash . '>';
2073 }
2074 if ($properties['allWrap']) {
2075 $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
2076 $tag = $wrapArr[0] . $tag . $wrapArr[1];
2077 }
2078 $tag .= LF;
2079
2080 return $tag;
2081 }
2082
2083 /**
2084 * Render inline CSS
2085 *
2086 * @return string
2087 */
2088 protected function renderCssInline()
2089 {
2090 $cssInline = '';
2091 if (!empty($this->cssInline)) {
2092 foreach ($this->cssInline as $name => $properties) {
2093 $cssCode = '/*' . htmlspecialchars($name) . '*/' . LF . $properties['code'] . LF;
2094 if ($properties['forceOnTop']) {
2095 $cssInline = $cssCode . $cssInline;
2096 } else {
2097 $cssInline .= $cssCode;
2098 }
2099 }
2100 $cssInline = $this->inlineCssWrap[0] . $cssInline . $this->inlineCssWrap[1];
2101 }
2102 return $cssInline;
2103 }
2104
2105 /**
2106 * Render JavaScript libraries
2107 *
2108 * @return array|string[] jsLibs and jsFooterLibs strings
2109 */
2110 protected function renderAdditionalJavaScriptLibraries()
2111 {
2112 $jsLibs = '';
2113 $jsFooterLibs = '';
2114 if (!empty($this->jsLibs)) {
2115 foreach ($this->jsLibs as $properties) {
2116 $properties['file'] = $this->getStreamlinedFileName($properties['file']);
2117 $async = $properties['async'] ? ' async="async"' : '';
2118 $defer = $properties['defer'] ? ' defer="defer"' : '';
2119 $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
2120 $crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
2121 $tag = '<script src="' . htmlspecialchars($properties['file']) . '" type="' . htmlspecialchars($properties['type']) . '"' . $async . $defer . $integrity . $crossorigin . '></script>';
2122 if ($properties['allWrap']) {
2123 $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
2124 $tag = $wrapArr[0] . $tag . $wrapArr[1];
2125 }
2126 $tag .= LF;
2127 if ($properties['forceOnTop']) {
2128 if ($properties['section'] === self::PART_HEADER) {
2129 $jsLibs = $tag . $jsLibs;
2130 } else {
2131 $jsFooterLibs = $tag . $jsFooterLibs;
2132 }
2133 } else {
2134 if ($properties['section'] === self::PART_HEADER) {
2135 $jsLibs .= $tag;
2136 } else {
2137 $jsFooterLibs .= $tag;
2138 }
2139 }
2140 }
2141 }
2142 if ($this->moveJsFromHeaderToFooter) {
2143 $jsFooterLibs = $jsLibs . LF . $jsFooterLibs;
2144 $jsLibs = '';
2145 }
2146 return [$jsLibs, $jsFooterLibs];
2147 }
2148
2149 /**
2150 * Render JavaScript files
2151 *
2152 * @return array|string[] jsFiles and jsFooterFiles strings
2153 */
2154 protected function renderJavaScriptFiles()
2155 {
2156 $jsFiles = '';
2157 $jsFooterFiles = '';
2158 if (!empty($this->jsFiles)) {
2159 foreach ($this->jsFiles as $file => $properties) {
2160 $file = $this->getStreamlinedFileName($file);
2161 $type = $properties['type'] ? ' type="' . htmlspecialchars($properties['type']) . '"' : '';
2162 $async = $properties['async'] ? ' async="async"' : '';
2163 $defer = $properties['defer'] ? ' defer="defer"' : '';
2164 $integrity = $properties['integrity'] ? ' integrity="' . htmlspecialchars($properties['integrity']) . '"' : '';
2165 $crossorigin = $properties['crossorigin'] ? ' crossorigin="' . htmlspecialchars($properties['crossorigin']) . '"' : '';
2166 $tag = '<script src="' . htmlspecialchars($file) . '"' . $type . $async . $defer . $integrity . $crossorigin . '></script>';
2167 if ($properties['allWrap']) {
2168 $wrapArr = explode($properties['splitChar'] ?: '|', $properties['allWrap'], 2);
2169 $tag = $wrapArr[0] . $tag . $wrapArr[1];
2170 }
2171 $tag .= LF;
2172 if ($properties['forceOnTop']) {
2173 if ($properties['section'] === self::PART_HEADER) {
2174 $jsFiles = $tag . $jsFiles;
2175 } else {
2176 $jsFooterFiles = $tag . $jsFooterFiles;
2177 }
2178 } else {
2179 if ($properties['section'] === self::PART_HEADER) {
2180 $jsFiles .= $tag;
2181 } else {
2182 $jsFooterFiles .= $tag;
2183 }
2184 }
2185 }
2186 }
2187 if ($this->moveJsFromHeaderToFooter) {
2188 $jsFooterFiles = $jsFiles . $jsFooterFiles;
2189 $jsFiles = '';
2190 }
2191 return [$jsFiles, $jsFooterFiles];
2192 }
2193
2194 /**
2195 * Render inline JavaScript
2196 *
2197 * @return array|string[] jsInline and jsFooterInline string
2198 */
2199 protected function renderInlineJavaScript()
2200 {
2201 $jsInline = '';
2202 $jsFooterInline = '';
2203 if (!empty($this->jsInline)) {
2204 foreach ($this->jsInline as $name => $properties) {
2205 $jsCode = '/*' . htmlspecialchars($name) . '*/' . LF . $properties['code'] . LF;
2206 if ($properties['forceOnTop']) {
2207 if ($properties['section'] === self::PART_HEADER) {
2208 $jsInline = $jsCode . $jsInline;
2209 } else {
2210 $jsFooterInline = $jsCode . $jsFooterInline;
2211 }
2212 } else {
2213 if ($properties['section'] === self::PART_HEADER) {
2214 $jsInline .= $jsCode;
2215 } else {
2216 $jsFooterInline .= $jsCode;
2217 }
2218 }
2219 }
2220 }
2221 if ($jsInline) {
2222 $jsInline = $this->inlineJavascriptWrap[0] . $jsInline . $this->inlineJavascriptWrap[1];
2223 }
2224 if ($jsFooterInline) {
2225 $jsFooterInline = $this->inlineJavascriptWrap[0] . $jsFooterInline . $this->inlineJavascriptWrap[1];
2226 }
2227 if ($this->moveJsFromHeaderToFooter) {
2228 $jsFooterInline = $jsInline . $jsFooterInline;
2229 $jsInline = '';
2230 }
2231 return [$jsInline, $jsFooterInline];
2232 }
2233
2234 /**
2235 * Include language file for inline usage
2236 *
2237 * @param string $fileRef
2238 * @param string $selectionPrefix
2239 * @param string $stripFromSelectionName
2240 * @throws \RuntimeException
2241 */
2242 protected function includeLanguageFileForInline($fileRef, $selectionPrefix = '', $stripFromSelectionName = '')
2243 {
2244 if (!isset($this->lang) || !isset($this->charSet)) {
2245 throw new \RuntimeException('Language and character encoding are not set.', 1284906026);
2246 }
2247 $labelsFromFile = [];
2248 $allLabels = $this->readLLfile($fileRef);
2249 if ($allLabels !== false) {
2250 // Merge language specific translations:
2251 if ($this->lang !== 'default' && isset($allLabels[$this->lang])) {
2252 $labels = array_merge($allLabels['default'], $allLabels[$this->lang]);
2253 } else {
2254 $labels = $allLabels['default'];
2255 }
2256 // Iterate through all locallang labels:
2257 foreach ($labels as $label => $value) {
2258 // If $selectionPrefix is set, only respect labels that start with $selectionPrefix
2259 if ($selectionPrefix === '' || strpos($label, $selectionPrefix) === 0) {
2260 // Remove substring $stripFromSelectionName from label
2261 $label = str_replace($stripFromSelectionName, '', $label);
2262 $labelsFromFile[$label] = $value;
2263 }
2264 }
2265 $this->inlineLanguageLabels = array_merge($this->inlineLanguageLabels, $labelsFromFile);
2266 }
2267 }
2268
2269 /**
2270 * Reads a locallang file.
2271 *
2272 * @param string $fileRef Reference to a relative filename to include.
2273 * @return array Returns the $LOCAL_LANG array found in the file. If no array found, returns empty array.
2274 */
2275 protected function readLLfile($fileRef)
2276 {
2277 /** @var LocalizationFactory $languageFactory */
2278 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
2279
2280 if ($this->lang !== 'default') {
2281 $languages = array_reverse($this->languageDependencies);
2282 // At least we need to have English
2283 if (empty($languages)) {
2284 $languages[] = 'default';
2285 }
2286 } else {
2287 $languages = ['default'];
2288 }
2289
2290 $localLanguage = [];
2291 foreach ($languages as $language) {
2292 $tempLL = $languageFactory->getParsedData($fileRef, $language);
2293
2294 $localLanguage['default'] = $tempLL['default'];
2295 if (!isset($localLanguage[$this->lang])) {
2296 $localLanguage[$this->lang] = $localLanguage['default'];
2297 }
2298 if ($this->lang !== 'default' && isset($tempLL[$language])) {
2299 // Merge current language labels onto labels from previous language
2300 // This way we have a labels with fall back applied
2301 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($localLanguage[$this->lang], $tempLL[$language], true, false);
2302 }
2303 }
2304
2305 return $localLanguage;
2306 }
2307
2308 /*****************************************************/
2309 /* */
2310 /* Tools */
2311 /* */
2312 /*****************************************************/
2313 /**
2314 * Concatenate files into one file
2315 * registered handler
2316 */
2317 protected function doConcatenate()
2318 {
2319 $this->doConcatenateCss();
2320 $this->doConcatenateJavaScript();
2321 }
2322
2323 /**
2324 * Concatenate JavaScript files according to the configuration.
2325 */
2326 protected function doConcatenateJavaScript()
2327 {
2328 if ($this->concatenateJavascript) {
2329 if (!empty($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsConcatenateHandler'])) {
2330 // use external concatenation routine
2331 $params = [
2332 'jsLibs' => &$this->jsLibs,
2333 'jsFiles' => &$this->jsFiles,
2334 'jsFooterFiles' => &$this->jsFooterFiles,
2335 'headerData' => &$this->headerData,
2336 'footerData' => &$this->footerData
2337 ];
2338 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsConcatenateHandler'], $params, $this);
2339 } else {
2340 $this->jsLibs = $this->getCompressor()->concatenateJsFiles($this->jsLibs);
2341 $this->jsFiles = $this->getCompressor()->concatenateJsFiles($this->jsFiles);
2342 $this->jsFooterFiles = $this->getCompressor()->concatenateJsFiles($this->jsFooterFiles);
2343 }
2344 }
2345 }
2346
2347 /**
2348 * Concatenate CSS files according to configuration.
2349 */
2350 protected function doConcatenateCss()
2351 {
2352 if ($this->concatenateCss) {
2353 if (!empty($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssConcatenateHandler'])) {
2354 // use external concatenation routine
2355 $params = [
2356 'cssFiles' => &$this->cssFiles,
2357 'cssLibs' => &$this->cssLibs,
2358 'headerData' => &$this->headerData,
2359 'footerData' => &$this->footerData
2360 ];
2361 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssConcatenateHandler'], $params, $this);
2362 } else {
2363 $this->cssLibs = $this->getCompressor()->concatenateCssFiles($this->cssLibs);
2364 $this->cssFiles = $this->getCompressor()->concatenateCssFiles($this->cssFiles);
2365 }
2366 }
2367 }
2368
2369 /**
2370 * Compresses inline code
2371 */
2372 protected function doCompress()
2373 {
2374 $this->doCompressJavaScript();
2375 $this->doCompressCss();
2376 }
2377
2378 /**
2379 * Compresses CSS according to configuration.
2380 */
2381 protected function doCompressCss()
2382 {
2383 if ($this->compressCss) {
2384 if (!empty($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssCompressHandler'])) {
2385 // Use external compression routine
2386 $params = [
2387 'cssInline' => &$this->cssInline,
2388 'cssFiles' => &$this->cssFiles,
2389 'cssLibs' => &$this->cssLibs,
2390 'headerData' => &$this->headerData,
2391 'footerData' => &$this->footerData
2392 ];
2393 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['cssCompressHandler'], $params, $this);
2394 } else {
2395 $this->cssLibs = $this->getCompressor()->compressCssFiles($this->cssLibs);
2396 $this->cssFiles = $this->getCompressor()->compressCssFiles($this->cssFiles);
2397 }
2398 }
2399 }
2400
2401 /**
2402 * Compresses JavaScript according to configuration.
2403 */
2404 protected function doCompressJavaScript()
2405 {
2406 if ($this->compressJavascript) {
2407 if (!empty($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsCompressHandler'])) {
2408 // Use external compression routine
2409 $params = [
2410 'jsInline' => &$this->jsInline,
2411 'jsFooterInline' => &$this->jsFooterInline,
2412 'jsLibs' => &$this->jsLibs,
2413 'jsFiles' => &$this->jsFiles,
2414 'jsFooterFiles' => &$this->jsFooterFiles,
2415 'headerData' => &$this->headerData,
2416 'footerData' => &$this->footerData
2417 ];
2418 GeneralUtility::callUserFunction($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['jsCompressHandler'], $params, $this);
2419 } else {
2420 // Traverse the arrays, compress files
2421 if (!empty($this->jsInline)) {
2422 foreach ($this->jsInline as $name => $properties) {
2423 if ($properties['compress']) {
2424 $error = '';
2425 $this->jsInline[$name]['code'] = GeneralUtility::minifyJavaScript($properties['code'], $error);
2426 if ($error) {
2427 $this->compressError .= 'Error with minify JS Inline Block "' . $name . '": ' . $error . LF;
2428 }
2429 }
2430 }
2431 }
2432 $this->jsLibs = $this->getCompressor()->compressJsFiles($this->jsLibs);
2433 $this->jsFiles = $this->getCompressor()->compressJsFiles($this->jsFiles);
2434 $this->jsFooterFiles = $this->getCompressor()->compressJsFiles($this->jsFooterFiles);
2435 }
2436 }
2437 }
2438
2439 /**
2440 * Returns instance of \TYPO3\CMS\Core\Resource\ResourceCompressor
2441 *
2442 * @return ResourceCompressor
2443 */
2444 protected function getCompressor()
2445 {
2446 if ($this->compressor === null) {
2447 $this->compressor = GeneralUtility::makeInstance(ResourceCompressor::class);
2448 }
2449 return $this->compressor;
2450 }
2451
2452 /**
2453 * Processes a Javascript file dependent on the current context
2454 *
2455 * Adds the version number for Frontend, compresses the file for Backend
2456 *
2457 * @param string $filename Filename
2458 * @return string New filename
2459 */
2460 protected function processJsFile($filename)
2461 {
2462 $filename = $this->getStreamlinedFileName($filename, false);
2463 if ($this->compressJavascript) {
2464 $filename = $this->getCompressor()->compressJsFile($filename);
2465 } elseif (TYPO3_MODE === 'FE') {
2466 $filename = GeneralUtility::createVersionNumberedFilename($filename);
2467 }
2468 return $this->getAbsoluteWebPath($filename);
2469 }
2470
2471 /**
2472 * This function acts as a wrapper to allow relative and paths starting with EXT: to be dealt with
2473 * in this very case to always return the absolute web path to be included directly before output.
2474 *
2475 * This is mainly added so the EXT: syntax can be resolved for PageRenderer in one central place,
2476 * and hopefully removed in the future by one standard API call.
2477 *
2478 * @param string $file the filename to process
2479 * @param bool $prepareForOutput whether the file should be prepared as version numbered file and prefixed as absolute webpath
2480 * @return string
2481 * @internal
2482 */
2483 protected function getStreamlinedFileName($file, $prepareForOutput = true)
2484 {
2485 if (strpos($file, 'EXT:') === 0) {
2486 $file = GeneralUtility::getFileAbsFileName($file);
2487 // as the path is now absolute, make it "relative" to the current script to stay compatible
2488 $file = PathUtility::getRelativePathTo($file);
2489 $file = rtrim($file, '/');
2490 } else {
2491 $file = GeneralUtility::resolveBackPath($file);
2492 }
2493 if ($prepareForOutput) {
2494 $file = GeneralUtility::createVersionNumberedFilename($file);
2495 $file = $this->getAbsoluteWebPath($file);
2496 }
2497 return $file;
2498 }
2499
2500 /**
2501 * Gets absolute web path of filename for backend disposal.
2502 * Resolving the absolute path in the frontend with conflict with
2503 * applying config.absRefPrefix in frontend rendering process.
2504 *
2505 * @param string $file
2506 * @return string
2507 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::setAbsRefPrefix()
2508 */
2509 protected function getAbsoluteWebPath(string $file): string
2510 {
2511 if (TYPO3_MODE === 'FE') {
2512 return $file;
2513 }
2514 return PathUtility::getAbsoluteWebPath($file);
2515 }
2516
2517 /*****************************************************/
2518 /* */
2519 /* Hooks */
2520 /* */
2521 /*****************************************************/
2522 /**
2523 * Execute PreRenderHook for possible manipulation
2524 */
2525 protected function executePreRenderHook()
2526 {
2527 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-preProcess'] ?? false;
2528 if (!$hooks) {
2529 return;
2530 }
2531 $params = [
2532 'jsLibs' => &$this->jsLibs,
2533 'jsFooterLibs' => &$this->jsFooterLibs,
2534 'jsFiles' => &$this->jsFiles,
2535 'jsFooterFiles' => &$this->jsFooterFiles,
2536 'cssLibs' => &$this->cssLibs,
2537 'cssFiles' => &$this->cssFiles,
2538 'headerData' => &$this->headerData,
2539 'footerData' => &$this->footerData,
2540 'jsInline' => &$this->jsInline,
2541 'jsFooterInline' => &$this->jsFooterInline,
2542 'cssInline' => &$this->cssInline
2543 ];
2544 foreach ($hooks as $hook) {
2545 GeneralUtility::callUserFunction($hook, $params, $this);
2546 }
2547 }
2548
2549 /**
2550 * PostTransform for possible manipulation of concatenated and compressed files
2551 */
2552 protected function executeRenderPostTransformHook()
2553 {
2554 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postTransform'] ?? false;
2555 if (!$hooks) {
2556 return;
2557 }
2558 $params = [
2559 'jsLibs' => &$this->jsLibs,
2560 'jsFooterLibs' => &$this->jsFooterLibs,
2561 'jsFiles' => &$this->jsFiles,
2562 'jsFooterFiles' => &$this->jsFooterFiles,
2563 'cssLibs' => &$this->cssLibs,
2564 'cssFiles' => &$this->cssFiles,
2565 'headerData' => &$this->headerData,
2566 'footerData' => &$this->footerData,
2567 'jsInline' => &$this->jsInline,
2568 'jsFooterInline' => &$this->jsFooterInline,
2569 'cssInline' => &$this->cssInline
2570 ];
2571 foreach ($hooks as $hook) {
2572 GeneralUtility::callUserFunction($hook, $params, $this);
2573 }
2574 }
2575
2576 /**
2577 * Execute postRenderHook for possible manipulation
2578 *
2579 * @param string $jsLibs
2580 * @param string $jsFiles
2581 * @param string $jsFooterFiles
2582 * @param string $cssLibs
2583 * @param string $cssFiles
2584 * @param string $jsInline
2585 * @param string $cssInline
2586 * @param string $jsFooterInline
2587 * @param string $jsFooterLibs
2588 */
2589 protected function executePostRenderHook(&$jsLibs, &$jsFiles, &$jsFooterFiles, &$cssLibs, &$cssFiles, &$jsInline, &$cssInline, &$jsFooterInline, &$jsFooterLibs)
2590 {
2591 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_pagerenderer.php']['render-postProcess'] ?? false;
2592 if (!$hooks) {
2593 return;
2594 }
2595 $params = [
2596 'jsLibs' => &$jsLibs,
2597 'jsFiles' => &$jsFiles,
2598 'jsFooterFiles' => &$jsFooterFiles,
2599 'cssLibs' => &$cssLibs,
2600 'cssFiles' => &$cssFiles,
2601 'headerData' => &$this->headerData,
2602 'footerData' => &$this->footerData,
2603 'jsInline' => &$jsInline,
2604 'cssInline' => &$cssInline,
2605 'xmlPrologAndDocType' => &$this->xmlPrologAndDocType,
2606 'htmlTag' => &$this->htmlTag,
2607 'headTag' => &$this->headTag,
2608 'charSet' => &$this->charSet,
2609 'metaCharsetTag' => &$this->metaCharsetTag,
2610 'shortcutTag' => &$this->shortcutTag,
2611 'inlineComments' => &$this->inlineComments,
2612 'baseUrl' => &$this->baseUrl,
2613 'baseUrlTag' => &$this->baseUrlTag,
2614 'favIcon' => &$this->favIcon,
2615 'iconMimeType' => &$this->iconMimeType,
2616 'titleTag' => &$this->titleTag,
2617 'title' => &$this->title,
2618 'metaTags' => &$this->metaTags,
2619 'jsFooterInline' => &$jsFooterInline,
2620 'jsFooterLibs' => &$jsFooterLibs,
2621 'bodyContent' => &$this->bodyContent
2622 ];
2623 foreach ($hooks as $hook) {
2624 GeneralUtility::callUserFunction($hook, $params, $this);
2625 }
2626 }
2627
2628 /**
2629 * Creates a CSS inline tag
2630 *
2631 * @param string $file the filename to process
2632 * @param array $properties
2633 * @return string
2634 */
2635 protected function createInlineCssTagFromFile(string $file, array $properties): string
2636 {
2637 $cssInline = file_get_contents($file);
2638
2639 return '<style type="text/css"'
2640 . ' media="' . htmlspecialchars($properties['media']) . '"'
2641 . ($properties['title'] ? ' title="' . htmlspecialchars($properties['title']) . '"' : '')
2642 . '>' . LF
2643 . '/*<![CDATA[*/' . LF . '<!-- ' . LF
2644 . $cssInline
2645 . '-->' . LF . '/*]]>*/' . LF . '</style>' . LF;
2646 }
2647 }