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