Completed
Push — master ( 7a82fc...a55b2b )
by
unknown
14:42
created

processHtmlBasedRenderingSettings()   F

Complexity

Conditions 183
Paths 0

Size

Total Lines 653
Code Lines 428

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 183
eloc 428
c 1
b 0
f 0
nc 0
nop 3
dl 0
loc 653
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
4
namespace TYPO3\CMS\Frontend\Http;
5
6
/*
7
 * This file is part of the TYPO3 CMS project.
8
 *
9
 * It is free software; you can redistribute it and/or modify it under
10
 * the terms of the GNU General Public License, either version 2
11
 * of the License, or any later version.
12
 *
13
 * For the full copyright and license information, please read the
14
 * LICENSE.txt file that was distributed with this source code.
15
 *
16
 * The TYPO3 project - inspiring people to share!
17
 */
18
19
use Psr\EventDispatcher\EventDispatcherInterface;
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Http\Server\RequestHandlerInterface;
23
use TYPO3\CMS\Core\Core\Environment;
24
use TYPO3\CMS\Core\Http\NullResponse;
25
use TYPO3\CMS\Core\Http\Response;
26
use TYPO3\CMS\Core\Information\Typo3Information;
27
use TYPO3\CMS\Core\Localization\LanguageService;
28
use TYPO3\CMS\Core\Page\AssetCollector;
29
use TYPO3\CMS\Core\Page\PageRenderer;
30
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
31
use TYPO3\CMS\Core\TimeTracker\TimeTracker;
32
use TYPO3\CMS\Core\Type\File\ImageInfo;
33
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
use TYPO3\CMS\Core\Utility\PathUtility;
36
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
37
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
38
use TYPO3\CMS\Frontend\Event\ModifyHrefLangTagsEvent;
39
use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
40
41
/**
42
 * This is the main entry point of the TypoScript driven standard front-end.
43
 *
44
 * "handle()" is called when all PSR-15 middlewares have been set up the PSR-7 ServerRequest object and the following
45
 * things have been evaluated
46
 * - correct page ID, page type (typeNum), rootline, MP etc.
47
 * - info if is cached content already available
48
 * - proper language
49
 * - proper TypoScript which should be processed.
50
 *
51
 * Then, this class is able to render the actual HTTP body part built via TypoScript. Here this is split into two parts:
52
 * - Everything included in <body>, done via page.10, page.20 etc.
53
 * - Everything around.
54
 *
55
 * If the content has been built together within the cache (cache_pages), it is fetched directly, and
56
 * any so-called "uncached" content is generated again.
57
 *
58
 * Some further hooks allow to post-processing the content.
59
 *
60
 * Then the right HTTP response headers are compiled together and sent as well.
61
 */
62
class RequestHandler implements RequestHandlerInterface
63
{
64
    /**
65
     * Instance of the timetracker
66
     * @var TimeTracker
67
     */
68
    protected $timeTracker;
69
70
    /**
71
     * @var EventDispatcherInterface
72
     */
73
    protected $eventDispatcher;
74
75
    public function __construct(EventDispatcherInterface $eventDispatcher)
76
    {
77
        $this->eventDispatcher = $eventDispatcher;
78
    }
79
80
    /**
81
     * Sets the global GET and POST to the values, so if people access $_GET and $_POST
82
     * Within hooks starting NOW (e.g. cObject), they get the "enriched" data from query params.
83
     *
84
     * This needs to be run after the request object has been enriched with modified GET/POST variables.
85
     *
86
     * @param ServerRequestInterface $request
87
     * @internal this safety net will be removed in TYPO3 v10.0.
88
     */
89
    protected function resetGlobalsToCurrentRequest(ServerRequestInterface $request)
90
    {
91
        if ($request->getQueryParams() !== $_GET) {
92
            $queryParams = $request->getQueryParams();
93
            $_GET = $queryParams;
94
            $GLOBALS['HTTP_GET_VARS'] = $_GET;
95
        }
96
        if ($request->getMethod() === 'POST') {
97
            $parsedBody = $request->getParsedBody();
98
            if (is_array($parsedBody) && $parsedBody !== $_POST) {
99
                $_POST = $parsedBody;
100
                $GLOBALS['HTTP_POST_VARS'] = $_POST;
101
            }
102
        }
103
        $GLOBALS['TYPO3_REQUEST'] = $request;
104
    }
105
106
    /**
107
     * Handles a frontend request, after finishing running middlewares
108
     *
109
     * @param ServerRequestInterface $request
110
     * @return ResponseInterface
111
     */
112
    public function handle(ServerRequestInterface $request): ResponseInterface
113
    {
114
        // Fetch the initialized time tracker object
115
        $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
116
        /** @var TypoScriptFrontendController $controller */
117
        $controller = $GLOBALS['TSFE'];
118
119
        $this->resetGlobalsToCurrentRequest($request);
120
121
        // Generate page
122
        if ($controller->isGeneratePage()) {
123
            $this->timeTracker->push('Page generation');
124
            $controller->generatePage_preProcessing();
125
            $controller->preparePageContentGeneration($request);
126
127
            // Content generation
128
            $this->timeTracker->incStackPointer();
129
            $this->timeTracker->push($controller->sPre, 'PAGE');
130
131
            $controller->content = $this->generatePageContent($controller, $request);
132
133
            $this->timeTracker->pull($this->timeTracker->LR ? $controller->content : '');
134
            $this->timeTracker->decStackPointer();
135
136
            $controller->generatePage_postProcessing();
137
            $this->timeTracker->pull();
138
        }
139
        $controller->releaseLocks();
140
141
        // Render non-cached page parts by replacing placeholders which are taken from cache or added during page generation
142
        if ($controller->isINTincScript()) {
143
            if (!$controller->isGeneratePage()) {
144
                // When page was generated, this was already called. Avoid calling this twice.
145
                $controller->preparePageContentGeneration($request);
146
            }
147
            $this->timeTracker->push('Non-cached objects');
148
            $controller->INTincScript();
149
            $this->timeTracker->pull();
150
        }
151
152
        // Create a Response object when sending content
153
        $response = new Response();
154
155
        // Output content
156
        // The if() condition can be removed in TYPO3 v11, which means that TYPO3 will not return a NullResponse
157
        // by default anymore, which it hasn't done without any extensions anyway already.
158
        $isOutputting = $controller->isOutputting(true);
159
        if ($isOutputting) {
0 ignored issues
show
introduced by
The condition $isOutputting is always true.
Loading history...
160
            $this->timeTracker->push('Print Content');
161
            $response = $controller->applyHttpHeadersToResponse($response);
162
            $controller->processContentForOutput(true);
163
            $this->timeTracker->pull();
164
        }
165
166
        // Hook for "end-of-frontend"
167
        $_params = ['pObj' => &$controller];
168
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_eofe'])) {
169
            trigger_error('The hook $TYPO3_CONF_VARS[SC_OPTIONS][tslib/class.tslib_fe.php][hook_eofe] will be removed in TYPO3 v11.0. The same functionality can be achieved by using a PSR-15 middleware.', E_USER_DEPRECATED);
170
        }
171
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['hook_eofe'] ?? [] as $_funcRef) {
172
            GeneralUtility::callUserFunction($_funcRef, $_params, $controller);
173
        }
174
        $this->displayPreviewInfoMessage($controller);
175
176
        if ($isOutputting) {
0 ignored issues
show
introduced by
The condition $isOutputting is always true.
Loading history...
177
            $response->getBody()->write($controller->content);
178
            return $response;
179
        }
180
        return new NullResponse();
181
    }
182
183
    /**
184
     * Generates the main body part for the page, and if "config.disableAllHeaderCode" is not active, triggers
185
     * pageRenderer to evaluate includeCSS, headTag etc. TypoScript processing to populate the pageRenderer.
186
     *
187
     * @param TypoScriptFrontendController $controller
188
     * @param ServerRequestInterface $request
189
     * @return string
190
     */
191
    protected function generatePageContent(TypoScriptFrontendController $controller, ServerRequestInterface $request): string
192
    {
193
        // Generate the main content between the <body> tags
194
        // This has to be done first, as some additional TSFE-related code could have been written
195
        $pageContent = $this->generatePageBodyContent($controller);
196
        // If 'disableAllHeaderCode' is set, all the pageRenderer settings are not evaluated
197
        if ($controller->config['config']['disableAllHeaderCode'] ?? false) {
198
            return $pageContent;
199
        }
200
        // Now, populate pageRenderer with all additional data
201
        $this->processHtmlBasedRenderingSettings($controller, $controller->getLanguage(), $request);
202
        $pageRenderer = $this->getPageRenderer();
203
        // Add previously generated page content within the <body> tag afterwards
204
        $pageRenderer->addBodyContent(LF . $pageContent);
205
        if ($controller->isINTincScript()) {
206
            // Store the serialized pageRenderer in configuration
207
            $controller->config['INTincScript_ext']['pageRenderer'] = serialize($pageRenderer);
208
            // Store the serialized AssetCollector in configuration
209
            $controller->config['INTincScript_ext']['assetCollector'] = serialize(GeneralUtility::makeInstance(AssetCollector::class));
210
            // Render complete page, keep placeholders for JavaScript and CSS
211
            return $pageRenderer->renderPageWithUncachedObjects($controller->config['INTincScript_ext']['divKey']);
212
        }
213
        // Render complete page
214
        return $pageRenderer->render();
215
    }
216
217
    /**
218
     * Generates the main content part within <body> tags (except JS files/CSS files), this means:
219
     * render everything that can be cached, otherwise put placeholders for COA_INT/USER_INT objects
220
     * in the content that is processed later-on.
221
     *
222
     * @param TypoScriptFrontendController $controller
223
     * @return string
224
     */
225
    protected function generatePageBodyContent(TypoScriptFrontendController $controller): string
226
    {
227
        $pageContent = $controller->cObj->cObjGet($controller->pSetup) ?: '';
0 ignored issues
show
Bug introduced by
It seems like $controller->pSetup can also be of type string; however, parameter $setup of TYPO3\CMS\Frontend\Conte...jectRenderer::cObjGet() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

227
        $pageContent = $controller->cObj->cObjGet(/** @scrutinizer ignore-type */ $controller->pSetup) ?: '';
Loading history...
228
        if ($controller->pSetup['wrap'] ?? false) {
229
            $pageContent = $controller->cObj->wrap($pageContent, $controller->pSetup['wrap']);
230
        }
231
        if ($controller->pSetup['stdWrap.'] ?? false) {
232
            $pageContent = $controller->cObj->stdWrap($pageContent, $controller->pSetup['stdWrap.']);
233
        }
234
        return $pageContent;
235
    }
236
237
    /**
238
     * At this point, the cacheable content has just been generated (thus, all content is available but hasn't been added
239
     * to PageRenderer yet). The method is called after the "main" page content, since some JS may be inserted at that point
240
     * that has been registered by cacheable plugins.
241
     * PageRenderer is now populated with all <head> data and additional JavaScript/CSS/FooterData/HeaderData that can be cached.
242
     * Once finished, the content is added to the >addBodyContent() functionality.
243
     *
244
     * @param TypoScriptFrontendController $controller
245
     * @param SiteLanguage $siteLanguage
246
     * @param ServerRequestInterface $request
247
     */
248
    protected function processHtmlBasedRenderingSettings(TypoScriptFrontendController $controller, SiteLanguage $siteLanguage, ServerRequestInterface $request): void
249
    {
250
        $pageRenderer = $this->getPageRenderer();
251
        if ($controller->config['config']['moveJsFromHeaderToFooter'] ?? false) {
252
            $pageRenderer->enableMoveJsFromHeaderToFooter();
253
        }
254
        if ($controller->config['config']['pageRendererTemplateFile'] ?? false) {
255
            try {
256
                $file = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($controller->config['config']['pageRendererTemplateFile']);
257
                $pageRenderer->setTemplateFile($file);
258
            } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
259
                // do nothing
260
            }
261
        }
262
        $headerComment = trim($controller->config['config']['headerComment'] ?? '');
263
        if ($headerComment) {
264
            $pageRenderer->addInlineComment("\t" . str_replace(LF, LF . "\t", $headerComment) . LF);
265
        }
266
        // Setting charset:
267
        $theCharset = $controller->metaCharset;
268
        $htmlTagAttributes = [];
269
        $htmlLang = $siteLanguage->getHreflang() ?: '';
270
271
        if ($siteLanguage->getDirection()) {
272
            $htmlTagAttributes['dir'] = htmlspecialchars($siteLanguage->getDirection());
273
        }
274
        // Setting document type:
275
        $docTypeParts = [];
276
        $xmlDocument = true;
277
        // Part 1: XML prologue
278
        switch ((string)($controller->config['config']['xmlprologue'] ?? '')) {
279
            case 'none':
280
                $xmlDocument = false;
281
                break;
282
            case 'xml_10':
283
                $docTypeParts[] = '<?xml version="1.0" encoding="' . $theCharset . '"?>';
284
                break;
285
            case 'xml_11':
286
                $docTypeParts[] = '<?xml version="1.1" encoding="' . $theCharset . '"?>';
287
                break;
288
            case '':
289
                if ($controller->xhtmlVersion) {
290
                    $docTypeParts[] = '<?xml version="1.0" encoding="' . $theCharset . '"?>';
291
                } else {
292
                    $xmlDocument = false;
293
                }
294
                break;
295
            default:
296
                $docTypeParts[] = $controller->config['config']['xmlprologue'];
297
        }
298
        // Part 2: DTD
299
        $doctype = $controller->config['config']['doctype'] ?? null;
300
        $defaultTypeAttributeForJavaScript = 'text/javascript';
301
        if ($doctype) {
302
            switch ($doctype) {
303
                case 'xhtml_trans':
304
                    $docTypeParts[] = '<!DOCTYPE html
305
    PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
306
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
307
                    break;
308
                case 'xhtml_strict':
309
                    $docTypeParts[] = '<!DOCTYPE html
310
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
311
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
312
                    break;
313
                case 'xhtml_basic':
314
                    $docTypeParts[] = '<!DOCTYPE html
315
    PUBLIC "-//W3C//DTD XHTML Basic 1.0//EN"
316
    "http://www.w3.org/TR/xhtml-basic/xhtml-basic10.dtd">';
317
                    break;
318
                case 'xhtml_11':
319
                    $docTypeParts[] = '<!DOCTYPE html
320
    PUBLIC "-//W3C//DTD XHTML 1.1//EN"
321
    "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
322
                    break;
323
                case 'xhtml+rdfa_10':
324
                    $docTypeParts[] = '<!DOCTYPE html
325
    PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN"
326
    "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">';
327
                    break;
328
                case 'html5':
329
                    $defaultTypeAttributeForJavaScript = '';
330
                    $docTypeParts[] = '<!DOCTYPE html>';
331
                    if ($xmlDocument) {
332
                        $pageRenderer->setMetaCharsetTag('<meta charset="|" />');
333
                    } else {
334
                        $pageRenderer->setMetaCharsetTag('<meta charset="|">');
335
                    }
336
                    break;
337
                case 'none':
338
                    break;
339
                default:
340
                    $docTypeParts[] = $doctype;
341
            }
342
        } else {
343
            $docTypeParts[] = '<!DOCTYPE html>';
344
            if ($xmlDocument) {
345
                $pageRenderer->setMetaCharsetTag('<meta charset="|" />');
346
            } else {
347
                $pageRenderer->setMetaCharsetTag('<meta charset="|">');
348
            }
349
            $defaultTypeAttributeForJavaScript = '';
350
        }
351
        if ($htmlLang) {
352
            if ($controller->xhtmlVersion) {
353
                $htmlTagAttributes['xml:lang'] = $htmlLang;
354
            }
355
            if ($controller->xhtmlVersion < 110 || $doctype === 'html5') {
356
                $htmlTagAttributes['lang'] = $htmlLang;
357
            }
358
        }
359
        if ($controller->xhtmlVersion || $doctype === 'html5' && $xmlDocument) {
360
            // We add this to HTML5 to achieve a slightly better backwards compatibility
361
            $htmlTagAttributes['xmlns'] = 'http://www.w3.org/1999/xhtml';
362
            if (is_array($controller->config['config']['namespaces.'])) {
363
                foreach ($controller->config['config']['namespaces.'] as $prefix => $uri) {
364
                    // $uri gets htmlspecialchared later
365
                    $htmlTagAttributes['xmlns:' . htmlspecialchars($prefix)] = $uri;
366
                }
367
            }
368
        }
369
        // Swap XML and doctype order around (for MSIE / Opera standards compliance)
370
        if ($controller->config['config']['doctypeSwitch'] ?? false) {
371
            $docTypeParts = array_reverse($docTypeParts);
372
        }
373
        // Adding doctype parts:
374
        if (!empty($docTypeParts)) {
375
            $pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts));
376
        }
377
        // Begin header section:
378
        $htmlTag = $this->generateHtmlTag($htmlTagAttributes, $controller->config['config'] ?? [], $controller->cObj);
0 ignored issues
show
Bug introduced by
It seems like $controller->cObj can also be of type string; however, parameter $cObj of TYPO3\CMS\Frontend\Http\...dler::generateHtmlTag() does only seem to accept TYPO3\CMS\Frontend\Conte...t\ContentObjectRenderer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

378
        $htmlTag = $this->generateHtmlTag($htmlTagAttributes, $controller->config['config'] ?? [], /** @scrutinizer ignore-type */ $controller->cObj);
Loading history...
379
        $pageRenderer->setHtmlTag($htmlTag);
380
        // Head tag:
381
        $headTag = $controller->pSetup['headTag'] ?? '<head>';
382
        if (isset($controller->pSetup['headTag.'])) {
383
            $headTag = $controller->cObj->stdWrap($headTag, $controller->pSetup['headTag.']);
384
        }
385
        $pageRenderer->setHeadTag($headTag);
386
        // Setting charset meta tag:
387
        $pageRenderer->setCharSet($theCharset);
388
        $pageRenderer->addInlineComment(GeneralUtility::makeInstance(Typo3Information::class)->getInlineHeaderComment());
389
        if ($controller->baseUrl) {
390
            $pageRenderer->setBaseUrl($controller->baseUrl);
391
        }
392
        if ($controller->pSetup['shortcutIcon'] ?? false) {
393
            try {
394
                $favIcon = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($controller->pSetup['shortcutIcon']);
395
                $iconFileInfo = GeneralUtility::makeInstance(ImageInfo::class, Environment::getPublicPath() . '/' . $favIcon);
396
                if ($iconFileInfo->isFile()) {
397
                    $iconMimeType = $iconFileInfo->getMimeType();
398
                    if ($iconMimeType) {
399
                        $iconMimeType = ' type="' . $iconMimeType . '"';
400
                        $pageRenderer->setIconMimeType($iconMimeType);
401
                    }
402
                    $pageRenderer->setFavIcon(PathUtility::getAbsoluteWebPath($controller->absRefPrefix . $favIcon));
403
                }
404
            } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
405
                // do nothing
406
            }
407
        }
408
        // Including CSS files
409
        if (is_array($controller->tmpl->setup['plugin.'] ?? null)) {
410
            $stylesFromPlugins = '';
411
            foreach ($controller->tmpl->setup['plugin.'] as $key => $iCSScode) {
412
                if (is_array($iCSScode)) {
413
                    if ($iCSScode['_CSS_DEFAULT_STYLE'] && empty($controller->config['config']['removeDefaultCss'])) {
414
                        if (isset($iCSScode['_CSS_DEFAULT_STYLE.'])) {
415
                            $cssDefaultStyle = $controller->cObj->stdWrap($iCSScode['_CSS_DEFAULT_STYLE'], $iCSScode['_CSS_DEFAULT_STYLE.']);
416
                        } else {
417
                            $cssDefaultStyle = $iCSScode['_CSS_DEFAULT_STYLE'];
418
                        }
419
                        $stylesFromPlugins .= '/* default styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssDefaultStyle . LF;
420
                    }
421
                    if ($iCSScode['_CSS_PAGE_STYLE'] && empty($controller->config['config']['removePageCss'])) {
422
                        $cssPageStyle = implode(LF, $iCSScode['_CSS_PAGE_STYLE']);
423
                        if (isset($iCSScode['_CSS_PAGE_STYLE.'])) {
424
                            $cssPageStyle = $controller->cObj->stdWrap($cssPageStyle, $iCSScode['_CSS_PAGE_STYLE.']);
425
                        }
426
                        $cssPageStyle = '/* specific page styles for extension "' . substr($key, 0, -1) . '" */' . LF . $cssPageStyle;
427
                        $this->addCssToPageRenderer($controller, $cssPageStyle, true, 'InlinePageCss');
428
                    }
429
                }
430
            }
431
            if (!empty($stylesFromPlugins)) {
432
                $this->addCssToPageRenderer($controller, $stylesFromPlugins, false, 'InlineDefaultCss');
433
            }
434
        }
435
        /**********************************************************************/
436
        /* config.includeCSS / config.includeCSSLibs
437
        /**********************************************************************/
438
        if (is_array($controller->pSetup['includeCSS.'] ?? null)) {
439
            foreach ($controller->pSetup['includeCSS.'] as $key => $CSSfile) {
440
                if (!is_array($CSSfile)) {
441
                    $cssFileConfig = &$controller->pSetup['includeCSS.'][$key . '.'];
442
                    if (isset($cssFileConfig['if.']) && !$controller->cObj->checkIf($cssFileConfig['if.'])) {
443
                        continue;
444
                    }
445
                    if ($cssFileConfig['external']) {
446
                        $ss = $CSSfile;
447
                    } else {
448
                        try {
449
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($CSSfile);
450
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
451
                            $ss = null;
452
                        }
453
                    }
454
                    if ($ss) {
455
                        if ($cssFileConfig['import']) {
456
                            if (!$cssFileConfig['external'] && $ss[0] !== '/') {
457
                                // To fix MSIE 6 that cannot handle these as relative paths (according to Ben v Ende)
458
                                $ss = GeneralUtility::dirname(GeneralUtility::getIndpEnv('SCRIPT_NAME')) . '/' . $ss;
459
                            }
460
                            $cssMedia = !empty($cssFileConfig['media']) ? ' ' . htmlspecialchars($cssFileConfig['media']) : '';
461
                            $pageRenderer->addCssInlineBlock('import_' . $key, '@import url("' . htmlspecialchars($ss) . '")' . $cssMedia . ';', empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop']);
462
                        } else {
463
                            $pageRenderer->addCssFile(
464
                                $ss,
465
                                $cssFileConfig['alternate'] ? 'alternate stylesheet' : 'stylesheet',
466
                                $cssFileConfig['media'] ?: 'all',
467
                                $cssFileConfig['title'] ?: '',
468
                                $cssFileConfig['external']  || (bool)$cssFileConfig['inline'] ? false : empty($cssFileConfig['disableCompression']),
469
                                (bool)$cssFileConfig['forceOnTop'],
470
                                $cssFileConfig['allWrap'],
471
                                (bool)$cssFileConfig['excludeFromConcatenation'] || (bool)$cssFileConfig['inline'],
472
                                $cssFileConfig['allWrap.']['splitChar'],
473
                                $cssFileConfig['inline']
474
                            );
475
                            unset($cssFileConfig);
476
                        }
477
                    }
478
                }
479
            }
480
        }
481
        if (is_array($controller->pSetup['includeCSSLibs.'] ?? null)) {
482
            foreach ($controller->pSetup['includeCSSLibs.'] as $key => $CSSfile) {
483
                if (!is_array($CSSfile)) {
484
                    $cssFileConfig = &$controller->pSetup['includeCSSLibs.'][$key . '.'];
485
                    if (isset($cssFileConfig['if.']) && !$controller->cObj->checkIf($cssFileConfig['if.'])) {
486
                        continue;
487
                    }
488
                    if ($cssFileConfig['external']) {
489
                        $ss = $CSSfile;
490
                    } else {
491
                        try {
492
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($CSSfile);
493
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
494
                            $ss = null;
495
                        }
496
                    }
497
                    if ($ss) {
498
                        if ($cssFileConfig['import']) {
499
                            if (!$cssFileConfig['external'] && $ss[0] !== '/') {
500
                                // To fix MSIE 6 that cannot handle these as relative paths (according to Ben v Ende)
501
                                $ss = GeneralUtility::dirname(GeneralUtility::getIndpEnv('SCRIPT_NAME')) . '/' . $ss;
502
                            }
503
                            $cssMedia = !empty($cssFileConfig['media']) ? ' ' . htmlspecialchars($cssFileConfig['media']) : '';
504
                            $pageRenderer->addCssInlineBlock('import_' . $key, '@import url("' . htmlspecialchars($ss) . '")' . $cssMedia . ';', empty($cssFileConfig['disableCompression']), (bool)$cssFileConfig['forceOnTop']);
505
                        } else {
506
                            $pageRenderer->addCssLibrary(
507
                                $ss,
508
                                $cssFileConfig['alternate'] ? 'alternate stylesheet' : 'stylesheet',
509
                                $cssFileConfig['media'] ?: 'all',
510
                                $cssFileConfig['title'] ?: '',
511
                                $cssFileConfig['external'] || (bool)$cssFileConfig['inline'] ? false : empty($cssFileConfig['disableCompression']),
512
                                (bool)$cssFileConfig['forceOnTop'],
513
                                $cssFileConfig['allWrap'],
514
                                (bool)$cssFileConfig['excludeFromConcatenation'] || (bool)$cssFileConfig['inline'],
515
                                $cssFileConfig['allWrap.']['splitChar'],
516
                                $cssFileConfig['inline']
517
                            );
518
                            unset($cssFileConfig);
519
                        }
520
                    }
521
                }
522
            }
523
        }
524
525
        // CSS_inlineStyle from TS
526
        $style = trim($controller->pSetup['CSS_inlineStyle'] ?? '');
527
        $style .= $controller->cObj->cObjGet($controller->pSetup['cssInline.'] ?? null, 'cssInline.');
528
        if (trim($style)) {
529
            $this->addCssToPageRenderer($controller, $style, true, 'additionalTSFEInlineStyle');
530
        }
531
        // JavaScript library files
532
        if (is_array($controller->pSetup['includeJSLibs.'] ?? null)) {
533
            foreach ($controller->pSetup['includeJSLibs.'] as $key => $JSfile) {
534
                if (!is_array($JSfile)) {
535
                    if (isset($controller->pSetup['includeJSLibs.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSLibs.'][$key . '.']['if.'])) {
536
                        continue;
537
                    }
538
                    if ($controller->pSetup['includeJSLibs.'][$key . '.']['external']) {
539
                        $ss = $JSfile;
540
                    } else {
541
                        try {
542
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile);
543
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
544
                            $ss = null;
545
                        }
546
                    }
547
                    if ($ss) {
548
                        $jsFileConfig = &$controller->pSetup['includeJSLibs.'][$key . '.'];
549
                        $type = $jsFileConfig['type'] ?? $defaultTypeAttributeForJavaScript;
550
                        $crossOrigin = $jsFileConfig['crossorigin'];
551
                        if (!$crossOrigin && $jsFileConfig['integrity'] && $jsFileConfig['external']) {
552
                            $crossOrigin = 'anonymous';
553
                        }
554
                        $pageRenderer->addJsLibrary(
555
                            $key,
556
                            $ss,
557
                            $type,
558
                            $jsFileConfig['external'] ? false : empty($jsFileConfig['disableCompression']),
559
                            (bool)$jsFileConfig['forceOnTop'],
560
                            $jsFileConfig['allWrap'],
561
                            (bool)$jsFileConfig['excludeFromConcatenation'],
562
                            $jsFileConfig['allWrap.']['splitChar'],
563
                            (bool)$jsFileConfig['async'],
564
                            $jsFileConfig['integrity'],
565
                            (bool)$jsFileConfig['defer'],
566
                            $crossOrigin,
567
                            (bool)$jsFileConfig['nomodule']
568
                        );
569
                        unset($jsFileConfig);
570
                    }
571
                }
572
            }
573
        }
574
        if (is_array($controller->pSetup['includeJSFooterlibs.'] ?? null)) {
575
            foreach ($controller->pSetup['includeJSFooterlibs.'] as $key => $JSfile) {
576
                if (!is_array($JSfile)) {
577
                    if (isset($controller->pSetup['includeJSFooterlibs.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSFooterlibs.'][$key . '.']['if.'])) {
578
                        continue;
579
                    }
580
                    if ($controller->pSetup['includeJSFooterlibs.'][$key . '.']['external']) {
581
                        $ss = $JSfile;
582
                    } else {
583
                        try {
584
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile);
585
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
586
                            $ss = null;
587
                        }
588
                    }
589
                    if ($ss) {
590
                        $jsFileConfig = &$controller->pSetup['includeJSFooterlibs.'][$key . '.'];
591
                        $type = $jsFileConfig['type'] ?? $defaultTypeAttributeForJavaScript;
592
                        $crossorigin = $jsFileConfig['crossorigin'];
593
                        if (!$crossorigin && $jsFileConfig['integrity'] && $jsFileConfig['external']) {
594
                            $crossorigin = 'anonymous';
595
                        }
596
                        $pageRenderer->addJsFooterLibrary(
597
                            $key,
598
                            $ss,
599
                            $type,
600
                            $jsFileConfig['external'] ? false : empty($jsFileConfig['disableCompression']),
601
                            (bool)$jsFileConfig['forceOnTop'],
602
                            $jsFileConfig['allWrap'],
603
                            (bool)$jsFileConfig['excludeFromConcatenation'],
604
                            $jsFileConfig['allWrap.']['splitChar'],
605
                            (bool)$jsFileConfig['async'],
606
                            $jsFileConfig['integrity'],
607
                            (bool)$jsFileConfig['defer'],
608
                            $crossorigin,
609
                            (bool)$jsFileConfig['nomodule']
610
                        );
611
                        unset($jsFileConfig);
612
                    }
613
                }
614
            }
615
        }
616
        // JavaScript files
617
        if (is_array($controller->pSetup['includeJS.'] ?? null)) {
618
            foreach ($controller->pSetup['includeJS.'] as $key => $JSfile) {
619
                if (!is_array($JSfile)) {
620
                    if (isset($controller->pSetup['includeJS.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJS.'][$key . '.']['if.'])) {
621
                        continue;
622
                    }
623
                    if ($controller->pSetup['includeJS.'][$key . '.']['external']) {
624
                        $ss = $JSfile;
625
                    } else {
626
                        try {
627
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile);
628
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
629
                            $ss = null;
630
                        }
631
                    }
632
                    if ($ss) {
633
                        $jsConfig = &$controller->pSetup['includeJS.'][$key . '.'];
634
                        $type = $jsConfig['type'] ?? $defaultTypeAttributeForJavaScript;
635
                        $crossorigin = $jsConfig['crossorigin'];
636
                        if (!$crossorigin && $jsConfig['integrity'] && $jsConfig['external']) {
637
                            $crossorigin = 'anonymous';
638
                        }
639
                        $pageRenderer->addJsFile(
640
                            $ss,
641
                            $type,
642
                            $jsConfig['external'] ? false : empty($jsConfig['disableCompression']),
643
                            (bool)$jsConfig['forceOnTop'],
644
                            $jsConfig['allWrap'],
645
                            (bool)$jsConfig['excludeFromConcatenation'],
646
                            $jsConfig['allWrap.']['splitChar'],
647
                            (bool)$jsConfig['async'],
648
                            $jsConfig['integrity'],
649
                            (bool)$jsConfig['defer'],
650
                            $crossorigin,
651
                            (bool)$jsConfig['nomodule']
652
                        );
653
                        unset($jsConfig);
654
                    }
655
                }
656
            }
657
        }
658
        if (is_array($controller->pSetup['includeJSFooter.'] ?? null)) {
659
            foreach ($controller->pSetup['includeJSFooter.'] as $key => $JSfile) {
660
                if (!is_array($JSfile)) {
661
                    if (isset($controller->pSetup['includeJSFooter.'][$key . '.']['if.']) && !$controller->cObj->checkIf($controller->pSetup['includeJSFooter.'][$key . '.']['if.'])) {
662
                        continue;
663
                    }
664
                    if ($controller->pSetup['includeJSFooter.'][$key . '.']['external']) {
665
                        $ss = $JSfile;
666
                    } else {
667
                        try {
668
                            $ss = GeneralUtility::makeInstance(FilePathSanitizer::class)->sanitize($JSfile);
669
                        } catch (\TYPO3\CMS\Core\Resource\Exception $e) {
670
                            $ss = null;
671
                        }
672
                    }
673
                    if ($ss) {
674
                        $jsConfig = &$controller->pSetup['includeJSFooter.'][$key . '.'];
675
                        $type = $jsConfig['type'] ?? $defaultTypeAttributeForJavaScript;
676
                        $crossorigin = $jsConfig['crossorigin'];
677
                        if (!$crossorigin && $jsConfig['integrity'] && $jsConfig['external']) {
678
                            $crossorigin = 'anonymous';
679
                        }
680
                        $pageRenderer->addJsFooterFile(
681
                            $ss,
682
                            $type,
683
                            $jsConfig['external'] ? false : empty($jsConfig['disableCompression']),
684
                            (bool)$jsConfig['forceOnTop'],
685
                            $jsConfig['allWrap'],
686
                            (bool)$jsConfig['excludeFromConcatenation'],
687
                            $jsConfig['allWrap.']['splitChar'],
688
                            (bool)$jsConfig['async'],
689
                            $jsConfig['integrity'],
690
                            (bool)$jsConfig['defer'],
691
                            $crossorigin,
692
                            (bool)$jsConfig['nomodule']
693
                        );
694
                        unset($jsConfig);
695
                    }
696
                }
697
            }
698
        }
699
        // Headerdata
700
        if (is_array($controller->pSetup['headerData.'] ?? null)) {
701
            $pageRenderer->addHeaderData($controller->cObj->cObjGet($controller->pSetup['headerData.'], 'headerData.'));
702
        }
703
        // Footerdata
704
        if (is_array($controller->pSetup['footerData.'] ?? null)) {
705
            $pageRenderer->addFooterData($controller->cObj->cObjGet($controller->pSetup['footerData.'], 'footerData.'));
706
        }
707
        $controller->generatePageTitle();
708
709
        // @internal hook for EXT:seo, will be gone soon, do not use it in your own extensions
710
        $_params = ['page' => $controller->page];
711
        $_ref = null;
712
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['TYPO3\CMS\Frontend\Page\PageGenerator']['generateMetaTags'] ?? [] as $_funcRef) {
713
            GeneralUtility::callUserFunction($_funcRef, $_params, $_ref);
714
        }
715
716
        $this->generateHrefLangTags($controller, $request);
717
        $this->generateMetaTagHtml(
718
            $controller->pSetup['meta.'] ?? [],
719
            $controller->cObj
0 ignored issues
show
Bug introduced by
It seems like $controller->cObj can also be of type string; however, parameter $cObj of TYPO3\CMS\Frontend\Http\...::generateMetaTagHtml() does only seem to accept TYPO3\CMS\Frontend\Conte...t\ContentObjectRenderer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

719
            /** @scrutinizer ignore-type */ $controller->cObj
Loading history...
720
        );
721
722
        $controller->INTincScript_loadJSCode();
723
        $scriptJsCode = '';
724
725
        if ($controller->spamProtectEmailAddresses && $controller->spamProtectEmailAddresses !== 'ascii') {
726
            $scriptJsCode = '
727
			/* decrypt helper function */
728
		function decryptCharcode(n,start,end,offset) {
729
			n = n + offset;
730
			if (offset > 0 && n > end) {
731
				n = start + (n - end - 1);
732
			} else if (offset < 0 && n < start) {
733
				n = end - (start - n - 1);
734
			}
735
			return String.fromCharCode(n);
736
		}
737
			/* decrypt string */
738
		function decryptString(enc,offset) {
739
			var dec = "";
740
			var len = enc.length;
741
			for(var i=0; i < len; i++) {
742
				var n = enc.charCodeAt(i);
743
				if (n >= 0x2B && n <= 0x3A) {
744
					dec += decryptCharcode(n,0x2B,0x3A,offset);	/* 0-9 . , - + / : */
745
				} else if (n >= 0x40 && n <= 0x5A) {
746
					dec += decryptCharcode(n,0x40,0x5A,offset);	/* A-Z @ */
747
				} else if (n >= 0x61 && n <= 0x7A) {
748
					dec += decryptCharcode(n,0x61,0x7A,offset);	/* a-z */
749
				} else {
750
					dec += enc.charAt(i);
751
				}
752
			}
753
			return dec;
754
		}
755
			/* decrypt spam-protected emails */
756
		function linkTo_UnCryptMailto(s) {
757
			location.href = decryptString(s,' . $controller->spamProtectEmailAddresses * -1 . ');
758
		}
759
		';
760
        }
761
        // Add inline JS
762
        $inlineJS = '';
763
        // defined in php
764
        if (is_array($controller->inlineJS)) {
0 ignored issues
show
introduced by
The condition is_array($controller->inlineJS) is always false.
Loading history...
765
            foreach ($controller->inlineJS as $key => $val) {
766
                if (!is_array($val)) {
767
                    $inlineJS .= LF . $val . LF;
768
                }
769
            }
770
        }
771
        // defined in TS with page.inlineJS
772
        // Javascript inline code
773
        $inline = $controller->cObj->cObjGet($controller->pSetup['jsInline.'] ?? null, 'jsInline.');
774
        if ($inline) {
775
            $inlineJS .= LF . $inline . LF;
776
        }
777
        // Javascript inline code for Footer
778
        $inlineFooterJs = $controller->cObj->cObjGet($controller->pSetup['jsFooterInline.'] ?? null, 'jsFooterInline.');
779
        // Should minify?
780
        if ($controller->config['config']['compressJs'] ?? false) {
781
            $pageRenderer->enableCompressJavascript();
782
            $minifyErrorScript = ($minifyErrorInline = '');
783
            $scriptJsCode = GeneralUtility::minifyJavaScript($scriptJsCode, $minifyErrorScript);
784
            if ($minifyErrorScript) {
785
                $this->timeTracker->setTSlogMessage($minifyErrorScript, 3);
786
            }
787
            if ($inlineJS) {
788
                $inlineJS = GeneralUtility::minifyJavaScript($inlineJS, $minifyErrorInline);
789
                if ($minifyErrorInline) {
790
                    $this->timeTracker->setTSlogMessage($minifyErrorInline, 3);
791
                }
792
            }
793
            if ($inlineFooterJs) {
794
                $inlineFooterJs = GeneralUtility::minifyJavaScript($inlineFooterJs, $minifyErrorInline);
795
                if ($minifyErrorInline) {
796
                    $this->timeTracker->setTSlogMessage($minifyErrorInline, 3);
797
                }
798
            }
799
        }
800
        if (!isset($controller->config['config']['removeDefaultJS']) || !$controller->config['config']['removeDefaultJS']) {
801
            // include default and inlineJS
802
            if ($scriptJsCode) {
803
                $pageRenderer->addJsInlineCode('_scriptCode', $scriptJsCode, $controller->config['config']['compressJs']);
804
            }
805
            if ($inlineJS) {
806
                $pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS, $controller->config['config']['compressJs']);
807
            }
808
            if ($inlineFooterJs) {
809
                $pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs, $controller->config['config']['compressJs']);
810
            }
811
        } elseif ($controller->config['config']['removeDefaultJS'] === 'external') {
812
            /*
813
             * This keeps inlineJS from *_INT Objects from being moved to external files.
814
             * At this point in frontend rendering *_INT Objects only have placeholders instead
815
             * of actual content so moving these placeholders to external files would
816
             *     a) break the JS file (syntax errors due to the placeholders)
817
             *     b) the needed JS would never get included to the page
818
             * Therefore inlineJS from *_INT Objects must not be moved to external files but
819
             * kept internal.
820
             */
821
            $inlineJSint = '';
822
            $this->stripIntObjectPlaceholder($inlineJS, $inlineJSint);
823
            if ($inlineJSint) {
824
                $pageRenderer->addJsInlineCode('TS_inlineJSint', $inlineJSint, $controller->config['config']['compressJs']);
825
            }
826
            if (trim($scriptJsCode . $inlineJS)) {
827
                $pageRenderer->addJsFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($scriptJsCode . $inlineJS), $defaultTypeAttributeForJavaScript, $controller->config['config']['compressJs']);
828
            }
829
            if ($inlineFooterJs) {
830
                $inlineFooterJSint = '';
831
                $this->stripIntObjectPlaceholder($inlineFooterJs, $inlineFooterJSint);
832
                if ($inlineFooterJSint) {
833
                    $pageRenderer->addJsFooterInlineCode('TS_inlineFooterJSint', $inlineFooterJSint, $controller->config['config']['compressJs']);
834
                }
835
                $pageRenderer->addJsFooterFile(GeneralUtility::writeJavaScriptContentToTemporaryFile($inlineFooterJs), $defaultTypeAttributeForJavaScript, $controller->config['config']['compressJs']);
836
            }
837
        } else {
838
            // Include only inlineJS
839
            if ($inlineJS) {
840
                $pageRenderer->addJsInlineCode('TS_inlineJS', $inlineJS, $controller->config['config']['compressJs']);
841
            }
842
            if ($inlineFooterJs) {
843
                $pageRenderer->addJsFooterInlineCode('TS_inlineFooter', $inlineFooterJs, $controller->config['config']['compressJs']);
844
            }
845
        }
846
        if (isset($controller->pSetup['inlineLanguageLabelFiles.']) && is_array($controller->pSetup['inlineLanguageLabelFiles.'])) {
847
            foreach ($controller->pSetup['inlineLanguageLabelFiles.'] as $key => $languageFile) {
848
                if (is_array($languageFile)) {
849
                    continue;
850
                }
851
                $languageFileConfig = &$controller->pSetup['inlineLanguageLabelFiles.'][$key . '.'];
852
                if (isset($languageFileConfig['if.']) && !$controller->cObj->checkIf($languageFileConfig['if.'])) {
853
                    continue;
854
                }
855
                $pageRenderer->addInlineLanguageLabelFile(
856
                    $languageFile,
857
                    $languageFileConfig['selectionPrefix'] ?: '',
858
                    $languageFileConfig['stripFromSelectionName'] ?: ''
859
                );
860
            }
861
        }
862
        if (isset($controller->pSetup['inlineSettings.']) && is_array($controller->pSetup['inlineSettings.'])) {
863
            $pageRenderer->addInlineSettingArray('TS', $controller->pSetup['inlineSettings.']);
864
        }
865
        // Compression and concatenate settings
866
        if ($controller->config['config']['compressCss'] ?? false) {
867
            $pageRenderer->enableCompressCss();
868
        }
869
        if ($controller->config['config']['compressJs'] ?? false) {
870
            $pageRenderer->enableCompressJavascript();
871
        }
872
        if ($controller->config['config']['concatenateCss'] ?? false) {
873
            $pageRenderer->enableConcatenateCss();
874
        }
875
        if ($controller->config['config']['concatenateJs'] ?? false) {
876
            $pageRenderer->enableConcatenateJavascript();
877
        }
878
        // Add header data block
879
        if ($controller->additionalHeaderData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $controller->additionalHeaderData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
880
            $pageRenderer->addHeaderData(implode(LF, $controller->additionalHeaderData));
881
        }
882
        // Add footer data block
883
        if ($controller->additionalFooterData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $controller->additionalFooterData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
884
            $pageRenderer->addFooterData(implode(LF, $controller->additionalFooterData));
885
        }
886
        // Header complete, now the body tag is added so the regular content can be applied later-on
887
        if ($controller->config['config']['disableBodyTag'] ?? false) {
888
            $bodyTag = '';
889
        } else {
890
            $defBT = (isset($controller->pSetup['bodyTagCObject']) && $controller->pSetup['bodyTagCObject'])
891
                ? $controller->cObj->cObjGetSingle($controller->pSetup['bodyTagCObject'], $controller->pSetup['bodyTagCObject.'], 'bodyTagCObject')
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
892
                : '<body>';
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
893
            $bodyTag = (isset($controller->pSetup['bodyTag']) && $controller->pSetup['bodyTag'])
894
                ? $controller->pSetup['bodyTag']
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
895
                : $defBT;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
896
            if (trim($controller->pSetup['bodyTagAdd'] ?? '')) {
897
                $bodyTag = preg_replace('/>$/', '', trim($bodyTag)) . ' ' . trim($controller->pSetup['bodyTagAdd']) . '>';
898
            }
899
        }
900
        $pageRenderer->addBodyContent(LF . $bodyTag);
901
    }
902
903
    /*************************
904
     *
905
     * Helper functions
906
     *
907
     *************************/
908
909
    /**
910
     * Searches for placeholder created from *_INT cObjects, removes them from
911
     * $searchString and merges them to $intObjects
912
     *
913
     * @param string $searchString The String which should be cleaned from int-object markers
914
     * @param string $intObjects The String the found int-placeholders are moved to (for further processing)
915
     */
916
    protected function stripIntObjectPlaceholder(&$searchString, &$intObjects)
917
    {
918
        $tempArray = [];
919
        preg_match_all('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', $searchString, $tempArray);
920
        $searchString = preg_replace('/\\<\\!--INT_SCRIPT.[a-z0-9]*--\\>/', '', $searchString);
921
        $intObjects = implode('', $tempArray[0]);
922
    }
923
924
    /**
925
     * Generate meta tags from meta tag TypoScript
926
     *
927
     * @param array $metaTagTypoScript TypoScript configuration for meta tags (e.g. $GLOBALS['TSFE']->pSetup['meta.'])
928
     * @param ContentObjectRenderer $cObj
929
     */
930
    protected function generateMetaTagHtml(array $metaTagTypoScript, ContentObjectRenderer $cObj)
931
    {
932
        $pageRenderer = $this->getPageRenderer();
933
934
        $typoScriptService = GeneralUtility::makeInstance(TypoScriptService::class);
935
        $conf = $typoScriptService->convertTypoScriptArrayToPlainArray($metaTagTypoScript);
936
        foreach ($conf as $key => $properties) {
937
            $replace = false;
938
            if (is_array($properties)) {
939
                $nodeValue = $properties['_typoScriptNodeValue'] ?? '';
940
                $value = trim($cObj->stdWrap($nodeValue, $metaTagTypoScript[$key . '.']) ?? '');
941
                if ($value === '' && !empty($properties['value'])) {
942
                    $value = $properties['value'];
943
                    $replace = false;
944
                }
945
            } else {
946
                $value = $properties;
947
            }
948
949
            $attribute = 'name';
950
            if ((is_array($properties) && !empty($properties['httpEquivalent'])) || strtolower($key) === 'refresh') {
951
                $attribute = 'http-equiv';
952
            }
953
            if (is_array($properties) && !empty($properties['attribute'])) {
954
                $attribute = $properties['attribute'];
955
            }
956
            if (is_array($properties) && !empty($properties['replace'])) {
957
                $replace = true;
958
            }
959
960
            if (!is_array($value)) {
961
                $value = (array)$value;
962
            }
963
            foreach ($value as $subValue) {
964
                if (trim($subValue ?? '') !== '') {
965
                    $pageRenderer->setMetaTag($attribute, $key, $subValue, [], $replace);
966
                }
967
            }
968
        }
969
    }
970
971
    /**
972
     * @return PageRenderer
973
     */
974
    protected function getPageRenderer(): PageRenderer
975
    {
976
        return GeneralUtility::makeInstance(PageRenderer::class);
977
    }
978
979
    /**
980
     * Adds inline CSS code, by respecting the inlineStyle2TempFile option
981
     *
982
     * @param TypoScriptFrontendController $controller
983
     * @param string $cssStyles the inline CSS styling
984
     * @param bool $excludeFromConcatenation option to see if it should be concatenated
985
     * @param string $inlineBlockName the block name to add it
986
     */
987
    protected function addCssToPageRenderer(TypoScriptFrontendController $controller, string $cssStyles, bool $excludeFromConcatenation, string $inlineBlockName)
988
    {
989
        if (empty($controller->config['config']['inlineStyle2TempFile'] ?? false)) {
990
            $this->getPageRenderer()->addCssInlineBlock($inlineBlockName, $cssStyles, !empty($controller->config['config']['compressCss'] ?? false));
991
        } else {
992
            $this->getPageRenderer()->addCssFile(
993
                GeneralUtility::writeStyleSheetContentToTemporaryFile($cssStyles),
994
                'stylesheet',
995
                'all',
996
                '',
997
                (bool)($controller->config['config']['compressCss'] ?? false),
998
                false,
999
                '',
1000
                $excludeFromConcatenation
1001
            );
1002
        }
1003
    }
1004
1005
    /**
1006
     * Generates the <html> tag by evaluating TypoScript configuration, usually found via:
1007
     *
1008
     * - Adding extra attributes in addition to pre-generated ones (e.g. "dir")
1009
     *     config.htmlTag.attributes.no-js = 1
1010
     *     config.htmlTag.attributes.empty-attribute =
1011
     *
1012
     * - Adding one full string (no stdWrap!) to the "<html $htmlTagAttributes {config.htmlTag_setParams}>" tag
1013
     *     config.htmlTag_setParams = string|"none"
1014
     *
1015
     *   If config.htmlTag_setParams = none is set, even the pre-generated values are not added at all anymore.
1016
     *
1017
     * - "config.htmlTag_stdWrap" always applies over the whole compiled tag.
1018
     *
1019
     * @param array $htmlTagAttributes pre-generated attributes by doctype/direction etc. values.
1020
     * @param array $configuration the TypoScript configuration "config." array
1021
     * @param ContentObjectRenderer $cObj
1022
     * @return string the full <html> tag as string
1023
     */
1024
    protected function generateHtmlTag(array $htmlTagAttributes, array $configuration, ContentObjectRenderer $cObj): string
1025
    {
1026
        if (is_array($configuration['htmlTag.']['attributes.'] ?? null)) {
1027
            $attributeString = '';
1028
            foreach ($configuration['htmlTag.']['attributes.'] as $attributeName => $value) {
1029
                $attributeString .= ' ' . htmlspecialchars($attributeName) . ($value !== '' ? '="' . htmlspecialchars((string)$value) . '"' : '');
1030
                // If e.g. "htmlTag.attributes.dir" is set, make sure it is not added again with "implodeAttributes()"
1031
                if (isset($htmlTagAttributes[$attributeName])) {
1032
                    unset($htmlTagAttributes[$attributeName]);
1033
                }
1034
            }
1035
            $attributeString = ltrim(GeneralUtility::implodeAttributes($htmlTagAttributes) . $attributeString);
1036
        } elseif (($configuration['htmlTag_setParams'] ?? '') === 'none') {
1037
            $attributeString = '';
1038
        } elseif (isset($configuration['htmlTag_setParams'])) {
1039
            $attributeString = $configuration['htmlTag_setParams'];
1040
        } else {
1041
            $attributeString = GeneralUtility::implodeAttributes($htmlTagAttributes);
1042
        }
1043
        $htmlTag = '<html' . ($attributeString ? ' ' . $attributeString : '') . '>';
1044
        if (isset($configuration['htmlTag_stdWrap.'])) {
1045
            $htmlTag = $cObj->stdWrap($htmlTag, $configuration['htmlTag_stdWrap.']);
1046
        }
1047
        return $htmlTag;
1048
    }
1049
1050
    protected function generateHrefLangTags(TypoScriptFrontendController $controller, ServerRequestInterface $request): void
1051
    {
1052
        $hrefLangs = $this->eventDispatcher->dispatch(
1053
            new ModifyHrefLangTagsEvent($request)
1054
        )->getHrefLangs();
1055
        if (count($hrefLangs) > 1) {
1056
            $data = [];
1057
            foreach ($hrefLangs as $hrefLang => $href) {
1058
                $data[] = sprintf('<link %s/>', GeneralUtility::implodeAttributes([
1059
                    'rel' => 'alternate',
1060
                    'hreflang' => $hrefLang,
1061
                    'href' => $href,
1062
                ], true));
1063
            }
1064
            $controller->additionalHeaderData[] = implode(LF, $data);
1065
        }
1066
    }
1067
1068
    /**
1069
     * Include the preview block in case we're looking at a hidden page in the LIVE workspace
1070
     *
1071
     * @param TypoScriptFrontendController $controller
1072
     * @internal this method might get moved to a PSR-15 middleware at some point
1073
     */
1074
    protected function displayPreviewInfoMessage(TypoScriptFrontendController $controller)
1075
    {
1076
        $isInPreviewMode = $controller->getContext()->hasAspect('frontend.preview')
1077
            && $controller->getContext()->getPropertyFromAspect('frontend.preview', 'isPreview');
1078
        if (!$isInPreviewMode || $controller->doWorkspacePreview() || ($controller->config['config']['disablePreviewNotification'] ?? false)) {
1079
            return;
1080
        }
1081
        if ($controller->config['config']['message_preview']) {
1082
            $message = $controller->config['config']['message_preview'];
1083
        } else {
1084
            $label = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_tsfe.xlf:preview');
1085
            $styles = [];
1086
            $styles[] = 'position: fixed';
1087
            $styles[] = 'top: 15px';
1088
            $styles[] = 'right: 15px';
1089
            $styles[] = 'padding: 8px 18px';
1090
            $styles[] = 'background: #fff3cd';
1091
            $styles[] = 'border: 1px solid #ffeeba';
1092
            $styles[] = 'font-family: sans-serif';
1093
            $styles[] = 'font-size: 14px';
1094
            $styles[] = 'font-weight: bold';
1095
            $styles[] = 'color: #856404';
1096
            $styles[] = 'z-index: 20000';
1097
            $styles[] = 'user-select: none';
1098
            $styles[] = 'pointer-events: none';
1099
            $styles[] = 'text-align: center';
1100
            $styles[] = 'border-radius: 2px';
1101
            $message = '<div id="typo3-preview-info" style="' . implode(';', $styles) . '">' . htmlspecialchars($label) . '</div>';
1102
        }
1103
        if (!empty($message)) {
1104
            $controller->content = str_ireplace('</body>', $message . '</body>', $controller->content);
1105
        }
1106
    }
1107
1108
    /**
1109
     * @return LanguageService
1110
     */
1111
    protected function getLanguageService()
1112
    {
1113
        return $GLOBALS['LANG'];
1114
    }
1115
}
1116