0d0c1cc297811c2deef02dcacf419408b6d6b8ab
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Template / DocumentTemplate.php
1 <?php
2 namespace TYPO3\CMS\Backend\Template;
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 Psr\Log\LoggerAwareInterface;
18 use Psr\Log\LoggerAwareTrait;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
21 use TYPO3\CMS\Core\Imaging\Icon;
22 use TYPO3\CMS\Core\Imaging\IconFactory;
23 use TYPO3\CMS\Core\Page\PageRenderer;
24 use TYPO3\CMS\Core\Service\MarkerBasedTemplateService;
25 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27 use TYPO3\CMS\Core\Utility\PathUtility;
28
29 /**
30 * TYPO3 Backend Template Class
31 *
32 * This class contains functions for starting and ending the HTML of backend modules
33 * It also contains methods for outputting sections of content.
34 * Further there are functions for making icons, links, setting form-field widths etc.
35 * Color scheme and stylesheet definitions are also available here.
36 * Finally this file includes the language class for TYPO3's backend.
37 *
38 * After this file $LANG and $TBE_TEMPLATE are global variables / instances of their respective classes.
39 *
40 * Please refer to Inside TYPO3 for a discussion of how to use this API.
41 */
42 class DocumentTemplate implements LoggerAwareInterface
43 {
44 use LoggerAwareTrait;
45
46 // Vars you typically might want to/should set from outside after making instance of this class:
47 /**
48 * This can be set to the HTML-code for a formtag.
49 * Useful when you need a form to span the whole page; Inserted exactly after the body-tag.
50 *
51 * @var string
52 */
53 public $form = '';
54
55 /**
56 * Additional header code (eg. a JavaScript section) could be accommulated in this var. It will be directly outputted in the header.
57 *
58 * @var string
59 */
60 public $JScode = '';
61
62 /**
63 * Similar to $JScode but for use as array with associative keys to prevent double inclusion of JS code. a <script> tag is automatically wrapped around.
64 *
65 * @var array
66 */
67 public $JScodeArray = ['jumpToUrl' => '
68 function jumpToUrl(URL) {
69 window.location.href = URL;
70 return false;
71 }
72 '];
73
74 /**
75 * Additional 'page-end' code could be accumulated in this var. It will be outputted at the end of page before </body> and some other internal page-end code.
76 *
77 * @var string
78 */
79 public $postCode = '';
80
81 /**
82 * HTML template with markers for module
83 *
84 * @var string
85 */
86 public $moduleTemplate = '';
87
88 /**
89 * The base file (not overlaid by TBE_STYLES) for the current module, useful for hooks when finding out which modules is rendered currently
90 *
91 * @var string
92 */
93 protected $moduleTemplateFilename = '';
94
95 /**
96 * Script ID
97 *
98 * @var string
99 */
100 public $scriptID = '';
101
102 /**
103 * Id which can be set for the body tag. Default value is based on script ID
104 *
105 * @var string
106 */
107 public $bodyTagId = '';
108
109 /**
110 * You can add additional attributes to the body-tag through this variable.
111 *
112 * @var string
113 */
114 public $bodyTagAdditions = '';
115
116 /**
117 * Additional CSS styles which will be added to the <style> section in the header
118 * used as array with associative keys to prevent double inclusion of CSS code
119 *
120 * @var array
121 */
122 public $inDocStylesArray = [];
123
124 /**
125 * Filename of stylesheet
126 *
127 * @var string
128 */
129 public $styleSheetFile = '';
130
131 /**
132 * Filename of stylesheet #2 - linked to right after the $this->styleSheetFile script
133 *
134 * @var string
135 */
136 public $styleSheetFile2 = '';
137
138 /**
139 * Filename of a post-stylesheet - included right after all inline styles.
140 *
141 * @var string
142 */
143 public $styleSheetFile_post = '';
144
145 /**
146 * Whether to use the X-UA-Compatible meta tag
147 *
148 * @var bool
149 */
150 protected $useCompatibilityTag = true;
151
152 /**
153 * X-Ua-Compatible version output in meta tag
154 *
155 * @var string
156 */
157 protected $xUaCompatibilityVersion = 'IE=edge';
158
159 // Skinning
160 /**
161 * Include these CSS directories from skins by default
162 *
163 * @var array
164 */
165 protected $stylesheetsSkins = [
166 'structure' => 'Resources/Public/Css/structure/',
167 'visual' => 'Resources/Public/Css/visual/'
168 ];
169
170 /**
171 * JavaScript files loaded for every page in the Backend
172 *
173 * @var array
174 */
175 protected $jsFiles = [];
176
177 /**
178 * JavaScript files loaded for every page in the Backend, but explicitly excluded from concatenation (useful for libraries etc.)
179 *
180 * @var array
181 */
182 protected $jsFilesNoConcatenation = [];
183
184 /**
185 * Indicates if a <div>-output section is open
186 *
187 * @var int
188 * @internal will be removed in TYPO3 v9
189 */
190 public $sectionFlag = 0;
191
192 /**
193 * (Default) Class for wrapping <DIV>-tag of page. Is set in class extensions.
194 *
195 * @var string
196 */
197 public $divClass = '';
198
199 /**
200 * @var string
201 */
202 public $pageHeaderBlock = '';
203
204 /**
205 * @var string
206 */
207 public $endOfPageJsBlock = '';
208
209 /**
210 * @var bool
211 */
212 public $hasDocheader = true;
213
214 /**
215 * @var PageRenderer
216 */
217 protected $pageRenderer;
218
219 /**
220 * Alternative template file
221 *
222 * @var string
223 */
224 protected $pageHeaderFooterTemplateFile = '';
225
226 /**
227 * Whether flashmessages should be rendered or not
228 *
229 * @var bool $showFlashMessages
230 */
231 public $showFlashMessages = true;
232
233 /**
234 * @var IconFactory
235 */
236 protected $iconFactory;
237
238 /**
239 * @var MarkerBasedTemplateService
240 */
241 protected $templateService;
242
243 /**
244 * Constructor
245 */
246 public function __construct()
247 {
248 // Initializes the page rendering object:
249 $this->initPageRenderer();
250
251 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
252
253 // initialize Marker Support
254 $this->templateService = GeneralUtility::makeInstance(MarkerBasedTemplateService::class);
255
256 // Setting default scriptID, trim forward slash from route
257 $this->scriptID = ltrim(GeneralUtility::_GET('route'), '/');
258 $this->bodyTagId = preg_replace('/[^A-Za-z0-9-]/', '-', $this->scriptID);
259 // Individual configuration per script? If so, make a recursive merge of the arrays:
260 if (is_array($GLOBALS['TBE_STYLES']['scriptIDindex'][$this->scriptID] ?? false)) {
261 // Make copy
262 $ovr = $GLOBALS['TBE_STYLES']['scriptIDindex'][$this->scriptID];
263 // merge styles.
264 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($GLOBALS['TBE_STYLES'], $ovr);
265 // Have to unset - otherwise the second instantiation will do it again!
266 unset($GLOBALS['TBE_STYLES']['scriptIDindex'][$this->scriptID]);
267 }
268 // Main Stylesheets:
269 if (!empty($GLOBALS['TBE_STYLES']['stylesheet'])) {
270 $this->styleSheetFile = $GLOBALS['TBE_STYLES']['stylesheet'];
271 }
272 if (!empty($GLOBALS['TBE_STYLES']['stylesheet2'])) {
273 $this->styleSheetFile2 = $GLOBALS['TBE_STYLES']['stylesheet2'];
274 }
275 if (!empty($GLOBALS['TBE_STYLES']['styleSheetFile_post'])) {
276 $this->styleSheetFile_post = $GLOBALS['TBE_STYLES']['styleSheetFile_post'];
277 }
278 if (!empty($GLOBALS['TBE_STYLES']['inDocStyles_TBEstyle'])) {
279 $this->inDocStylesArray['TBEstyle'] = $GLOBALS['TBE_STYLES']['inDocStyles_TBEstyle'];
280 }
281 // include all stylesheets
282 foreach ($this->getSkinStylesheetDirectories() as $stylesheetDirectory) {
283 $this->addStyleSheetDirectory($stylesheetDirectory);
284 }
285 }
286
287 /**
288 * Initializes the page renderer object
289 */
290 protected function initPageRenderer()
291 {
292 if ($this->pageRenderer !== null) {
293 return;
294 }
295 $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
296 $this->pageRenderer->setLanguage($GLOBALS['LANG']->lang);
297 $this->pageRenderer->enableConcatenateFiles();
298 $this->pageRenderer->enableCompressCss();
299 $this->pageRenderer->enableCompressJavascript();
300 // Add all JavaScript files defined in $this->jsFiles to the PageRenderer
301 foreach ($this->jsFilesNoConcatenation as $file) {
302 $this->pageRenderer->addJsFile(
303 $file,
304 'text/javascript',
305 true,
306 false,
307 '',
308 true
309 );
310 }
311 // Add all JavaScript files defined in $this->jsFiles to the PageRenderer
312 foreach ($this->jsFiles as $file) {
313 $this->pageRenderer->addJsFile($file);
314 }
315 if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] === 1) {
316 $this->pageRenderer->enableDebugMode();
317 }
318 }
319
320 /*****************************************
321 *
322 * EVALUATION FUNCTIONS
323 * Various centralized processing
324 *
325 *****************************************/
326
327 /**
328 * Returns a linked shortcut-icon which will call the shortcut frame and set a shortcut there back to the calling page/module
329 *
330 * @param string $gvList Is the list of GET variables to store (if any)
331 * @param string $setList Is the list of SET[] variables to store (if any) - SET[] variables a stored in $GLOBALS["SOBE"]->MOD_SETTINGS for backend modules
332 * @param string $modName Module name string
333 * @param string|int $motherModName Is used to enter the "parent module name" if the module is a submodule under eg. Web>* or File>*. You can also set this value to 1 in which case the currentLoadedModule is sent to the shortcut script (so - not a fixed value!) - that is used in file_edit and wizard_rte modules where those are really running as a part of another module.
334 * @param string $classes
335 * @return string HTML content
336 */
337 public function makeShortcutIcon($gvList, $setList, $modName, $motherModName = '', $classes = '')
338 {
339 $gvList = 'route,' . $gvList;
340 $storeUrl = $this->makeShortcutUrl($gvList, $setList);
341 $pathInfo = parse_url(GeneralUtility::getIndpEnv('REQUEST_URI'));
342 // Fallback for alt_mod. We still pass in the old xMOD... stuff, but TBE_MODULES only knows about "record_edit".
343 // We still need to pass the xMOD name to createShortcut below, since this is used for icons.
344 $moduleName = $modName === 'xMOD_alt_doc.php' ? 'record_edit' : $modName;
345 // Add the module identifier automatically if typo3/index.php is used:
346 if (GeneralUtility::_GET('M') !== null) {
347 $storeUrl = '&M=' . $moduleName . $storeUrl;
348 }
349 if ((int)$motherModName === 1) {
350 $motherModule = 'top.currentModuleLoaded';
351 } elseif ($motherModName) {
352 $motherModule = GeneralUtility::quoteJSvalue($motherModName);
353 } else {
354 $motherModule = '\'\'';
355 }
356 $confirmationText = GeneralUtility::quoteJSvalue($GLOBALS['LANG']->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark'));
357
358 $shortcutUrl = $pathInfo['path'] . '?' . $storeUrl;
359 $shortcutExist = BackendUtility::shortcutExists($shortcutUrl);
360
361 if ($shortcutExist) {
362 return '<a class="active ' . htmlspecialchars($classes) . '" title="">' .
363 $this->iconFactory->getIcon('actions-system-shortcut-active', Icon::SIZE_SMALL)->render() . '</a>';
364 }
365 $url = GeneralUtility::quoteJSvalue(rawurlencode($shortcutUrl));
366 $onClick = 'top.TYPO3.ShortcutMenu.createShortcut(' . GeneralUtility::quoteJSvalue(rawurlencode($modName)) .
367 ', ' . $url . ', ' . $confirmationText . ', ' . $motherModule . ', this);return false;';
368
369 return '<a href="#" class="' . htmlspecialchars($classes) . '" onclick="' . htmlspecialchars($onClick) . '" title="' .
370 htmlspecialchars($GLOBALS['LANG']->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.makeBookmark')) . '">' .
371 $this->iconFactory->getIcon('actions-system-shortcut-new', Icon::SIZE_SMALL)->render() . '</a>';
372 }
373
374 /**
375 * MAKE url for storing
376 * Internal func
377 *
378 * @param string $gvList Is the list of GET variables to store (if any)
379 * @param string $setList Is the list of SET[] variables to store (if any) - SET[] variables a stored in $GLOBALS["SOBE"]->MOD_SETTINGS for backend modules
380 * @return string
381 * @access private
382 * @see makeShortcutIcon()
383 */
384 public function makeShortcutUrl($gvList, $setList)
385 {
386 $GET = GeneralUtility::_GET();
387 $storeArray = array_merge(GeneralUtility::compileSelectedGetVarsFromArray($gvList, $GET), ['SET' => GeneralUtility::compileSelectedGetVarsFromArray($setList, (array)$GLOBALS['SOBE']->MOD_SETTINGS)]);
388 $storeUrl = GeneralUtility::implodeArrayForUrl('', $storeArray);
389 return $storeUrl;
390 }
391
392 /**
393 * Returns <input> attributes to set the width of an text-type input field.
394 * For client browsers with no CSS support the cols/size attribute is returned.
395 * For CSS compliant browsers (recommended) a ' style="width: ...px;"' is returned.
396 *
397 * @param int $size A relative number which multiplied with approx. 10 will lead to the width in pixels
398 * @param bool $textarea A flag you can set for textareas - DEPRECATED as there is no difference any more between the two
399 * @param string $styleOverride A string which will be returned as attribute-value for style="" instead of the calculated width (if CSS is enabled)
400 * @return string Tag attributes for an <input> tag (regarding width)
401 * @deprecated
402 */
403 public function formWidth($size = 48, $textarea = false, $styleOverride = '')
404 {
405 trigger_error('This method will be removed in TYPO3 10 - use responsive code or direct inline styles to format your input fields instead.', E_USER_DEPRECATED);
406 return ' style="' . ($styleOverride ?: 'width:' . ceil($size * 9.58) . 'px;') . '"';
407 }
408
409 /**
410 * Returns JavaScript variables setting the returnUrl and thisScript location for use by JavaScript on the page.
411 * Used in fx. db_list.php (Web>List)
412 *
413 * @param string $thisLocation URL to "this location" / current script
414 * @return string Urls are returned as JavaScript variables T3_RETURN_URL and T3_THIS_LOCATION
415 * @see typo3/db_list.php
416 */
417 public function redirectUrls($thisLocation = '')
418 {
419 $thisLocation = $thisLocation ? $thisLocation : GeneralUtility::linkThisScript([
420 'CB' => '',
421 'SET' => '',
422 'cmd' => '',
423 'popViewId' => ''
424 ]);
425 $out = '
426 var T3_RETURN_URL = ' . GeneralUtility::quoteJSvalue(str_replace('%20', '', rawurlencode(GeneralUtility::sanitizeLocalUrl(GeneralUtility::_GP('returnUrl'))))) . ';
427 var T3_THIS_LOCATION = ' . GeneralUtility::quoteJSvalue(str_replace('%20', '', rawurlencode($thisLocation))) . '
428 ';
429 return $out;
430 }
431
432 /**
433 * Defines whether to use the X-UA-Compatible meta tag.
434 *
435 * @param bool $useCompatibilityTag Whether to use the tag
436 */
437 public function useCompatibilityTag($useCompatibilityTag = true)
438 {
439 $this->useCompatibilityTag = (bool)$useCompatibilityTag;
440 }
441
442 /*****************************************
443 *
444 * PAGE BUILDING FUNCTIONS.
445 * Use this to build the HTML of your backend modules
446 *
447 *****************************************/
448 /**
449 * Returns page start
450 * This includes the proper header with charset, title, meta tag and beginning body-tag.
451 *
452 * @param string $title HTML Page title for the header
453 * @return string Returns the whole header section of a HTML-document based on settings in internal variables (like styles, javascript code, charset, generator and docType)
454 * @see endPage()
455 */
456 public function startPage($title)
457 {
458 // hook pre start page
459 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/template.php']['preStartPageHook'] ?? [] as $hookFunction) {
460 $hookParameters = [
461 'title' => &$title
462 ];
463 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
464 }
465 // alternative template for Header and Footer
466 if ($this->pageHeaderFooterTemplateFile) {
467 $file = GeneralUtility::getFileAbsFileName($this->pageHeaderFooterTemplateFile);
468 if ($file) {
469 $this->pageRenderer->setTemplateFile($file);
470 }
471 }
472
473 // Disable rendering of XHTML tags
474 $this->pageRenderer->setRenderXhtml(false);
475
476 $languageCode = $this->pageRenderer->getLanguage() === 'default' ? 'en' : $this->pageRenderer->getLanguage();
477 $this->pageRenderer->setHtmlTag('<html lang="' . $languageCode . '">');
478
479 $headerStart = '<!DOCTYPE html>';
480 $this->pageRenderer->setXmlPrologAndDocType($headerStart);
481 $this->pageRenderer->setHeadTag('<head>' . LF . '<!-- TYPO3 Script ID: ' . htmlspecialchars($this->scriptID) . ' -->');
482 header('Content-Type:text/html;charset=utf-8');
483 $this->pageRenderer->setCharSet('utf-8');
484 $this->pageRenderer->setMetaTag('name', 'generator', $this->generator());
485 $this->pageRenderer->setMetaTag('name', 'robots', 'noindex,follow');
486 $this->pageRenderer->setMetaTag('name', 'viewport', 'width=device-width, initial-scale=1');
487 $this->pageRenderer->setFavIcon($this->getBackendFavicon());
488 if ($this->useCompatibilityTag) {
489 $this->pageRenderer->setMetaTag('http-equiv', 'X-UA-Compatible', $this->xUaCompatibilityVersion);
490 }
491 $this->pageRenderer->setTitle($title);
492 // add docstyles
493 $this->docStyle();
494 $this->pageRenderer->addHeaderData($this->JScode);
495 foreach ($this->JScodeArray as $name => $code) {
496 $this->pageRenderer->addJsInlineCode($name, $code, false);
497 }
498
499 // Load jquery and twbs JS libraries on every backend request
500 $this->pageRenderer->loadJquery();
501 // Note: please do not reference "bootstrap" outside of the TYPO3 Core (not in your own extensions)
502 // as this is preliminary as long as Twitter bootstrap does not support AMD modules
503 // this logic will be changed once Twitter bootstrap 4 is included
504 $this->pageRenderer->addJsFile('EXT:core/Resources/Public/JavaScript/Contrib/bootstrap/bootstrap.js');
505
506 // hook for additional headerData
507 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/template.php']['preHeaderRenderHook'] ?? [] as $hookFunction) {
508 $hookParameters = [
509 'pageRenderer' => &$this->pageRenderer
510 ];
511 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
512 }
513 // Construct page header.
514 $str = $this->pageRenderer->render(PageRenderer::PART_HEADER);
515 $this->JScode = '';
516 $this->JScodeArray = [];
517 $this->endOfPageJsBlock = $this->pageRenderer->render(PageRenderer::PART_FOOTER);
518 $str .= $this->docBodyTagBegin() . ($this->divClass ? '
519
520 <!-- Wrapping DIV-section for whole page BEGIN -->
521 <div class="' . $this->divClass . '">
522 ' : '') . trim($this->form);
523 return $str;
524 }
525
526 /**
527 * Returns page end; This includes finishing form, div, body and html tags.
528 *
529 * @return string The HTML end of a page
530 * @see startPage()
531 */
532 public function endPage()
533 {
534 $str = $this->postCode . GeneralUtility::wrapJS(BackendUtility::getUpdateSignalCode()) . ($this->form ? '
535 </form>' : '');
536 // If something is in buffer like debug, put it to end of page
537 if (ob_get_contents()) {
538 $str .= ob_get_clean();
539 if (!headers_sent()) {
540 header('Content-Encoding: None');
541 }
542 }
543 $str .= ($this->divClass ? '
544
545 <!-- Wrapping DIV-section for whole page END -->
546 </div>' : '') . $this->endOfPageJsBlock;
547
548 // Logging: Can't find better place to put it:
549 $this->logger->debug('END of BACKEND session', ['_FLUSH' => true]);
550 return $str;
551 }
552
553 /**
554 * Shortcut for render the complete page of a module
555 *
556 * @param string $title page title
557 * @param string $content page content
558 * @return string complete page
559 */
560 public function render($title, $content)
561 {
562 $pageContent = $this->startPage($title);
563 $pageContent .= $content;
564 $pageContent .= $this->endPage();
565 return $this->insertStylesAndJS($pageContent);
566 }
567
568 /**
569 * Creates the bodyTag.
570 * You can add to the bodyTag by $this->bodyTagAdditions
571 *
572 * @return string HTML body tag
573 */
574 public function docBodyTagBegin()
575 {
576 return '<body ' . trim($this->bodyTagAdditions . ($this->bodyTagId ? ' id="' . $this->bodyTagId . '"' : '')) . '>';
577 }
578
579 /**
580 * Outputting document style
581 *
582 * @return string HTML style section/link tags
583 */
584 public function docStyle()
585 {
586 // Implode it all:
587 $inDocStyles = implode(LF, $this->inDocStylesArray);
588
589 // Reset styles so they won't be added again in insertStylesAndJS()
590 $this->inDocStylesArray = [];
591
592 if ($this->styleSheetFile) {
593 $this->pageRenderer->addCssFile($this->styleSheetFile);
594 }
595 if ($this->styleSheetFile2) {
596 $this->pageRenderer->addCssFile($this->styleSheetFile2);
597 }
598
599 if ($inDocStyles !== '') {
600 $this->pageRenderer->addCssInlineBlock('inDocStyles', $inDocStyles . LF . '/*###POSTCSSMARKER###*/');
601 }
602
603 if ($this->styleSheetFile_post) {
604 $this->pageRenderer->addCssFile($this->styleSheetFile_post);
605 }
606 }
607
608 /**
609 * Insert additional style sheet link
610 *
611 * @param string $key some key identifying the style sheet
612 * @param string $href uri to the style sheet file
613 * @param string $title value for the title attribute of the link element
614 * @param string $relation value for the rel attribute of the link element
615 */
616 public function addStyleSheet($key, $href, $title = '', $relation = 'stylesheet')
617 {
618 $this->pageRenderer->addCssFile($href, $relation, 'screen', $title);
619 }
620
621 /**
622 * Add all *.css files of the directory $path to the stylesheets
623 *
624 * @param string $path directory to add
625 */
626 public function addStyleSheetDirectory($path)
627 {
628 $path = GeneralUtility::getFileAbsFileName($path);
629 // Read all files in directory and sort them alphabetically
630 $cssFiles = GeneralUtility::getFilesInDir($path, 'css');
631 foreach ($cssFiles as $cssFile) {
632 $this->pageRenderer->addCssFile(PathUtility::getRelativePathTo($path) . $cssFile);
633 }
634 }
635
636 /**
637 * Insert post rendering document style into already rendered content
638 * This is needed for extobjbase
639 *
640 * @param string $content style-content to insert.
641 * @return string content with inserted styles
642 */
643 public function insertStylesAndJS($content)
644 {
645 $styles = LF . implode(LF, $this->inDocStylesArray);
646 $content = str_replace('/*###POSTCSSMARKER###*/', $styles, $content);
647
648 // Insert accumulated JS
649 $jscode = $this->JScode . LF . GeneralUtility::wrapJS(implode(LF, $this->JScodeArray));
650 $content = str_replace('<!--###POSTJSMARKER###-->', $jscode, $content);
651 return $content;
652 }
653
654 /**
655 * Returns an array of all stylesheet directories belonging to core and skins
656 *
657 * @return array Stylesheet directories
658 */
659 public function getSkinStylesheetDirectories()
660 {
661 $stylesheetDirectories = [];
662 // Stylesheets from skins
663 // merge default css directories ($this->stylesheetsSkin) with additional ones and include them
664 if (is_array($GLOBALS['TBE_STYLES']['skins'])) {
665 // loop over all registered skins
666 foreach ($GLOBALS['TBE_STYLES']['skins'] as $skinExtKey => $skin) {
667 $skinStylesheetDirs = $this->stylesheetsSkins;
668 // Skins can add custom stylesheetDirectories using
669 // $GLOBALS['TBE_STYLES']['skins'][$_EXTKEY]['stylesheetDirectories']
670 if (is_array($skin['stylesheetDirectories'])) {
671 $skinStylesheetDirs = array_merge($skinStylesheetDirs, $skin['stylesheetDirectories']);
672 }
673 // Add all registered directories
674 foreach ($skinStylesheetDirs as $stylesheetDir) {
675 // for EXT:myskin/stylesheets/ syntax
676 if (strpos($stylesheetDir, 'EXT:') === 0) {
677 list($extKey, $path) = explode('/', substr($stylesheetDir, 4), 2);
678 if (!empty($extKey) && ExtensionManagementUtility::isLoaded($extKey) && !empty($path)) {
679 $stylesheetDirectories[] = ExtensionManagementUtility::extPath($extKey) . $path;
680 }
681 } else {
682 // For relative paths
683 $stylesheetDirectories[] = ExtensionManagementUtility::extPath($skinExtKey) . $stylesheetDir;
684 }
685 }
686 }
687 }
688 return $stylesheetDirectories;
689 }
690
691 /**
692 * Returns generator meta tag
693 *
694 * @return string <meta> tag with name "generator
695 */
696 public function generator()
697 {
698 return 'TYPO3 CMS, ' . TYPO3_URL_GENERAL . ', &#169; Kasper Sk&#229;rh&#248;j ' . TYPO3_copyright_year . ', extensions are copyright of their respective owners.';
699 }
700
701 /**
702 * Returns X-UA-Compatible meta tag
703 * @deprecated
704 *
705 * @param string $content Content of the compatible tag (default: IE-8)
706 * @return string <meta http-equiv="X-UA-Compatible" content="???" />
707 */
708 public function xUaCompatible($content = 'IE=8')
709 {
710 trigger_error('Method DocumentTemplate->xUaCompatible is deprecated and will be removed with v10. Use pageRenderer->setMetaTag instead.', E_USER_DEPRECATED);
711 return '<meta http-equiv="X-UA-Compatible" content="' . $content . '" />';
712 }
713
714 /*****************************************
715 *
716 * OTHER ELEMENTS
717 * Tables, buttons, formatting dimmed/red strings
718 *
719 ******************************************/
720
721 /**
722 * Function to load a HTML template file with markers.
723 * When calling from own extension, use syntax getHtmlTemplate('EXT:extkey/template.html')
724 *
725 * @param string $filename tmpl name, usually in the typo3/template/ directory
726 * @return string HTML of template
727 */
728 public function getHtmlTemplate($filename)
729 {
730 // setting the name of the original HTML template
731 $this->moduleTemplateFilename = $filename;
732 if ($GLOBALS['TBE_STYLES']['htmlTemplates'][$filename]) {
733 $filename = $GLOBALS['TBE_STYLES']['htmlTemplates'][$filename];
734 }
735 $filename = GeneralUtility::getFileAbsFileName($filename);
736 return $filename !== '' ? file_get_contents($filename) : '';
737 }
738
739 /**
740 * Define the template for the module
741 *
742 * @param string $filename filename
743 */
744 public function setModuleTemplate($filename)
745 {
746 $this->moduleTemplate = $this->getHtmlTemplate($filename);
747 }
748
749 /**
750 * Put together the various elements for the module <body> using a static HTML
751 * template
752 *
753 * @param array $pageRecord Record of the current page, used for page path and info
754 * @param array $buttons HTML for all buttons
755 * @param array $markerArray HTML for all other markers
756 * @param array $subpartArray HTML for the subparts
757 * @return string Composite HTML
758 */
759 public function moduleBody($pageRecord = [], $buttons = [], $markerArray = [], $subpartArray = [])
760 {
761 // Get the HTML template for the module
762 $moduleBody = $this->templateService->getSubpart($this->moduleTemplate, '###FULLDOC###');
763 // Add CSS
764 $this->inDocStylesArray[] = 'html { overflow: hidden; }';
765 // Get the page path for the docheader
766 $markerArray['PAGEPATH'] = $this->getPagePath($pageRecord);
767 // Get the page info for the docheader
768 $markerArray['PAGEINFO'] = $this->getPageInfo($pageRecord);
769 // Get all the buttons for the docheader
770 $docHeaderButtons = $this->getDocHeaderButtons($buttons);
771 // Merge docheader buttons with the marker array
772 $markerArray = array_merge($markerArray, $docHeaderButtons);
773 // replacing subparts
774 foreach ($subpartArray as $marker => $content) {
775 $moduleBody = $this->templateService->substituteSubpart($moduleBody, $marker, $content);
776 }
777 // adding flash messages
778 if ($this->showFlashMessages) {
779 $flashMessages = $this->getFlashMessages();
780 if (!empty($flashMessages)) {
781 $markerArray['FLASHMESSAGES'] = $flashMessages;
782 // If there is no dedicated marker for the messages present
783 // then force them to appear before the content
784 if (strpos($moduleBody, '###FLASHMESSAGES###') === false) {
785 $moduleBody = str_replace('###CONTENT###', '###FLASHMESSAGES######CONTENT###', $moduleBody);
786 }
787 }
788 }
789 // Hook for adding more markers/content to the page, like the version selector
790 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/template.php']['moduleBodyPostProcess'] ?? [] as $funcRef) {
791 $params = [
792 'moduleTemplateFilename' => &$this->moduleTemplateFilename,
793 'moduleTemplate' => &$this->moduleTemplate,
794 'moduleBody' => &$moduleBody,
795 'markers' => &$markerArray,
796 'parentObject' => &$this
797 ];
798 GeneralUtility::callUserFunction($funcRef, $params, $this);
799 }
800 // Replacing all markers with the finished markers and return the HTML content
801 return $this->templateService->substituteMarkerArray($moduleBody, $markerArray, '###|###');
802 }
803
804 /**
805 * Get the default rendered FlashMessages from queue
806 *
807 * @return string
808 */
809 public function getFlashMessages()
810 {
811 /** @var $flashMessageService \TYPO3\CMS\Core\Messaging\FlashMessageService */
812 $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
813 /** @var $defaultFlashMessageQueue \TYPO3\CMS\Core\Messaging\FlashMessageQueue */
814 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
815 return $defaultFlashMessageQueue->renderFlashMessages();
816 }
817
818 /**
819 * Fill the button lists with the defined HTML
820 *
821 * @param array $buttons HTML for all buttons
822 * @return array Containing HTML for both buttonlists
823 */
824 protected function getDocHeaderButtons($buttons)
825 {
826 $markers = [];
827 // Fill buttons for left and right float
828 $floats = ['left', 'right'];
829 foreach ($floats as $key) {
830 // Get the template for each float
831 $buttonTemplate = $this->templateService->getSubpart($this->moduleTemplate, '###BUTTON_GROUPS_' . strtoupper($key) . '###');
832 // Fill the button markers in this float
833 $buttonTemplate = $this->templateService->substituteMarkerArray($buttonTemplate, $buttons, '###|###', true);
834 // getting the wrap for each group
835 $buttonWrap = $this->templateService->getSubpart($this->moduleTemplate, '###BUTTON_GROUP_WRAP###');
836 // looping through the groups (max 6) and remove the empty groups
837 for ($groupNumber = 1; $groupNumber < 6; $groupNumber++) {
838 $buttonMarker = '###BUTTON_GROUP' . $groupNumber . '###';
839 $buttonGroup = $this->templateService->getSubpart($buttonTemplate, $buttonMarker);
840 if (trim($buttonGroup)) {
841 if ($buttonWrap) {
842 $buttonGroup = $this->templateService->substituteMarker($buttonWrap, '###BUTTONS###', $buttonGroup);
843 }
844 $buttonTemplate = $this->templateService->substituteSubpart($buttonTemplate, $buttonMarker, trim($buttonGroup));
845 }
846 }
847 // Replace the marker with the template and remove all line breaks (for IE compat)
848 $markers['BUTTONLIST_' . strtoupper($key)] = str_replace(LF, '', $buttonTemplate);
849 }
850 // Hook for manipulating docHeaderButtons
851 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/template.php']['docHeaderButtonsHook'] ?? [] as $funcRef) {
852 $params = [
853 'buttons' => $buttons,
854 'markers' => &$markers,
855 'pObj' => &$this
856 ];
857 GeneralUtility::callUserFunction($funcRef, $params, $this);
858 }
859 return $markers;
860 }
861
862 /**
863 * Generate the page path for docheader
864 *
865 * @param array $pageRecord Current page
866 * @return string Page path
867 */
868 protected function getPagePath($pageRecord)
869 {
870 // Is this a real page
871 if (is_array($pageRecord) && $pageRecord['uid']) {
872 $title = substr($pageRecord['_thePathFull'], 0, -1);
873 // Remove current page title
874 $pos = strrpos($title, $pageRecord['title']);
875 if ($pos !== false) {
876 $title = substr($title, 0, $pos);
877 }
878 } else {
879 $title = '';
880 }
881 // Setting the path of the page
882 $pagePath = htmlspecialchars($GLOBALS['LANG']->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.path')) . ': <span class="typo3-docheader-pagePath">';
883 // crop the title to title limit (or 50, if not defined)
884 $cropLength = empty($GLOBALS['BE_USER']->uc['titleLen']) ? 50 : $GLOBALS['BE_USER']->uc['titleLen'];
885 $croppedTitle = GeneralUtility::fixed_lgd_cs($title, -$cropLength);
886 if ($croppedTitle !== $title) {
887 $pagePath .= '<abbr title="' . htmlspecialchars($title) . '">' . htmlspecialchars($croppedTitle) . '</abbr>';
888 } else {
889 $pagePath .= htmlspecialchars($title);
890 }
891 $pagePath .= '</span>';
892 return $pagePath;
893 }
894
895 /**
896 * Setting page icon with context menu + uid for docheader
897 *
898 * @param array $pageRecord Current page
899 * @return string Page info
900 */
901 protected function getPageInfo($pageRecord)
902 {
903 // Add icon with context menu, etc:
904 // If there IS a real page
905 if (is_array($pageRecord) && $pageRecord['uid']) {
906 $alttext = BackendUtility::getRecordIconAltText($pageRecord, 'pages');
907 $iconImg = '<span title="' . htmlspecialchars($alttext) . '">' . $this->iconFactory->getIconForRecord('pages', $pageRecord, Icon::SIZE_SMALL)->render() . '</span>';
908 // Make Icon:
909 $theIcon = BackendUtility::wrapClickMenuOnIcon($iconImg, 'pages', $pageRecord['uid']);
910 $uid = $pageRecord['uid'];
911 $title = BackendUtility::getRecordTitle('pages', $pageRecord);
912 } else {
913 // On root-level of page tree
914 // Make Icon
915 $iconImg = '<span title="' . htmlspecialchars($GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']) . '">' . $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render() . '</span>';
916 if ($GLOBALS['BE_USER']->isAdmin()) {
917 $theIcon = BackendUtility::wrapClickMenuOnIcon($iconImg, 'pages');
918 } else {
919 $theIcon = $iconImg;
920 }
921 $uid = '0';
922 $title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
923 }
924 // Setting icon with context menu + uid
925 $pageInfo = $theIcon . '<strong>' . htmlspecialchars($title) . '&nbsp;[' . $uid . ']</strong>';
926 return $pageInfo;
927 }
928
929 /**
930 * Retrieves configured favicon for backend (with fallback)
931 *
932 * @return string
933 */
934 protected function getBackendFavicon()
935 {
936 $backendFavicon = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend', 'backendFavicon');
937 if (!empty($backendFavicon)) {
938 $path = $this->getUriForFileName($backendFavicon);
939 } else {
940 $path = ExtensionManagementUtility::extPath('backend') . 'Resources/Public/Icons/favicon.ico';
941 }
942 return PathUtility::getAbsoluteWebPath($path);
943 }
944
945 /**
946 * Returns the uri of a relative reference, resolves the "EXT:" prefix
947 * (way of referring to files inside extensions) and checks that the file is inside
948 * the project root of the TYPO3 installation
949 *
950 * @param string $filename The input filename/filepath to evaluate
951 * @return string Returns the filename of $filename if valid, otherwise blank string.
952 */
953 protected function getUriForFileName($filename)
954 {
955 if (strpos($filename, '://')) {
956 return $filename;
957 }
958 $urlPrefix = '';
959 if (strpos($filename, 'EXT:') === 0) {
960 $absoluteFilename = GeneralUtility::getFileAbsFileName($filename);
961 $filename = '';
962 if ($absoluteFilename !== '') {
963 $filename = PathUtility::getAbsoluteWebPath($absoluteFilename);
964 }
965 } elseif (strpos($filename, '/') !== 0) {
966 $urlPrefix = GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
967 }
968 return $urlPrefix . $filename;
969 }
970 }