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