[FEATURE] Allow to override htmlTag attributes 76/58976/3
authorBenni Mack <benni@typo3.org>
Thu, 29 Nov 2018 06:47:13 +0000 (07:47 +0100)
committerBenni Mack <benni@typo3.org>
Mon, 3 Dec 2018 19:47:30 +0000 (20:47 +0100)
The new Site handling functionality should ideally avoid
TypoScript conditions as much as possible. However,
if someone wanted to add attributes via config.htmlTag_setParams
all language-dependant properties (e.g. dir and lang) which
are generated by the site configuration are gone, leading to the
way of having to workaround this issue by using stdWrap or conditions)

In order to re-implement this functionality but also build on a
more flexible level, a new TypoScript property
"config.htmlTag.attributes." is added, which is an array
for setting attributes to the <html> tag dynamically.

This superseds the previous config.htmlTag_setParams option
by providing a more flexible API to add attributes.

`config.htmlTag.attributes.class = no-js`

It is even possible to add attributes without a value:
`config.htmlTag.attributes.amp = `

would render `<html lang="en" amp>` - keeping the existing
parameters that are set by other TypoScript anyways.

Resolves: #87033
Releases: master
Change-Id: I6a18671fffbf97aab8d3dda938dc261706e4e6fd
Reviewed-on: https://review.typo3.org/58976
Reviewed-by: Josef Glatz <josef.glatz@typo3.org>
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Benjamin Kott <benjamin.kott@outlook.com>
Tested-by: Benjamin Kott <benjamin.kott@outlook.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87033-NewTypoScriptPropertyConfightmlTagattributes.rst [new file with mode: 0644]
typo3/sysext/frontend/Classes/Http/RequestHandler.php
typo3/sysext/frontend/Classes/Page/PageGenerator.php
typo3/sysext/frontend/Tests/Unit/Http/RequestHandlerTest.php

diff --git a/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87033-NewTypoScriptPropertyConfightmlTagattributes.rst b/typo3/sysext/core/Documentation/Changelog/9.5.x/Feature-87033-NewTypoScriptPropertyConfightmlTagattributes.rst
new file mode 100644 (file)
index 0000000..04fee74
--- /dev/null
@@ -0,0 +1,44 @@
+.. include:: ../../Includes.txt
+
+===================================================================
+Feature: #87033 - New TypoScript Property config.htmlTag.attributes
+===================================================================
+
+See :issue:`87033`
+
+Description
+===========
+
+The new site handling functionality adds some attributes to the frontend rendering <html> tag automatically, even per language
+(e.g. "lang" and "dir" attributes) without having to use TypoScript anymore.
+
+However, if custom properties should be added, e.g. `<html lang="fr" amp>`, this is not possible anymore without having
+to reintroduce TypoScript conditions or complex stdWrap functionality. In previous versions the property `config.htmlTag_setParams`
+was used to override all properties, but since there were conditions in place for languages, this was a one-liner change per language.
+
+In order to use the full power of TypoScript flexibility, it is possible now to use `config.htmlTag.attributes` which
+allows to override and add custom attributes via TypoScript without having to re-add the existing attributes generated by SiteHandling.
+
+This property supersedes the previous `config.htmlTag_setParams` option by providing a more flexible API to add attributes.
+
+Examples:
+
+- :ts:`config.htmlTag.attributes.class = no-js`
+
+Will result in `<html lang="fr" class="no-js">`.
+
+It is even possible to add attributes without a value:
+
+`config.htmlTag.attributes.amp = `
+
+will render `<html lang="it" amp>`
+
+Please note that "lang" attribute in these examples are auto-generated by Site configuration, depending on the value added there.
+
+
+Impact
+======
+
+If the TypoScript option `config.htmlTag.attributes` is used, then `config.htmlTag_setParams` has no effect anymore.
+
+.. index:: TypoScript, ext:frontend, NotScanned
\ No newline at end of file
index b68fef7..86f1ffc 100644 (file)
@@ -401,15 +401,7 @@ class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterf
             $pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts));
         }
         // Begin header section:
-        if (($controller->config['config']['htmlTag_setParams'] ?? '') !== 'none') {
-            $_attr = $controller->config['config']['htmlTag_setParams'] ?? GeneralUtility::implodeAttributes($htmlTagAttributes);
-        } else {
-            $_attr = '';
-        }
-        $htmlTag = '<html' . ($_attr ? ' ' . $_attr : '') . '>';
-        if (isset($controller->config['config']['htmlTag_stdWrap.'])) {
-            $htmlTag = $controller->cObj->stdWrap($htmlTag, $controller->config['config']['htmlTag_stdWrap.']);
-        }
+        $htmlTag = $this->generateHtmlTag($htmlTagAttributes, $controller->config['config'] ?? [], $controller->cObj);
         $pageRenderer->setHtmlTag($htmlTag);
         // Head tag:
         $headTag = $controller->pSetup['headTag'] ?? '<head>';
@@ -1113,6 +1105,51 @@ class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterf
     }
 
     /**
+     * Generates the <html> tag by evaluting TypoScript configuration, usually found via:
+     *
+     * - Adding extra attributes in addition to pre-generated ones (e.g. "dir")
+     *     config.htmlTag.attributes.no-js = 1
+     *     config.htmlTag.attributes.empty-attribute =
+     *
+     * - Adding one full string (no stdWrap!) to the "<html $htmlTagAttributes {config.htmlTag_setParams}>" tag
+     *     config.htmlTag_setParams = string|"none"
+     *
+     *   If config.htmlTag_setParams = none is set, even the pre-generated values are not added at all anymore.
+     *
+     * - "config.htmlTag_stdWrap" always applies over the whole compiled tag.
+     *
+     * @param array $htmlTagAttributes pre-generated attributes by doctype/direction etc. values.
+     * @param array $configuration the TypoScript configuration "config." array
+     * @param ContentObjectRenderer $cObj
+     * @return string the full <html> tag as string
+     */
+    protected function generateHtmlTag(array $htmlTagAttributes, array $configuration, ContentObjectRenderer $cObj): string
+    {
+        if (is_array($configuration['htmlTag.']['attributes.'] ?? null)) {
+            $attributeString = '';
+            foreach ($configuration['htmlTag.']['attributes.'] as $attributeName => $value) {
+                $attributeString .= ' ' . htmlspecialchars($attributeName) . ($value !== '' ? '="' . htmlspecialchars((string)$value) . '"' : '');
+                // If e.g. "htmlTag.attributes.dir" is set, make sure it is not added again with "implodeAttributes()"
+                if (isset($htmlTagAttributes[$attributeName])) {
+                    unset($htmlTagAttributes[$attributeName]);
+                }
+            }
+            $attributeString = ltrim(GeneralUtility::implodeAttributes($htmlTagAttributes) . $attributeString);
+        } elseif (($configuration['htmlTag_setParams'] ?? '') === 'none') {
+            $attributeString = '';
+        } elseif (isset($configuration['htmlTag_setParams'])) {
+            $attributeString = $configuration['htmlTag_setParams'];
+        } else {
+            $attributeString = GeneralUtility::implodeAttributes($htmlTagAttributes);
+        }
+        $htmlTag = '<html' . ($attributeString ? ' ' . $attributeString : '') . '>';
+        if (isset($configuration['htmlTag_stdWrap.'])) {
+            $htmlTag = $cObj->stdWrap($htmlTag, $configuration['htmlTag_stdWrap.']);
+        }
+        return $htmlTag;
+    }
+
+    /**
      * This request handler can handle any frontend request.
      *
      * @param ServerRequestInterface $request
index 0a776a3..0bc5d65 100644 (file)
@@ -223,10 +223,22 @@ class PageGenerator
             $pageRenderer->setXmlPrologAndDocType(implode(LF, $docTypeParts));
         }
         // Begin header section:
-        if (($tsfe->config['config']['htmlTag_setParams'] ?? '') !== 'none') {
-            $_attr = $tsfe->config['config']['htmlTag_setParams'] ?? GeneralUtility::implodeAttributes($htmlTagAttributes);
-        } else {
+        if (is_array($tsfe->config['config']['htmlTag.']['attributes.'] ?? null)) {
             $_attr = '';
+            foreach ($tsfe->config['config']['htmlTag.']['attributes.'] as $attributeName => $value) {
+                $_attr .= ' ' . htmlspecialchars($attributeName) . ($value !== '' ? '="' . htmlspecialchars((string)$value) . '"' : '');
+                // If e.g. "htmlTag.attributes.dir" is set, make sure it is not added again with "implodeAttributes()"
+                if (isset($htmlTagAttributes[$attributeName])) {
+                    unset($htmlTagAttributes[$attributeName]);
+                }
+            }
+            $_attr = GeneralUtility::implodeAttributes($htmlTagAttributes) . $_attr;
+        } elseif (($tsfe->config['config']['htmlTag_setParams'] ?? '') === 'none') {
+            $_attr = '';
+        } elseif (isset($tsfe->config['config']['htmlTag_setParams'])) {
+            $_attr = $tsfe->config['config']['htmlTag_setParams'];
+        } else {
+            $_attr = GeneralUtility::implodeAttributes($htmlTagAttributes);
         }
         $htmlTag = '<html' . ($_attr ? ' ' . $_attr : '') . '>';
         if (isset($tsfe->config['config']['htmlTag_stdWrap.'])) {
index 19c2ad4..784d114 100644 (file)
@@ -31,6 +31,71 @@ use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
  */
 class RequestHandlerTest extends UnitTestCase
 {
+    public function generateHtmlTagIncludesAllPossibilitiesDataProvider()
+    {
+        return [
+            'no original values' => [
+                [],
+                [],
+                '<html>'
+            ],
+            'no additional values' => [
+                ['dir' => 'left'],
+                [],
+                '<html dir="left">'
+            ],
+            'no additional values #2' => [
+                ['dir' => 'left', 'xmlns:dir' => 'left'],
+                [],
+                '<html dir="left" xmlns:dir="left">'
+            ],
+            'disable all attributes' => [
+                ['dir' => 'left', 'xmlns:dir' => 'left'],
+                ['htmlTag_setParams' => 'none'],
+                '<html>'
+            ],
+            'only add setParams' => [
+                ['dir' => 'left', 'xmlns:dir' => 'left'],
+                ['htmlTag_setParams' => 'amp'],
+                '<html amp>'
+            ],
+            'attributes property trumps htmlTag_setParams' => [
+                ['dir' => 'left', 'xmlns:dir' => 'left'],
+                ['htmlTag.' => ['attributes.' => ['amp' => '']], 'htmlTag_setParams' => 'none'],
+                '<html dir="left" xmlns:dir="left" amp>'
+            ],
+            'attributes property with mixed values' => [
+                ['dir' => 'left', 'xmlns:dir' => 'left'],
+                ['htmlTag.' => ['attributes.' => ['amp' => '', 'no-js' => 'true', 'additional-enabled' => 0]]],
+                '<html dir="left" xmlns:dir="left" amp no-js="true" additional-enabled="0">'
+            ],
+            'attributes property overrides default settings' => [
+                ['dir' => 'left'],
+                ['htmlTag.' => ['attributes.' => ['amp' => '', 'dir' => 'right']]],
+                '<html amp dir="right">'
+            ],
+        ];
+    }
+
+    /**
+     * Does not test stdWrap functionality.
+     *
+     * @param $htmlTagAttributes
+     * @param $configuration
+     * @param $expectedResult
+     * @test
+     * @dataProvider generateHtmlTagIncludesAllPossibilitiesDataProvider
+     */
+    public function generateHtmlTagIncludesAllPossibilities($htmlTagAttributes, $configuration, $expectedResult)
+    {
+        $subject = $this->getAccessibleMock(RequestHandler::class, ['dummy'], [], '', false);
+        $cObj = $this->prophesize(ContentObjectRenderer::class);
+        $cObj->stdWrap(Argument::cetera())->shouldNotBeCalled();
+        $result = $subject->_call('generateHtmlTag', $htmlTagAttributes, $configuration, $cObj->reveal());
+
+        $this->assertEquals($expectedResult, $result);
+    }
+
     /**
      * @return array
      */