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