[FEATURE] Introduce fluid data-processor for language menus 97/56597/19
authorSimon Gilli <typo3@gilbertsoft.org>
Fri, 6 Apr 2018 19:00:04 +0000 (21:00 +0200)
committerBenjamin Kott <benjamin.kott@outlook.com>
Fri, 27 Apr 2018 08:46:28 +0000 (10:46 +0200)
The HMENU is extended to support the auto filling of the special.value
with all languages defined for the current site. To each menu item the
corresponding SiteLanguage is appended as array to be available at
TypoScript.

This menu processor utilizes HMENU to generate a json encoded language
menu string that will be decoded again and assigned to FLUIDTEMPLATE as
variable.

Resolves: #84650
Resolves: #84775
Releases: master
Depends: #Iabeeb6835a98c8f5a71d502379ed63a68dfad6dd
Change-Id: I5b602256962deb47e89fc190401dc0281dc5ebb0
Reviewed-on: https://review.typo3.org/56597
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com>
Tested-by: Benjamin Kott <benjamin.kott@outlook.com>
typo3/sysext/core/Documentation/Changelog/master/Feature-84650-IntroduceLanguageMenuProcessor.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-84775-ExtendHMENUForLanguageMenus.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/Menu/AbstractMenuContentObject.php
typo3/sysext/frontend/Classes/DataProcessing/LanguageMenuProcessor.php [new file with mode: 0644]

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84650-IntroduceLanguageMenuProcessor.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84650-IntroduceLanguageMenuProcessor.rst
new file mode 100644 (file)
index 0000000..f859d55
--- /dev/null
@@ -0,0 +1,59 @@
+.. include:: ../../Includes.txt
+
+===================================================================
+Feature: #84650 - Introduce fluid data processor for language menus
+===================================================================
+
+See :issue:`84650`
+
+Description
+===========
+
+This feature introduces a new `LanguageMenuProcessor` for Fluid based
+language menus based on the languages defined for the current site.
+
+Options
+-------
+
+:`if`:         TypoScript if condition
+:`languages`:  A list of comma separated language IDs (e.g. 0,1,2) to use for
+               the menu creation or `auto` to load from site languages
+:`as`:         The variable to be used within the result
+
+Example TypoScript configuration
+--------------------------------
+
+.. code-block:: typoscript
+
+   10 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
+   10 {
+      languages = auto
+      as = languageNavigation
+   }
+
+
+Example Fluid-Template
+----------------------
+
+.. code-block:: html
+
+   <f:if condition="{languageNavigation}">
+      <ul id="language" class="language-menu">
+         <f:for each="{languageNavigation}" as="item">
+            <li class="{f:if(condition: item.active, then: 'active')}{f:if(condition: item.available, else: ' text-muted')}">
+               <f:if condition="{item.available}">
+                  <f:then>
+                     <a href="{item.link}" hreflang="{item.hreflang}" title="{item.navigationTitle}">
+                        <span>{item.navigationTitle}</span>
+                     </a>
+                  </f:then>
+                  <f:else>
+                     <span>{item.navigationTitle}</span>
+                  </f:else>
+               </f:if>
+            </li>
+         </f:for>
+      </ul>
+   </f:if>
+
+.. index:: Fluid, TypoScript, Frontend
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-84775-ExtendHMENUForLanguageMenus.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-84775-ExtendHMENUForLanguageMenus.rst
new file mode 100644 (file)
index 0000000..089a473
--- /dev/null
@@ -0,0 +1,37 @@
+.. include:: ../../Includes.txt
+
+============================================================================================
+Feature: #84775 - Extend HMENU to support auto filling of special.value for special=language
+============================================================================================
+
+See :issue:`84775`
+
+Description
+===========
+
+This feature extends the `HMENU` content object to support the auto filling of
+`special.value` for language menus with the site languages available for the
+current site. Setting `special.value` to `auto` will include all available
+languages from the current site.
+
+In case of `special.value = auto` the register `languages_HMENU` will be set
+with the determined IDs for the further usage in TypoScript.
+
+Changed options
+---------------
+
+:`special.value`:  A list of comma separated language IDs (e.g. 0,1,2) or
+                   `auto` to load the list from site languages
+
+Example TypoScript configuration
+--------------------------------
+
+.. code-block:: typoscript
+
+   10 = HMENU
+   10 {
+      special = language
+      special.value = auto
+   }
+
+.. index:: Frontend, TypoScript
index ede2443..547079a 100644 (file)
@@ -17,6 +17,8 @@ namespace TYPO3\CMS\Frontend\ContentObject\Menu;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Database\ConnectionPool;
 use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\SiteFinder;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\TypoScript\TemplateService;
 use TYPO3\CMS\Core\TypoScript\TypoScriptService;
@@ -652,8 +654,21 @@ abstract class AbstractMenuContentObject
         // Getting current page record NOT overlaid by any translation:
         $tsfe = $this->getTypoScriptFrontendController();
         $currentPageWithNoOverlay = $this->sys_page->getRawRecord('pages', $tsfe->page['uid']);
-        // Traverse languages set up:
-        $languageItems = GeneralUtility::intExplode(',', $specialValue);
+
+        if ($specialValue === 'auto') {
+            $site = $this->getCurrentSite();
+            $languageItems = [];
+            $languages = $site->getLanguages();
+
+            foreach ($languages as $languageUid => $language) {
+                $languageItems[] = $languageUid;
+            }
+        } else {
+            $languageItems = GeneralUtility::intExplode(',', $specialValue);
+        }
+
+        $tsfe->register['languages_HMENU'] = implode(',', $languageItems);
+
         foreach ($languageItems as $sUid) {
             // Find overlay record:
             if ($sUid) {
@@ -2302,6 +2317,18 @@ abstract class AbstractMenuContentObject
     }
 
     /**
+     * Returns the currently configured "site" if a site is configured (= resolved) in the current request.
+     *
+     * @return Site
+     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
+     */
+    protected function getCurrentSite(): Site
+    {
+        $finder = GeneralUtility::makeInstance(SiteFinder::class);
+        return $finder->getSiteByPageId((int)$this->getTypoScriptFrontendController()->id);
+    }
+
+    /**
      * Set the parentMenuArr and key to provide the parentMenu informations to the
      * subMenu, special fur IProcFunc and itemArrayProcFunc user functions.
      *
diff --git a/typo3/sysext/frontend/Classes/DataProcessing/LanguageMenuProcessor.php b/typo3/sysext/frontend/Classes/DataProcessing/LanguageMenuProcessor.php
new file mode 100644 (file)
index 0000000..2c4a361
--- /dev/null
@@ -0,0 +1,456 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Frontend\DataProcessing;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Site\Entity\Site;
+use TYPO3\CMS\Core\Site\SiteFinder;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
+use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
+use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
+
+/**
+ * This menu processor generates a json encoded menu string that will be
+ * decoded again and assigned to FLUIDTEMPLATE as variable.
+ *
+ * Options:
+ * if        - TypoScript if condition
+ * languages - A list of languages id's (e.g. 0,1,2) to use for the menu
+ *             creation or 'auto' to load from system or site languages
+ * as        - The variable to be used within the result
+ *
+ * Example TypoScript configuration:
+ * 10 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
+ * 10 {
+ *   as = languagenavigation
+ * }
+ */
+class LanguageMenuProcessor implements DataProcessorInterface
+{
+    protected const LINK_PLACEHOLDER = '###LINKPLACEHOLDER###';
+
+    /**
+     * The content object renderer
+     *
+     * @var ContentObjectRenderer
+     */
+    public $cObj;
+
+    /**
+     * The processor configuration
+     *
+     * @var array
+     */
+    protected $processorConfiguration;
+
+    /**
+     * Allowed configuration keys for menu generation, other keys
+     * will throw an exception to prevent configuration errors.
+     *
+     * @var array
+     */
+    protected $allowedConfigurationKeys = [
+        'if',
+        'if.',
+        'languages',
+        'languages.',
+        'as'
+    ];
+
+    /**
+     * Remove keys from configuration that should not be passed
+     * to HMENU to prevent configuration errors
+     *
+     * @var array
+     */
+    protected $removeConfigurationKeysForHmenu = [
+        'languages',
+        'languages.',
+        'as'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $menuConfig = [
+        'special' => 'language',
+        'wrap' => '[|]'
+    ];
+
+    /**
+     * @var array
+     */
+    protected $menuLevelConfig = [
+        'doNotLinkIt' => '1',
+        'wrapItemAndSub' => '{|}, |*| {|}, |*| {|}',
+        'stdWrap.' => [
+            'cObject' => 'COA',
+            'cObject.' => [
+                '1' => 'LOAD_REGISTER',
+                '1.' => [
+                    'languageId.' => [
+                        'cObject' => 'TEXT',
+                        'cObject.' => [
+                            'value.' => [
+                                'data' => 'register:languages_HMENU'
+                            ],
+                            'listNum.' => [
+                                'stdWrap.' => [
+                                    'data' => 'register:count_HMENU_MENUOBJ',
+                                    'wrap' => '|-1'
+                                ],
+                                'splitChar' => ','
+                            ]
+                        ]
+                    ]
+                ],
+                '10' => 'TEXT',
+                '10.' => [
+                    'stdWrap.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'wrap' => '"languageId":|'
+                ],
+                '11' => 'USER',
+                '11.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'locale',
+                    'stdWrap.' => [
+                        'wrap' => ',"locale":|'
+                    ]
+                ],
+                '20' => 'USER',
+                '20.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'title',
+                    'stdWrap.' => [
+                        'wrap' => ',"title":|'
+                    ]
+                ],
+                '21' => 'USER',
+                '21.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'navigationTitle',
+                    'stdWrap.' => [
+                        'wrap' => ',"navigationTitle":|'
+                    ]
+                ],
+                '22' => 'USER',
+                '22.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'twoLetterIsoCode',
+                    'stdWrap.' => [
+                        'wrap' => ',"twoLetterIsoCode":|'
+                    ]
+                ],
+                '23' => 'USER',
+                '23.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'hreflang',
+                    'stdWrap.' => [
+                        'wrap' => ',"hreflang":|'
+                    ]
+                ],
+                '24' => 'USER',
+                '24.' => [
+                    'userFunc' => 'TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor->getFieldAsJson',
+                    'language.' => [
+                        'data' => 'register:languageId'
+                    ],
+                    'field' => 'direction',
+                    'stdWrap.' => [
+                        'wrap' => ',"direction":|'
+                    ]
+                ],
+                '90' => 'TEXT',
+                '90.' => [
+                    'value' => self::LINK_PLACEHOLDER,
+                    'wrap' => ',"link":|',
+                ],
+                '91' => 'TEXT',
+                '91.' => [
+                    'value' => '0',
+                    'wrap' => ',"active":|'
+                ],
+                '92' => 'TEXT',
+                '92.' => [
+                    'value' => '0',
+                    'wrap' => ',"current":|'
+                ],
+                '93' => 'TEXT',
+                '93.' => [
+                    'value' => '1',
+                    'wrap' => ',"available":|'
+                ],
+                '99' => 'RESTORE_REGISTER'
+            ]
+        ]
+    ];
+
+    /**
+     * @var array
+     */
+    protected $menuDefaults = [
+        'as' => 'languagemenu'
+    ];
+
+    /**
+     * @var string
+     */
+    protected $menuTargetVariableName;
+
+    /**
+     * @var ContentDataProcessor
+     */
+    protected $contentDataProcessor;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->contentDataProcessor = GeneralUtility::makeInstance(ContentDataProcessor::class);
+    }
+
+    /**
+     * Get configuration value from processorConfiguration
+     *
+     * @param string $key
+     * @return string
+     */
+    protected function getConfigurationValue(string $key): string
+    {
+        return $this->cObj->stdWrapValue($key, $this->processorConfiguration, $this->menuDefaults[$key]);
+    }
+
+    /**
+     * @return TypoScriptFrontendController
+     */
+    protected function getTypoScriptFrontendController(): TypoScriptFrontendController
+    {
+        return $GLOBALS['TSFE'];
+    }
+
+    /**
+     * Returns the currently configured "site" if a site is configured (= resolved) in the current request.
+     *
+     * @return Site
+     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
+     */
+    protected function getCurrentSite(): Site
+    {
+        $finder = GeneralUtility::makeInstance(SiteFinder::class);
+        return $finder->getSiteByPageId((int)$this->getTypoScriptFrontendController()->id);
+    }
+
+    /**
+     * JSON Encode
+     *
+     * @param mixed $value
+     * @return string
+     */
+    protected function jsonEncode($value): string
+    {
+        return json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE);
+    }
+
+    /**
+     * @throws \InvalidArgumentException
+     */
+    protected function validateConfiguration()
+    {
+        $invalidArguments = [];
+        foreach ($this->processorConfiguration as $key => $value) {
+            if (!in_array($key, $this->allowedConfigurationKeys)) {
+                $invalidArguments[str_replace('.', '', $key)] = $key;
+            }
+        }
+        if (!empty($invalidArguments)) {
+            throw new \InvalidArgumentException('LanguageMenuProcessor configuration contains invalid arguments: ' . implode(', ', $invalidArguments), 1522959188);
+        }
+    }
+
+    /**
+     * Process languages and filter the configuration
+     */
+    protected function prepareConfiguration(): void
+    {
+        $this->menuConfig += $this->processorConfiguration;
+
+        // Process languages
+        if (empty($this->menuConfig['languages']) && empty($this->menuConfig['languages.'])) {
+            $this->menuConfig['special.']['value'] = 'auto';
+        } elseif (!empty($this->menuConfig['languages.'])) {
+            $this->menuConfig['special.']['value'] = $this->cObj->stdWrap($this->menuConfig['languages'], $this->menuConfig['languages.']);
+        } else {
+            $this->menuConfig['special.']['value'] = $this->menuConfig['languages'];
+        }
+
+        // Filter configuration
+        foreach ($this->menuConfig as $key => $value) {
+            if (in_array($key, $this->removeConfigurationKeysForHmenu, true)) {
+                unset($this->menuConfig[$key]);
+            }
+        }
+    }
+
+    /**
+     * Build the menu configuration so it can be treated by HMENU cObject
+     */
+    protected function buildConfiguration(): void
+    {
+        $this->menuConfig['1'] = 'TMENU';
+        $this->menuConfig['1.']['IProcFunc'] = LanguageMenuProcessor::class . '->replacePlaceholderInRenderedMenuItem';
+        $this->menuConfig['1.']['NO'] = '1';
+        $this->menuConfig['1.']['NO.'] = $this->menuLevelConfig;
+        $this->menuConfig['1.']['ACT'] = $this->menuConfig['1.']['NO'];
+        $this->menuConfig['1.']['ACT.'] = $this->menuConfig['1.']['NO.'];
+        $this->menuConfig['1.']['ACT.']['stdWrap.']['cObject.']['91.']['value'] = '1';
+        $this->menuConfig['1.']['CUR'] = $this->menuConfig['1.']['ACT'];
+        $this->menuConfig['1.']['CUR.'] = $this->menuConfig['1.']['ACT.'];
+        $this->menuConfig['1.']['CUR.']['stdWrap.']['cObject.']['92.']['value'] = '1';
+        $this->menuConfig['1.']['USERDEF1'] = $this->menuConfig['1.']['NO'];
+        $this->menuConfig['1.']['USERDEF1.'] = $this->menuConfig['1.']['NO.'];
+        $this->menuConfig['1.']['USERDEF1.']['stdWrap.']['cObject.']['93.']['value'] = '0';
+        $this->menuConfig['1.']['USERDEF2'] = $this->menuConfig['1.']['ACT'];
+        $this->menuConfig['1.']['USERDEF2.'] = $this->menuConfig['1.']['ACT.'];
+        $this->menuConfig['1.']['USERDEF2.']['stdWrap.']['cObject.']['93.']['value'] = '0';
+    }
+
+    /**
+     * Validate and Build the menu configuration so it can be treated by HMENU cObject
+     */
+    protected function validateAndBuildConfiguration(): void
+    {
+        // Validate Configuration
+        $this->validateConfiguration();
+
+        // Build Configuration
+        $this->prepareConfiguration();
+        $this->buildConfiguration();
+    }
+
+    /**
+     * @param ContentObjectRenderer $cObj The data of the content element or page
+     * @param array $contentObjectConfiguration The configuration of Content Object
+     * @param array $processorConfiguration The configuration of this processor
+     * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
+     * @return array the processed data as key/value store
+     */
+    public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData): array
+    {
+        $this->cObj = $cObj;
+        $this->processorConfiguration = $processorConfiguration;
+
+        // Get Configuration
+        $this->menuTargetVariableName = $this->getConfigurationValue('as');
+
+        // Validate and Build Configuration
+        $this->validateAndBuildConfiguration();
+
+        // Process Configuration
+        $menuContentObject = $cObj->getContentObject('HMENU');
+        $renderedMenu = $menuContentObject->render($this->menuConfig);
+        if ($renderedMenu) {
+            // Process menu
+            $menu = json_decode($renderedMenu, true);
+            $processedMenu = [];
+
+            foreach ($menu as $key => $language) {
+                $processedMenu[$key] = $language;
+            }
+
+            $processedData[$this->menuTargetVariableName] = $processedMenu;
+        }
+
+        return $processedData;
+    }
+
+    /**
+     * This UserFunc gets the link and the target
+     *
+     * @param array $menuItem
+     * @return array
+     */
+    public function replacePlaceholderInRenderedMenuItem(array $menuItem): array
+    {
+        $link = $this->jsonEncode($menuItem['linkHREF']['HREF']);
+
+        $menuItem['parts']['title'] = str_replace(self::LINK_PLACEHOLDER, $link, $menuItem['parts']['title']);
+
+        return $menuItem;
+    }
+
+    /**
+     * Returns the data from the field and language submitted by $conf in JSON format
+     *
+     * @param string Empty string (no content to process)
+     * @param array TypoScript configuration
+     * @return string JSON encoded data
+     * @throws \InvalidArgumentException
+     * @throws \TYPO3\CMS\Core\Exception\SiteNotFoundException
+     */
+    public function getFieldAsJson(string $content, array $conf): string
+    {
+        // Support of stdWrap for parameters
+        if (isset($conf['language.'])) {
+            $conf['language'] = $this->cObj->stdWrap($conf['language'], $conf['language.']);
+            unset($conf['language.']);
+        }
+        if (isset($conf['field.'])) {
+            $conf['field'] = $this->cObj->stdWrap($conf['field'], $conf['field.']);
+            unset($conf['field.']);
+        }
+
+        // Check required fields
+        if ($conf['language'] === '') {
+            throw new \InvalidArgumentException('Argument \'language\' must be supplied.', 1522959186);
+        }
+        if ($conf['field'] === '') {
+            throw new \InvalidArgumentException('Argument \'field\' must be supplied.', 1522959187);
+        }
+
+        // Get and check current site
+        $site = $this->getCurrentSite();
+
+        // Throws InvalidArgumentException in case language is not found which is fine
+        $language = $site->getLanguageById((int)$conf['language'])->toArray();
+
+        // Check field for return exists
+        if ($language !== null && !isset($language[$conf['field']])) {
+            throw new \InvalidArgumentException('Invalid value \'' . $conf['field'] . '\' for argument \'field\' supplied.', 1524063160);
+        }
+
+        return $this->jsonEncode($language[$conf['field']]);
+    }
+}