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