[BUGFIX] Do not force content_fallback to default page language
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Controller / TypoScriptFrontendController.php
index dffd47e..edba1ff 100644 (file)
@@ -15,6 +15,8 @@ namespace TYPO3\CMS\Frontend\Controller;
  */
 
 use Doctrine\DBAL\Exception\ConnectionException;
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
@@ -22,17 +24,17 @@ use TYPO3\CMS\Core\Charset\CharsetConverter;
 use TYPO3\CMS\Core\Controller\ErrorPageController;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\Query\QueryHelper;
+use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
 use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
-use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
 use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
 use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
-use TYPO3\CMS\Core\Localization\Locales;
-use TYPO3\CMS\Core\Localization\LocalizationFactory;
+use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
 use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Resource\StorageRepository;
 use TYPO3\CMS\Core\Service\DependencyOrderingService;
@@ -66,8 +68,10 @@ use TYPO3\CMS\Frontend\View\AdminPanelView;
  * The use of this class should be inspired by the order of function calls as
  * found in \TYPO3\CMS\Frontend\Http\RequestHandler.
  */
-class TypoScriptFrontendController
+class TypoScriptFrontendController implements LoggerAwareInterface
 {
+    use LoggerAwareTrait;
+
     /**
      * The page id (int)
      * @var string
@@ -172,11 +176,6 @@ class TypoScriptFrontendController
     public $MP = '';
 
     /**
-     * @var string
-     */
-    public $RDCT = '';
-
-    /**
      * This can be set from applications as a way to tag cached versions of a page
      * and later perform some external cache management, like clearing only a part
      * of the cache of a page...
@@ -392,10 +391,6 @@ class TypoScriptFrontendController
      *
      * Keys in use:
      *
-     * JSFormValidate: <script type="text/javascript" src="'.$GLOBALS["TSFE"]->absRefPrefix.'typo3/sysext/frontend/Resources/Public/JavaScript/jsfunc.validateform.js"></script>
-     * JSMenuCode, JSMenuCode_menu: JavaScript for the JavaScript menu
-     * JSCode: reserved
-     *
      * used to accumulate additional HTML-code for the header-section,
      * <head>...</head>. Insert either associative keys (like
      * additionalHeaderData['myStyleSheet'], see reserved keys above) or num-keys
@@ -561,7 +556,10 @@ class TypoScriptFrontendController
      * Site content overlay flag; If set - and sys_language_content is > 0 - ,
      * records selected will try to look for a translation pointing to their uid. (If
      * configured in [ctrl][languageField] / [ctrl][transOrigP...]
-     * @var int
+     * Possible values: [0,1,hideNonTranslated]
+     * This flag is set based on TypoScript config.sys_language_overlay setting
+     *
+     * @var int|string
      */
     public $sys_language_contentOL = 0;
 
@@ -701,23 +699,11 @@ class TypoScriptFrontendController
     public $lang = '';
 
     /**
-     * @var array
-     */
-    public $LL_labels_cache = [];
-
-    /**
-     * @var array
-     */
-    public $LL_files_cache = [];
-
-    /**
-     * List of language dependencies for actual language. This is used for local
-     * variants of a language that depend on their "main" language, like Brazilian,
-     * Portuguese or Canadian French.
+     * Internal calculations for labels
      *
-     * @var array
+     * @var LanguageService
      */
-    protected $languageDependencies = [];
+    protected $languageService;
 
     /**
      * @var LockingStrategyInterface[][]
@@ -797,10 +783,9 @@ class TypoScriptFrontendController
      * @param string $cHash The value of GeneralUtility::_GP('cHash')
      * @param string $_2 previously was used to define the jumpURL
      * @param string $MP The value of GeneralUtility::_GP('MP')
-     * @param string $RDCT The value of GeneralUtility::_GP('RDCT')
      * @see \TYPO3\CMS\Frontend\Http\RequestHandler
      */
-    public function __construct($_ = null, $id, $type, $no_cache = '', $cHash = '', $_2 = null, $MP = '', $RDCT = '')
+    public function __construct($_ = null, $id, $type, $no_cache = '', $cHash = '', $_2 = null, $MP = '')
     {
         // Setting some variables:
         $this->id = $id;
@@ -813,11 +798,11 @@ class TypoScriptFrontendController
                 $warning = '&no_cache=1 has been supplied, so caching is disabled! URL: "' . GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL') . '"';
                 $this->disableCache();
             }
-            GeneralUtility::sysLog($warning, 'cms', GeneralUtility::SYSLOG_SEVERITY_WARNING);
+            // note: we need to instantiate the logger manually here since the injection happens after the constructor
+            GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__)->warning($warning);
         }
         $this->cHash = $cHash;
         $this->MP = $GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids'] ? (string)$MP : '';
-        $this->RDCT = $RDCT;
         $this->uniqueString = md5(microtime());
         $this->initPageRenderer();
         // Call post processing function for constructor:
@@ -870,7 +855,7 @@ class TypoScriptFrontendController
             if ($this->checkPageUnavailableHandler()) {
                 $this->pageUnavailableAndExit($message);
             } else {
-                GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                $this->logger->emergency($message, ['exception' => $exception]);
                 throw new ServiceUnavailableException($message, 1301648782);
             }
         }
@@ -883,38 +868,6 @@ class TypoScriptFrontendController
         }
     }
 
-    /**
-     * Looks up the value of $this->RDCT in the database and if it is
-     * found to be associated with a redirect URL then the redirection
-     * is carried out with a 'Location:' header
-     * May exit after sending a location-header.
-     */
-    public function sendRedirect()
-    {
-        if ($this->RDCT) {
-            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-                ->getQueryBuilderForTable('cache_md5params');
-
-            $row = $queryBuilder
-                ->select('params')
-                ->from('cache_md5params')
-                ->where(
-                    $queryBuilder->expr()->eq(
-                        'md5hash',
-                        $queryBuilder->createNamedParameter($this->RDCT, \PDO::PARAM_STR)
-                    )
-                )
-                ->execute()
-                ->fetch();
-
-            if ($row) {
-                $this->updateMD5paramsRecord($this->RDCT);
-                header('Location: ' . $row['params']);
-                die;
-            }
-        }
-    }
-
     /********************************************
      *
      * Initializing, resolving page id
@@ -1015,9 +968,7 @@ class TypoScriptFrontendController
         if (!empty($gr_array) && !$this->loginAllowedInBranch_mode) {
             $this->gr_list .= ',' . implode(',', $gr_array);
         }
-        if ($this->fe_user->writeDevLog) {
-            GeneralUtility::devLog('Valid usergroups for TSFE: ' . $this->gr_list, __CLASS__);
-        }
+        $this->logger->debug('Valid usergroups for TSFE: ' . $this->gr_list);
     }
 
     /**
@@ -1291,9 +1242,63 @@ class TypoScriptFrontendController
     }
 
     /**
-     * Get The Page ID
-     * This gets the id of the page, checks if the page is in the domain and if the page is accessible
-     * Sets variables such as $this->sys_page, $this->loginUser, $this->gr_list, $this->id, $this->type, $this->domainStartPage
+     * Resolves the page id and sets up several related properties.
+     *
+     * If $this->id is not set at all or is not a plain integer, the method
+     * does it's best to set the value to an integer. Resolving is based on
+     * this options:
+     *
+     * - Splitting $this->id if it contains an additional type parameter.
+     * - Getting the id for an alias in $this->id
+     * - Finding the domain record start page
+     * - First visible page
+     * - Relocating the id below the domain record if outside
+     *
+     * The following properties may be set up or updated:
+     *
+     * - id
+     * - requestedId
+     * - type
+     * - domainStartPage
+     * - sys_page
+     * - sys_page->where_groupAccess
+     * - sys_page->where_hid_del
+     * - loginUser
+     * - gr_list
+     * - no_cache
+     * - register['SYS_LASTCHANGED']
+     * - pageNotFound
+     *
+     * Via getPageAndRootlineWithDomain()
+     *
+     * - rootLine
+     * - page
+     * - MP
+     * - originalShortcutPage
+     * - originalMountPointPage
+     * - pageAccessFailureHistory['direct_access']
+     * - pageNotFound
+     *
+     * @todo:
+     *
+     * On the first impression the method does to much. This is increased by
+     * the fact, that is is called repeated times by the method determineId.
+     * The reasons are manifold.
+     *
+     * 1.) The first part, the creation of sys_page, the type and alias
+     * resolution don't need to be repeated. They could be separated to be
+     * called only once.
+     *
+     * 2.) The user group setup could be done once on a higher level.
+     *
+     * 3.) The workflow of the resolution could be elaborated to be less
+     * tangled. Maybe the check of the page id to be below the domain via the
+     * root line doesn't need to be done each time, but for the final result
+     * only.
+     *
+     * 4.) The root line does not need to be directly addressed by this class.
+     * A root line is always related to one page. The rootline could be handled
+     * indirectly by page objects. Page objects still don't exist.
      *
      * @throws ServiceUnavailableException
      * @access private
@@ -1343,7 +1348,7 @@ class TypoScriptFrontendController
                     if ($this->checkPageUnavailableHandler()) {
                         $this->pageUnavailableAndExit($message);
                     } else {
-                        GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                        $this->logger->alert($message);
                         throw new ServiceUnavailableException($message, 1301648975);
                     }
                 }
@@ -1458,7 +1463,7 @@ class TypoScriptFrontendController
                 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
                     $this->pageNotFoundAndExit($message);
                 } else {
-                    GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                    $this->logger->error($message);
                     throw new PageNotFoundException($message, 1301648780);
                 }
             }
@@ -1469,7 +1474,7 @@ class TypoScriptFrontendController
             if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
                 $this->pageNotFoundAndExit($message);
             } else {
-                GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                $this->logger->error($message);
                 throw new PageNotFoundException($message, 1301648781);
             }
         }
@@ -1507,7 +1512,7 @@ class TypoScriptFrontendController
             if ($this->checkPageUnavailableHandler()) {
                 $this->pageUnavailableAndExit($message);
             } else {
-                GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                $this->logger->error($message);
                 throw new ServiceUnavailableException($message, 1301648167);
             }
         }
@@ -1518,7 +1523,7 @@ class TypoScriptFrontendController
                 if ($this->checkPageUnavailableHandler()) {
                     $this->pageUnavailableAndExit($message);
                 } else {
-                    GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                    $this->logger->warning($message);
                     throw new ServiceUnavailableException($message, 1301648234);
                 }
             } else {
@@ -1596,7 +1601,7 @@ class TypoScriptFrontendController
             } else {
                 $pageLog[] = $page['uid'];
                 $message = 'Page shortcuts were looping in uids ' . implode(',', $pageLog) . '...!';
-                GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                $this->logger->error($message);
                 throw new \RuntimeException($message, 1294587212);
             }
         }
@@ -1847,14 +1852,12 @@ class TypoScriptFrontendController
                 $pageUid = $this->sys_page->getDomainStartPage(implode('.', $host), GeneralUtility::getIndpEnv('SCRIPT_NAME'), GeneralUtility::getIndpEnv('REQUEST_URI'));
                 if ($pageUid) {
                     return $pageUid;
-                } else {
-                    array_shift($host);
                 }
+                array_shift($host);
             }
             return $pageUid;
-        } else {
-            return $this->sys_page->getDomainStartPage(GeneralUtility::getIndpEnv('HTTP_HOST'), GeneralUtility::getIndpEnv('SCRIPT_NAME'), GeneralUtility::getIndpEnv('REQUEST_URI'));
         }
+        return $this->sys_page->getDomainStartPage(GeneralUtility::getIndpEnv('HTTP_HOST'), GeneralUtility::getIndpEnv('SCRIPT_NAME'), GeneralUtility::getIndpEnv('REQUEST_URI'));
     }
 
     /**
@@ -2225,11 +2228,10 @@ class TypoScriptFrontendController
             if (is_array($pageSectionCacheContent)) {
                 // we have the content, nice that some other process did the work for us already
                 $this->releaseLock('pagesection');
-            } else {
-                // We keep the lock set, because we are the ones generating the page now
+            }
+            // We keep the lock set, because we are the ones generating the page now
                 // and filling the cache.
                 // This indicates that we have to release the lock in the Registry later in releaseLocks()
-            }
         }
 
         if (is_array($pageSectionCacheContent)) {
@@ -2263,11 +2265,10 @@ class TypoScriptFrontendController
                     if (is_array($row)) {
                         // we have the content, nice that some other process did the work for us
                         $this->releaseLock('pages');
-                    } else {
-                        // We keep the lock set, because we are the ones generating the page now
+                    }
+                    // We keep the lock set, because we are the ones generating the page now
                         // and filling the cache.
                         // This indicates that we have to release the lock in the Registry later in releaseLocks()
-                    }
                 }
                 if (is_array($row)) {
                     // we have data from cache
@@ -2446,7 +2447,7 @@ class TypoScriptFrontendController
                         $this->pageUnavailableAndExit($message);
                     } else {
                         $explanation = 'This means that there is no TypoScript object of type PAGE with typeNum=' . $this->type . ' configured.';
-                        GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                        $this->logger->alert($message);
                         throw new ServiceUnavailableException($message . ' ' . $explanation, 1294587217);
                     }
                 } else {
@@ -2492,7 +2493,7 @@ class TypoScriptFrontendController
                     $this->pageUnavailableAndExit('No TypoScript template found!');
                 } else {
                     $message = 'No TypoScript template found!';
-                    GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+                    $this->logger->alert($message);
                     throw new ServiceUnavailableException($message, 1294587218);
                 }
             }
@@ -2540,7 +2541,14 @@ class TypoScriptFrontendController
         }
 
         // Initialize charset settings etc.
-        $this->initLLvars();
+        $languageKey = $this->config['config']['language'] ?? 'default';
+        $this->lang = $languageKey;
+        $this->setOutputLanguage($languageKey);
+
+        // Rendering charset of HTML page.
+        if (isset($this->config['config']['metaCharset']) && $this->config['config']['metaCharset'] !== 'utf-8') {
+            $this->metaCharset = $this->config['config']['metaCharset'];
+        }
 
         // Get values from TypoScript:
         $this->sys_language_uid = ($this->sys_language_content = (int)$this->config['config']['sys_language_uid']);
@@ -2566,13 +2574,24 @@ class TypoScriptFrontendController
                                 $this->pageNotFoundAndExit('Page is not available in the requested language (strict).');
                                 break;
                             case 'content_fallback':
-                                $fallBackOrder = GeneralUtility::intExplode(',', $sys_language_content);
+                                // Setting content uid (but leaving the sys_language_uid) when a content_fallback
+                                // value was found.
+                                $fallBackOrder = GeneralUtility::trimExplode(',', $sys_language_content);
                                 foreach ($fallBackOrder as $orderValue) {
-                                    if ((string)$orderValue === '0' || !empty($this->sys_page->getPageOverlay($this->id, $orderValue))) {
-                                        $this->sys_language_content = $orderValue;
-                                        // Setting content uid (but leaving the sys_language_uid)
+                                    if ($orderValue === '0' || $orderValue === '') {
+                                        $this->sys_language_content = 0;
+                                        break;
+                                    }
+                                    if (MathUtility::canBeInterpretedAsInteger($orderValue) && !empty($this->sys_page->getPageOverlay($this->id, (int)$orderValue))) {
+                                        $this->sys_language_content = (int)$orderValue;
                                         break;
                                     }
+                                    if ($orderValue === 'pageNotFound') {
+                                        // The existing fallbacks have not been found, but instead of continuing
+                                        // page rendering with default language, a "page not found" message should be shown
+                                        // instead.
+                                        $this->pageNotFoundAndExit('Page is not available in the requested language (fallbacks did not apply).');
+                                    }
                                 }
                                 break;
                             case 'ignore':
@@ -2594,7 +2613,7 @@ class TypoScriptFrontendController
         // If default translation is not available:
         if ((!$this->sys_language_uid || !$this->sys_language_content) && GeneralUtility::hideIfDefaultLanguage($this->page['l18n_cfg'])) {
             $message = 'Page is not available in default language.';
-            GeneralUtility::sysLog($message, 'cms', GeneralUtility::SYSLOG_SEVERITY_ERROR);
+            $this->logger->error($message);
             $this->pageNotFoundAndExit($message);
         }
         $this->updateRootLinesWithTranslations();
@@ -2619,7 +2638,7 @@ class TypoScriptFrontendController
             if (!empty($this->config['config']['sys_language_isocode_default'])) {
                 $this->sys_language_isocode = $this->config['config']['sys_language_isocode_default'];
             } else {
-                $this->sys_language_isocode = $this->lang !== 'default' ? $this->lang : 'en';
+                $this->sys_language_isocode = $languageKey !== 'default' ? $languageKey : 'en';
             }
         }
 
@@ -2783,7 +2802,12 @@ class TypoScriptFrontendController
     public function calculateLinkVars()
     {
         $this->linkVars = '';
-        $linkVars = GeneralUtility::trimExplode(',', (string)$this->config['config']['linkVars']);
+        if (empty($this->config['config']['linkVars'])) {
+            return;
+        }
+
+        $linkVars = $this->splitLinkVarsString((string)$this->config['config']['linkVars']);
+
         if (empty($linkVars)) {
             return;
         }
@@ -2816,6 +2840,28 @@ class TypoScriptFrontendController
     }
 
     /**
+     * Split the link vars string by "," but not if the "," is inside of braces
+     *
+     * @param $string
+     *
+     * @return array
+     */
+    protected function splitLinkVarsString(string $string): array
+    {
+        $tempCommaReplacementString = '###KASPER###';
+
+        // replace every "," wrapped in "()" by a "unique" string
+        $string = preg_replace_callback('/\((?>[^()]|(?R))*\)/', function ($result) use ($tempCommaReplacementString) {
+            return str_replace(',', $tempCommaReplacementString, $result[0]);
+        }, $string);
+
+        $string = GeneralUtility::trimExplode(',', $string);
+
+        // replace all "unique" strings back to ","
+        return str_replace($tempCommaReplacementString, ',', $string);
+    }
+
+    /**
      * Redirect to target page if the current page is an overlaid mountpoint.
      *
      * If the current page is of type mountpoint and should be overlaid with the contents of the mountpoint page
@@ -3480,17 +3526,7 @@ class TypoScriptFrontendController
             $this->sendCacheHeaders();
         }
         // Set headers, if any
-        if (is_array($this->config['config']['additionalHeaders.'])) {
-            ksort($this->config['config']['additionalHeaders.']);
-            foreach ($this->config['config']['additionalHeaders.'] as $options) {
-                header(
-                    trim($options['header']),
-                    // "replace existing headers" is turned on by default, unless turned off
-                    ($options['replace'] !== '0'),
-                    ((int)$options['httpResponseCode'] ?: null)
-                );
-            }
-        }
+        $this->sendAdditionalHeaders();
         // Send appropriate status code in case of temporary content
         if ($this->tempContent) {
             $this->addTempContentHttpHeaders();
@@ -3506,17 +3542,6 @@ class TypoScriptFrontendController
                 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
             }
         }
-        // Send content-length header.
-        // Notice that all HTML content outside the length of the content-length header will be cut off!
-        // Therefore content of unknown length from included PHP-scripts and if admin users are logged
-        // in (admin panel might show...) or if debug mode is turned on, we disable it!
-        if (
-            (!isset($this->config['config']['enableContentLengthHeader']) || $this->config['config']['enableContentLengthHeader'])
-            && !$this->beUserLogin && !$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
-            && !$this->config['config']['debug'] && !$this->doWorkspacePreview()
-        ) {
-            header('Content-Length: ' . strlen($this->content));
-        }
     }
 
     /**
@@ -3771,28 +3796,7 @@ class TypoScriptFrontendController
     {
         $explanationText = $explanation !== '' ? ' - ' . $explanation : '';
         $this->getTimeTracker()->setTSlogMessage($typoScriptProperty . ' is deprecated.' . $explanationText, 2);
-        GeneralUtility::deprecationLog('TypoScript ' . $typoScriptProperty . ' is deprecated' . $explanationText);
-    }
-
-    /**
-     * Updates the tstamp field of a cache_md5params record to the current time.
-     *
-     * @param string $hash The hash string identifying the cache_md5params record for which to update the "tstamp" field to the current time.
-     * @access private
-     */
-    public function updateMD5paramsRecord($hash)
-    {
-        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getConnectionForTable('cache_md5params');
-        $connection->update(
-            'cache_md5params',
-            [
-                'tstamp' => $GLOBALS['EXEC_TIME']
-            ],
-            [
-                'md5hash' => $hash
-            ]
-        );
+        trigger_error('TypoScript property ' . $typoScriptProperty . ' is deprecated' . $explanationText, E_USER_DEPRECATED);
     }
 
     /********************************************
@@ -3929,17 +3933,11 @@ class TypoScriptFrontendController
     /**
      * Sets the cache-flag to 1. Could be called from user-included php-files in order to ensure that a page is not cached.
      *
-     * @param string $reason An optional reason to be written to the syslog.
+     * @param string $reason An optional reason to be written to the log.
      * @param bool $internal Whether the call is done from core itself (should only be used by core).
      */
     public function set_no_cache($reason = '', $internal = false)
     {
-        if ($internal && isset($GLOBALS['BE_USER'])) {
-            $severity = GeneralUtility::SYSLOG_SEVERITY_NOTICE;
-        } else {
-            $severity = GeneralUtility::SYSLOG_SEVERITY_WARNING;
-        }
-
         if ($reason !== '') {
             $warning = '$TSFE->set_no_cache() was triggered. Reason: ' . $reason . '.';
         } else {
@@ -3963,7 +3961,11 @@ class TypoScriptFrontendController
             $warning .= ' Caching is disabled!';
             $this->disableCache();
         }
-        GeneralUtility::sysLog($warning, 'cms', $severity);
+        if ($internal && isset($GLOBALS['BE_USER'])) {
+            $this->logger->notice($warning);
+        } else {
+            $this->logger->warning($warning);
+        }
     }
 
     /**
@@ -4067,27 +4069,7 @@ class TypoScriptFrontendController
      */
     public function sL($input)
     {
-        if (substr($input, 0, 4) !== 'LLL:') {
-            // Not a label, return the key as this
-            return $input;
-        }
-        // If cached label
-        if (!isset($this->LL_labels_cache[$this->lang][$input])) {
-            $restStr = trim(substr($input, 4));
-            $extPrfx = '';
-            if (strpos($restStr, 'EXT:') === 0) {
-                $restStr = trim(substr($restStr, 4));
-                $extPrfx = 'EXT:';
-            }
-            $parts = explode(':', $restStr);
-            $parts[0] = $extPrfx . $parts[0];
-            // Getting data if not cached
-            if (!isset($this->LL_files_cache[$parts[0]])) {
-                $this->LL_files_cache[$parts[0]] = $this->readLLfile($parts[0]);
-            }
-            $this->LL_labels_cache[$this->lang][$input] = $this->getLLL($parts[1], $this->LL_files_cache[$parts[0]]);
-        }
-        return $this->LL_labels_cache[$this->lang][$input];
+        return $this->languageService->sL($input);
     }
 
     /**
@@ -4095,37 +4077,12 @@ class TypoScriptFrontendController
      *
      * @param string $fileRef Reference to a relative filename to include.
      * @return array Returns the $LOCAL_LANG array found in the file. If no array found, returns empty array.
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10
      */
     public function readLLfile($fileRef)
     {
-        /** @var $languageFactory LocalizationFactory */
-        $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
-
-        if ($this->lang !== 'default') {
-            $languages = array_reverse($this->languageDependencies);
-            // At least we need to have English
-            if (empty($languages)) {
-                $languages[] = 'default';
-            }
-        } else {
-            $languages = ['default'];
-        }
-
-        $localLanguage = [];
-        foreach ($languages as $language) {
-            $tempLL = $languageFactory->getParsedData($fileRef, $language);
-            $localLanguage['default'] = $tempLL['default'];
-            if (!isset($localLanguage[$this->lang])) {
-                $localLanguage[$this->lang] = $localLanguage['default'];
-            }
-            if ($this->lang !== 'default' && isset($tempLL[$language])) {
-                // Merge current language labels onto labels from previous language
-                // This way we have a label with fall back applied
-                ArrayUtility::mergeRecursiveWithOverrule($localLanguage[$this->lang], $tempLL[$language], true, false);
-            }
-        }
-
-        return $localLanguage;
+        trigger_error('This method will be removed in TYPO3 v10, as the method LanguageService->includeLLFile() can be used directly.', E_USER_DEPRECATED);
+        return $this->languageService->includeLLFile($fileRef, false, true);
     }
 
     /**
@@ -4134,12 +4091,15 @@ class TypoScriptFrontendController
      * @param string $index Local_lang key for which to return label (language is determined by $this->lang)
      * @param array $LOCAL_LANG The locallang array in which to search
      * @return string Label value of $index key.
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10, use LanguageService->getLLL() directly
      */
     public function getLLL($index, $LOCAL_LANG)
     {
+        trigger_error('This method will be removed in TYPO3 v10, as the method LanguageService->getLLL() can be used directly.', E_USER_DEPRECATED);
         if (isset($LOCAL_LANG[$this->lang][$index][0]['target'])) {
             return $LOCAL_LANG[$this->lang][$index][0]['target'];
-        } elseif (isset($LOCAL_LANG['default'][$index][0]['target'])) {
+        }
+        if (isset($LOCAL_LANG['default'][$index][0]['target'])) {
             return $LOCAL_LANG['default'][$index][0]['target'];
         }
         return false;
@@ -4147,36 +4107,38 @@ class TypoScriptFrontendController
 
     /**
      * Initializing the getLL variables needed.
+     *
+     * @see settingLanguage()
+     * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.
      */
     public function initLLvars()
     {
-        // Init languageDependencies list
-        $this->languageDependencies = [];
-        // Setting language key and split index:
+        trigger_error('This method will be removed in TYPO3 v10, the initialization can be altered via hooks within settingLanguage().', E_USER_DEPRECATED);
         $this->lang = $this->config['config']['language'] ?: 'default';
-        $this->pageRenderer->setLanguage($this->lang);
-
-        // Finding the requested language in this list based
-        // on the $lang key being inputted to this function.
-        /** @var $locales Locales */
-        $locales = GeneralUtility::makeInstance(Locales::class);
-        $locales->initialize();
-
-        // Language is found. Configure it:
-        if (in_array($this->lang, $locales->getLocales())) {
-            $this->languageDependencies[] = $this->lang;
-            foreach ($locales->getLocaleDependencies($this->lang) as $language) {
-                $this->languageDependencies[] = $language;
-            }
-        }
+        $this->setOutputLanguage($this->lang);
 
         // Rendering charset of HTML page.
         if ($this->config['config']['metaCharset']) {
-            $this->metaCharset = $this->config['config']['metaCharset'];
+            $this->metaCharset = trim(strtolower($this->config['config']['metaCharset']));
         }
     }
 
     /**
+     * Sets all internal measures what language the page should be rendered.
+     * This is not for records, but rather the HTML / charset and the locallang labels
+     *
+     * @param string $language - usually set via TypoScript config.language = dk
+     */
+    protected function setOutputLanguage($language = 'default')
+    {
+        $this->pageRenderer->setLanguage($language);
+        $this->languageService = GeneralUtility::makeInstance(LanguageService::class);
+        // Always disable debugging for TSFE
+        $this->languageService->debugKey = false;
+        $this->languageService->init($language);
+    }
+
+    /**
      * Converts input string from utf-8 to metaCharset IF the two charsets are different.
      *
      * @param string $content Content to be converted.
@@ -4364,7 +4326,7 @@ class TypoScriptFrontendController
             $sysDomainData = $runtimeCache->get($entryIdentifier);
         } else {
             $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_domain');
-            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
+            $queryBuilder->setRestrictions(GeneralUtility::makeInstance(DefaultRestrictionContainer::class));
             $result = $queryBuilder
                 ->select('uid', 'pid', 'domainName', 'forced')
                 ->from('sys_domain')
@@ -4514,9 +4476,8 @@ class TypoScriptFrontendController
             $this->locks[$type]['accessLock']->release();
             if ($locked) {
                 break;
-            } else {
-                throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
             }
+            throw new \RuntimeException('Could not acquire page lock for ' . $key . '.', 1460975877);
         } while (true);
     }
 
@@ -4545,6 +4506,47 @@ class TypoScriptFrontendController
     }
 
     /**
+     * Send additional headers from config.additionalHeaders
+     *
+     * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::processOutput()
+     */
+    protected function sendAdditionalHeaders()
+    {
+        if (!isset($this->config['config']['additionalHeaders.'])) {
+            return;
+        }
+        ksort($this->config['config']['additionalHeaders.']);
+        foreach ($this->config['config']['additionalHeaders.'] as $options) {
+            if (!is_array($options)) {
+                continue;
+            }
+            $header = $options['header'] ?? '';
+            $header = isset($options['header.'])
+                ? $this->cObj->stdWrap(trim($header), $options['header.'])
+                : trim($header);
+            if ($header === '') {
+                continue;
+            }
+            $replace = $options['replace'] ?? '';
+            $replace = isset($options['replace.'])
+                ? $this->cObj->stdWrap($replace, $options['replace.'])
+                : $replace;
+            $httpResponseCode = $options['httpResponseCode'] ?? '';
+            $httpResponseCode = isset($options['httpResponseCode.'])
+                ? $this->cObj->stdWrap($httpResponseCode, $options['httpResponseCode.'])
+                : $httpResponseCode;
+            $httpResponseCode = (int)$httpResponseCode;
+
+            header(
+                $header,
+                // "replace existing headers" is turned on by default, unless turned off
+                $replace !== '0',
+                $httpResponseCode ?: null
+            );
+        }
+    }
+
+    /**
      * Returns the current BE user.
      *
      * @return \TYPO3\CMS\Backend\FrontendBackendUserAuthentication