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