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