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