From a0aa6b99540e76478f7aedf63384151e96a2307c Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Mon, 3 Jun 2019 09:15:30 +0200
Subject: [PATCH] [TASK] Set locale for requests earlier in Frontend process

Due to non-site handling, setting the locale was built on top of
TypoScript settings. This was the reason, this was encapsulated
within "$TSFE->settingLocale()".

However, as the locale is now always available once SiteLanguage
has been resolved, this can be handled very early.

On top, the functionaltiy can be extracted from TSFE completely,
and be universally used within "Locales".

The method $TSFE->settingLocale() is now deprecated.

Resolves: #88473
Releases: master
Change-Id: I28c057ecc6d6ba37153a09812a61e5827cdb7bc5
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/60863
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
---
 .../core/Classes/Localization/Locales.php     | 39 +++++++++++
 ...ScriptFrontendController-settingLocale.rst | 40 ++++++++++++
 .../Tests/Unit/Localization/LocalesTest.php   | 65 +++++++++++++++++++
 .../TypoScriptFrontendController.php          | 23 ++-----
 .../PrepareTypoScriptFrontendRendering.php    |  3 +-
 .../Classes/Middleware/SiteResolver.php       |  5 ++
 .../Unit/Middleware/SiteResolverTest.php      |  2 +
 .../Php/MethodCallMatcher.php                 |  7 ++
 .../Classes/Service/RedirectService.php       |  1 -
 9 files changed, 163 insertions(+), 22 deletions(-)
 create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Deprecation-88473-TypoScriptFrontendController-settingLocale.rst

diff --git a/typo3/sysext/core/Classes/Localization/Locales.php b/typo3/sysext/core/Classes/Localization/Locales.php
index 0c8c4c913bc9..977248bb4f96 100644
--- a/typo3/sysext/core/Classes/Localization/Locales.php
+++ b/typo3/sysext/core/Classes/Localization/Locales.php
@@ -14,6 +14,8 @@ namespace TYPO3\CMS\Core\Localization;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Log\LogManager;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -256,4 +258,41 @@ class Locales implements \TYPO3\CMS\Core\SingletonInterface
         }
         return $selectedLanguage;
     }
+
+    /**
+     * Setting locale based on a SiteLanguage's defined locale.
+     * Used for frontend rendering, previously set within TSFE->settingLocale
+     *
+     * @param SiteLanguage $siteLanguage
+     * @return bool whether the locale was found on the system (and could be set properly) or not
+     */
+    public static function setSystemLocaleFromSiteLanguage(SiteLanguage $siteLanguage): bool
+    {
+        $locale = $siteLanguage->getLocale();
+        // No locale was given, so return false;
+        if (!$locale) {
+            return false;
+        }
+        $availableLocales = GeneralUtility::trimExplode(',', $locale, true);
+        // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma
+        // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC
+        // @see https://bugs.php.net/bug.php?id=53711
+        $locale = setlocale(LC_COLLATE, ...$availableLocales);
+        if ($locale) {
+            // As str_* methods are locale aware and turkish has no upper case I
+            // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE
+            // @see http://bugs.php.net/bug.php?id=35050
+            if (strpos($locale, 'tr') !== 0) {
+                setlocale(LC_CTYPE, ...$availableLocales);
+            }
+            setlocale(LC_MONETARY, ...$availableLocales);
+            setlocale(LC_TIME, ...$availableLocales);
+        } else {
+            GeneralUtility::makeInstance(LogManager::class)
+                ->getLogger(__CLASS__)
+                ->error('Locale "' . htmlspecialchars($siteLanguage->getLocale()) . '" not found.');
+            return false;
+        }
+        return true;
+    }
 }
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-88473-TypoScriptFrontendController-settingLocale.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-88473-TypoScriptFrontendController-settingLocale.rst
new file mode 100644
index 000000000000..b94c28beab2d
--- /dev/null
+++ b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-88473-TypoScriptFrontendController-settingLocale.rst
@@ -0,0 +1,40 @@
+.. include:: ../../Includes.txt
+
+=================================================================
+Deprecation: #88473 - TypoScriptFrontendController->settingLocale
+=================================================================
+
+See :issue:`88473`
+
+Description
+===========
+
+Due to Site Handling, setting the locale information (:php:`setlocale`) can be handled
+much earlier without any dependencies on the global TSFE object.
+
+The functionality of the method :php:`TypoScriptFrontendController->settingLocale()` has
+been moved into :php:`Locales::setSystemLocaleFromSiteLanguage()`. The former method
+has been marked as deprecated.
+
+
+Impact
+======
+
+Calling :php:`TypoScriptFrontendController->settingLocale()` will trigger a PHP :php:`E_USER_DEPRECATED` error.
+
+
+Affected Installations
+======================
+
+Any TYPO3 installation with a third party extension booting up a custom Frontend system and
+explicitly call the method above.
+
+
+Migration
+=========
+
+Migrate the existing PHP code to :php:`Locales::setSystemLocaleFromSiteLanguage()` or ensure
+that the SiteResolver middleware for Frontend Requests is executed where the locale is now
+set automatically.
+
+.. index:: Frontend, PHP-API, FullyScanned
\ No newline at end of file
diff --git a/typo3/sysext/core/Tests/Unit/Localization/LocalesTest.php b/typo3/sysext/core/Tests/Unit/Localization/LocalesTest.php
index 9733eabf78e2..4f044e5a4353 100644
--- a/typo3/sysext/core/Tests/Unit/Localization/LocalesTest.php
+++ b/typo3/sysext/core/Tests/Unit/Localization/LocalesTest.php
@@ -14,7 +14,9 @@ namespace TYPO3\CMS\Core\Tests\Unit\Localization;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Http\Uri;
 use TYPO3\CMS\Core\Localization\Locales;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
 /**
@@ -22,6 +24,28 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
  */
 class LocalesTest extends UnitTestCase
 {
+    protected $resetSingletonInstances = true;
+
+    /**
+     * @var string
+     */
+    protected $originalLocale;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->originalLocale = setlocale(LC_COLLATE, 0);
+    }
+
+    protected function tearDown(): void
+    {
+        // Restore original locale
+        setlocale(LC_COLLATE, $this->originalLocale);
+        setlocale(LC_MONETARY, $this->originalLocale);
+        setlocale(LC_TIME, $this->originalLocale);
+        parent::tearDown();
+    }
+
     /**
      * @return array
      */
@@ -61,4 +85,45 @@ class LocalesTest extends UnitTestCase
         );
         $this->assertSame($expected, $detectedLanguage);
     }
+
+    /**
+     * @test
+     */
+    public function setSystemLocaleFromSiteLanguageWithoutLocaleDoesNotSetLocale(): void
+    {
+        $language = new SiteLanguage(0, '', new Uri('/'), []);
+        $result = Locales::setSystemLocaleFromSiteLanguage($language);
+        static::assertFalse($result);
+        $currentLocale = setlocale(LC_COLLATE, 0);
+        // Check that the locale was not overridden
+        static::assertEquals($this->originalLocale, $currentLocale);
+    }
+
+    /**
+     * @test
+     */
+    public function setSystemLocaleFromSiteLanguageWithProperLocaleSetsLocale(): void
+    {
+        $locale = 'en_US';
+        $language = new SiteLanguage(0, $locale, new Uri('/'), []);
+        $result = Locales::setSystemLocaleFromSiteLanguage($language);
+        static::assertTrue($result);
+        $currentLocale = setlocale(LC_COLLATE, 0);
+        // Check that the locale was overridden
+        static::assertEquals($locale, $currentLocale);
+    }
+
+    /**
+     * @test
+     */
+    public function setSystemLocaleFromSiteLanguageWithInvalidLocaleDoesNotSetLocale(): void
+    {
+        $locale = 'af_EUR';
+        $language = new SiteLanguage(0, $locale, new Uri('/'), []);
+        $result = Locales::setSystemLocaleFromSiteLanguage($language);
+        static::assertFalse($result);
+        $currentLocale = setlocale(LC_COLLATE, 0);
+        // Check that the locale was not overridden
+        static::assertEquals($this->originalLocale, $currentLocale);
+    }
 }
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index 56feba241f0f..c815de099d6a 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -2079,29 +2079,14 @@ class TypoScriptFrontendController implements LoggerAwareInterface
 
     /**
      * Setting locale for frontend rendering
+     * @deprecated will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead.
      */
     public function settingLocale()
     {
+        trigger_error('TSFE->settingLocale() will be removed in TYPO3 v11.0. Use Locales::setSystemLocaleFromSiteLanguage() instead, as this functionality is independent of TSFE.', E_USER_DEPRECATED);
         $siteLanguage = $this->getCurrentSiteLanguage();
-        $locale = $siteLanguage->getLocale();
-        if ($locale) {
-            $availableLocales = GeneralUtility::trimExplode(',', $locale, true);
-            // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma
-            // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC
-            // @see https://bugs.php.net/bug.php?id=53711
-            $locale = setlocale(LC_COLLATE, ...$availableLocales);
-            if ($locale) {
-                // As str_* methods are locale aware and turkish has no upper case I
-                // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE
-                // @see http://bugs.php.net/bug.php?id=35050
-                if (strpos($locale, 'tr') !== 0) {
-                    setlocale(LC_CTYPE, ...$availableLocales);
-                }
-                setlocale(LC_MONETARY, ...$availableLocales);
-                setlocale(LC_TIME, ...$availableLocales);
-            } else {
-                $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($locale) . '" not found.', 3);
-            }
+        if ($siteLanguage->getLocale() && !Locales::setSystemLocaleFromSiteLanguage($siteLanguage)) {
+            $this->getTimeTracker()->setTSlogMessage('Locale "' . htmlspecialchars($siteLanguage->getLocale()) . '" not found.', 3);
         }
     }
 
diff --git a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
index 2e754e618278..e0f903950f99 100644
--- a/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
+++ b/typo3/sysext/frontend/Classes/Middleware/PrepareTypoScriptFrontendRendering.php
@@ -71,9 +71,8 @@ class PrepareTypoScriptFrontendRendering implements MiddlewareInterface
         $this->controller->getConfigArray();
 
         // Setting language and locale
-        $this->timeTracker->push('Setting language and locale');
+        $this->timeTracker->push('Setting language');
         $this->controller->settingLanguage();
-        $this->controller->settingLocale();
         $this->timeTracker->pull();
 
         // Convert POST data to utf-8 for internal processing if metaCharset is different
diff --git a/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php
index 8423e557af44..dc5e5de53afb 100644
--- a/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php
+++ b/typo3/sysext/frontend/Classes/Middleware/SiteResolver.php
@@ -19,8 +19,10 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Psr\Http\Server\MiddlewareInterface;
 use Psr\Http\Server\RequestHandlerInterface;
+use TYPO3\CMS\Core\Localization\Locales;
 use TYPO3\CMS\Core\Routing\SiteMatcher;
 use TYPO3\CMS\Core\Routing\SiteRouteResult;
+use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -60,6 +62,9 @@ class SiteResolver implements MiddlewareInterface
         $request = $request->withAttribute('site', $routeResult->getSite());
         $request = $request->withAttribute('language', $routeResult->getLanguage());
         $request = $request->withAttribute('routing', $routeResult);
+        if ($routeResult->getLanguage() instanceof SiteLanguage) {
+            Locales::setSystemLocaleFromSiteLanguage($routeResult->getLanguage());
+        }
         return $handler->handle($request);
     }
 }
diff --git a/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php b/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
index 68a75c986b56..5d2fe6672a7a 100644
--- a/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
+++ b/typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php
@@ -93,6 +93,8 @@ class SiteResolverTest extends UnitTestCase
     {
         // restore locale to original setting
         setlocale(LC_COLLATE, $this->originalLocale);
+        setlocale(LC_MONETARY, $this->originalLocale);
+        setlocale(LC_TIME, $this->originalLocale);
         parent::tearDown();
     }
 
diff --git a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
index 7fe89fe8a565..101b3cee10a6 100644
--- a/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
+++ b/typo3/sysext/install/Configuration/ExtensionScanner/Php/MethodCallMatcher.php
@@ -4154,4 +4154,11 @@ return [
             'Deprecation-88406-SetCacheHashnoCacheHashOptionsInViewHelpersAndUriBuilder.rst'
         ],
     ],
+    'TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController->settingLocale' => [
+        'numberOfMandatoryArguments' => 0,
+        'maximumNumberOfArguments' => 0,
+        'restFiles' => [
+            'Deprecation-88473-TypoScriptFrontendController-settingLocale.rst'
+        ],
+    ],
 ];
diff --git a/typo3/sysext/redirects/Classes/Service/RedirectService.php b/typo3/sysext/redirects/Classes/Service/RedirectService.php
index 5a7afc0164f7..47abfde6f548 100644
--- a/typo3/sysext/redirects/Classes/Service/RedirectService.php
+++ b/typo3/sysext/redirects/Classes/Service/RedirectService.php
@@ -282,7 +282,6 @@ class RedirectService implements LoggerAwareInterface
         $controller->calculateLinkVars($queryParams);
         $controller->getConfigArray();
         $controller->settingLanguage();
-        $controller->settingLocale();
         $controller->newCObj();
         return $controller;
     }
-- 
GitLab