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