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