[FEATURE] Enable/disable a specific SiteLanguage in Frontend 34/57934/5
authorBenni Mack <benni@typo3.org>
Thu, 16 Aug 2018 20:25:28 +0000 (22:25 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Fri, 17 Aug 2018 10:13:04 +0000 (12:13 +0200)
When adding a new language/translation it is quite common
to disable a language until it's "going live", so only logged-in
users can see the language variant.

A new checkbox "enabled in Frontend" is added to the site
configuration.

Resolves: #85164
Releases: master
Change-Id: Ib4265e76f3ace29c4942cd165182191042ae54a6
Reviewed-on: https://review.typo3.org/57934
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Jan Helke <typo3@helke.de>
Tested-by: Jan Helke <typo3@helke.de>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
typo3/sysext/backend/Classes/Controller/SiteConfigurationController.php
typo3/sysext/backend/Configuration/SiteConfiguration/site_language.php
typo3/sysext/backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf
typo3/sysext/core/Classes/Site/Entity/Site.php
typo3/sysext/core/Classes/Site/Entity/SiteInterface.php
typo3/sysext/core/Classes/Site/Entity/SiteLanguage.php
typo3/sysext/core/Classes/Site/SiteFinder.php
typo3/sysext/core/Documentation/Changelog/master/Feature-85164-EnableLanguagesPerSite.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/DataProcessing/LanguageMenuProcessor.php
typo3/sysext/frontend/Classes/Middleware/SiteResolver.php
typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php

index 9efb73b..5ba8531 100644 (file)
@@ -273,6 +273,8 @@ class SiteConfigurationController
                                 $childRowData[$childFieldName] = $childFieldValue;
                             } elseif ($type === 'select') {
                                 $childRowData[$childFieldName] = $childFieldValue;
+                            } elseif ($type === 'check') {
+                                $childRowData[$childFieldName] = (bool)$childFieldValue;
                             } else {
                                 throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
                             }
index f3ff5b4..248399e 100644 (file)
@@ -74,6 +74,20 @@ return [
                 'placeholder' => 'en-US',
             ],
         ],
+        'enabled' => [
+            'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_language.enabled',
+            'config' => [
+                'type' => 'check',
+                'renderType' => 'checkboxToggle',
+                'default' => 1,
+                'items' => [
+                    [
+                        0 => '',
+                        1 => ''
+                    ]
+                ]
+            ],
+        ],
         'direction' => [
             'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site_language.direction',
             'config' => [
@@ -394,7 +408,7 @@ return [
     ],
     'types' => [
         '1' => [
-            'showitem' => 'languageId, title, navigationTitle, base, locale, iso-639-1, hreflang, direction, typo3Language, flag, fallbackType, fallbacks',
+            'showitem' => 'languageId, title, navigationTitle, base, locale, iso-639-1, hreflang, direction, typo3Language, flag, fallbackType, fallbacks, enabled',
         ],
     ],
 ];
index a99d4ca..94f38a7 100644 (file)
@@ -52,6 +52,9 @@
                        <trans-unit id="site_language.hreflang">
                                <source>Language tag defined by RFC 1766 / 3066 for "lang" and "hreflang" attributes</source>
                        </trans-unit>
+                       <trans-unit id="site_language.enabled">
+                               <source>Language visible in frontend</source>
+                       </trans-unit>
                        <trans-unit id="site_language.direction">
                                <source>Language direction for "dir" attribute</source>
                        </trans-unit>
index 85b7ba4..afe1f61 100644 (file)
@@ -158,6 +158,22 @@ class Site implements SiteInterface
      */
     public function getLanguages(): array
     {
+        $languages = [];
+        foreach ($this->languages as $languageId => $language) {
+            if ($language->enabled()) {
+                $languages[$languageId] = $language;
+            }
+        }
+        return $languages;
+    }
+
+    /**
+     * Returns all available languages of this site, even the ones disabled for frontend usages
+     *
+     * @return SiteLanguage[]
+     */
+    public function getAllLanguages(): array
+    {
         return $this->languages;
     }
 
index 11a0ac9..d86cddf 100644 (file)
@@ -29,7 +29,7 @@ interface SiteInterface
     public function getRootPageId(): int;
 
     /**
-     * Returns all available languages of this site
+     * Returns all available languages of this site visible in the frontend
      *
      * @return SiteLanguage[]
      */
index 2c9ff62..318e8ce 100644 (file)
@@ -100,6 +100,11 @@ class SiteLanguage
     protected $fallbackLanguageIds = [];
 
     /**
+     * @var bool
+     */
+    protected $enabled = true;
+
+    /**
      * Additional parameters configured for this site language
      * @var array
      */
@@ -107,6 +112,7 @@ class SiteLanguage
 
     /**
      * SiteLanguage constructor.
+     *
      * @param int $languageId
      * @param string $locale
      * @param string $base
@@ -145,6 +151,9 @@ class SiteLanguage
         if (!empty($attributes['fallbacks'])) {
             $this->fallbackLanguageIds = $attributes['fallbacks'];
         }
+        if (isset($attributes['enabled'])) {
+            $this->enabled = (bool)$attributes['enabled'];
+        }
     }
 
     /**
@@ -167,6 +176,7 @@ class SiteLanguage
             'typo3Language' => $this->getTypo3Language(),
             'flagIdentifier' => $this->getFlagIdentifier(),
             'fallbackType' => $this->getFallbackType(),
+            'hidden' => $this->isEnabled(),
             'fallbackLanguageIds' => $this->getFallbackLanguageIds(),
         ];
     }
@@ -258,6 +268,16 @@ class SiteLanguage
     }
 
     /**
+     * Returns true if the language is available in frontend usage
+     *
+     * @return bool
+     */
+    public function enabled(): bool
+    {
+        return $this->enabled;
+    }
+
+    /**
      * @return string
      */
     public function getFallbackType(): string
index bd78cdb..508d065 100644 (file)
@@ -80,7 +80,7 @@ class SiteFinder
         $collection = new RouteCollection();
         $groupedRoutes = [];
         foreach ($this->sites as $site) {
-            foreach ($site->getLanguages() as $siteLanguage) {
+            foreach ($site->getAllLanguages() as $siteLanguage) {
                 $urlParts = parse_url($siteLanguage->getBase());
                 $route = new Route(
                     ($urlParts['path'] ?? '/') . '{next}',
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-85164-EnableLanguagesPerSite.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-85164-EnableLanguagesPerSite.rst
new file mode 100644 (file)
index 0000000..778a13e
--- /dev/null
@@ -0,0 +1,23 @@
+.. include:: ../../Includes.txt
+
+======================================================
+Feature: #85164 - Enable Languages on a per-site basis
+======================================================
+
+See :issue:`85164`
+
+Description
+===========
+
+When configuring a new site with multiple languages, is it now possible to not allow a language to be rendered
+in the TYPO3 Frontend. A new checkbox in the Site Handling module allows to add a language but not render it in
+Frontend to allow to prepare a new translation of a website before it is going live.
+
+
+Impact
+======
+
+Previously this wasn't as easy as doing this with one click, and took various places into account to switch
+a translation of a website "live". Going live is now as easy as turning on one checkbox.
+
+.. index:: Frontend
index 2c4a361..6f0b375 100644 (file)
@@ -444,7 +444,12 @@ class LanguageMenuProcessor implements DataProcessorInterface
         $site = $this->getCurrentSite();
 
         // Throws InvalidArgumentException in case language is not found which is fine
-        $language = $site->getLanguageById((int)$conf['language'])->toArray();
+        $language = $site->getLanguageById((int)$conf['language']);
+        if ($language->enabled()) {
+            $language = $language->toArray();
+        } else {
+            $language = null;
+        }
 
         // Check field for return exists
         if ($language !== null && !isset($language[$conf['field']])) {
index 37d8aa1..9002c0c 100644 (file)
@@ -22,6 +22,7 @@ use Psr\Http\Server\RequestHandlerInterface;
 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
 use Symfony\Component\Routing\Matcher\UrlMatcher;
 use Symfony\Component\Routing\RequestContext;
+use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
 use TYPO3\CMS\Core\Http\NormalizedParams;
@@ -29,6 +30,8 @@ use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\Controller\ErrorController;
+use TYPO3\CMS\Frontend\Page\PageAccessFailureReasons;
 
 /**
  * Identifies if a site is configured for the request, based on "id" and "L" GET/POST parameters, or the requested
@@ -76,6 +79,15 @@ class SiteResolver implements MiddlewareInterface
             try {
                 $site = $this->finder->getSiteByPageId((int)$pageId);
                 $language = $site->getLanguageById((int)$languageId);
+                // language is hidden but also not visible to the BE user, this needs to fail
+                if ($language && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
+                    $request = $request->withAttribute('site', $site);
+                    return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'Page is not available in the requested language.',
+                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
+                    );
+                }
             } catch (SiteNotFoundException $e) {
                 // No site found by ID
             }
@@ -101,6 +113,15 @@ class SiteResolver implements MiddlewareInterface
                 $result = $matcher->match($request->getUri()->getPath());
                 $site = $result['site'];
                 $language = $result['language'];
+                // language is found, and hidden but also not visible to the BE user, this needs to fail
+                if ($language && !$this->isLanguageEnabled($language, $GLOBALS['BE_USER'] ?? null)) {
+                    $request = $request->withAttribute('site', $site);
+                    return GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
+                        $request,
+                        'Page is not available in the requested language.',
+                        ['code' => PageAccessFailureReasons::LANGUAGE_NOT_AVAILABLE]
+                    );
+                }
             } catch (ResourceNotFoundException $e) {
                 // No site found
             }
@@ -200,4 +221,20 @@ class SiteResolver implements MiddlewareInterface
             ->fetch();
         return $row ? (int)$row['pid'] : null;
     }
+
+    /**
+     * Checks if the language is allowed in Frontend, if not, check if there is valid BE user
+     *
+     * @param SiteLanguage|null $language
+     * @param BackendUserAuthentication|null $user
+     * @return bool
+     */
+    protected function isLanguageEnabled(SiteLanguage $language, BackendUserAuthentication $user = null): bool
+    {
+        // language is hidden, check if a possible backend user is allowed to access the language
+        if ($language->enabled() || ($user instanceof BackendUserAuthentication && $user->checkLanguageAccess($language->getLanguageId()))) {
+            return true;
+        }
+        return false;
+    }
 }
index 5d13243..b1aaa06 100644 (file)
@@ -26,6 +26,7 @@ use TYPO3\CMS\Core\Site\Entity\Site;
 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
 use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Frontend\Middleware\SiteResolver;
+use TYPO3\CMS\Frontend\Tests\Functional\SiteHandling\Fixtures\PhpError;
 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
 use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
 
@@ -372,4 +373,48 @@ class SiteResolverTest extends UnitTestCase
             $this->assertEquals($expectedBase, $result['language-base']);
         }
     }
+
+    /**
+     * @test
+     */
+    public function checkIf404IsSiteLanguageIsDisabledInFrontend()
+    {
+        $this->siteFinder->_set('sites', [
+            'mixed-site' => new Site('mixed-site', 13, [
+                'base' => '/',
+                'errorHandling' => [
+                    [
+                        'errorCode' => 404,
+                        'errorHandler' => 'PHP',
+                        'errorPhpClassFQCN' => PhpError::class
+                    ]
+                ],
+                'languages' => [
+                    0 => [
+                        'languageId' => 0,
+                        'locale' => 'en_US.UTF-8',
+                        'base' => '/en/',
+                        'enabled' => false
+                    ],
+                    1 => [
+                        'languageId' => 1,
+                        'locale' => 'fr_CA.UTF-8',
+                        'base' => '/fr/',
+                        'enabled' => true
+                    ]
+                ]
+            ]),
+        ]);
+
+        // Reqest to default page
+        $request = new ServerRequest('https://twenty.one/en/pilots/', 'GET');
+        $subject = new SiteResolver($this->siteFinder);
+        $response = $subject->process($request, $this->siteFoundRequestHandler);
+        $this->assertEquals(404, $response->getStatusCode());
+
+        $request = new ServerRequest('https://twenty.one/fr/pilots/', 'GET');
+        $subject = new SiteResolver($this->siteFinder);
+        $response = $subject->process($request, $this->siteFoundRequestHandler);
+        $this->assertEquals(200, $response->getStatusCode());
+    }
 }