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