From 88c15bc17e45674ece09dbda613d24951a8f468a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech>
Date: Mon, 12 Feb 2024 13:25:45 +0100
Subject: [PATCH] [BUGFIX] Ensure correct custom (sub)category handling in
 ConstantEditor
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The constant editor has been revamped with #98357 using the
new TypoScript parser introduced with TYPO3 v12 under the
hood.

For category and subCategory handling lower-cased keys have
been used, missing to lower-case the custom categories and
subcategories names (keys) integrator are able to add since
aeons. That leads to a failing match later on, and adding
constants to the generic `other` subcategory.

The revamped code supports only one category per subCategory
sorting value. The documentation say that this **should** be
omitted, but it worked in the past and broke with the revamped
implementation.

This change fixes muliple issues with the constant editor:

* Ensure that custom categories and subcategories are read
  lower-case, otherwise later lookup will fail and sorted
  into sub-categorie `other`.
* Add addtional array level to have `subCategory->items`
  per second sorting value as array (to avoid overriding).
* Add additional item loop to the fluid template to respect
  the additional array level.

Note: In the past it was possible to display a constant
in multiple categories/subcategories by adding them again
with different category configure comment lines. This is
not supported anymore and not considered as bug.

Resolves: #103088
Related: #98357
Related: #97816
Releases: main, 12.4
Change-Id: I4403420cc957cdef4bf077365deac0f7af621c71
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/82972
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: core-ci <typo3@b13.com>
---
 .../AST/Visitor/AstConstantCommentVisitor.php |   4 +-
 .../Controller/ConstantEditorController.php   |   2 +-
 .../Partials/ConstantEditorFields.html        | 316 +++++++++---------
 3 files changed, 162 insertions(+), 160 deletions(-)

diff --git a/typo3/sysext/core/Classes/TypoScript/AST/Visitor/AstConstantCommentVisitor.php b/typo3/sysext/core/Classes/TypoScript/AST/Visitor/AstConstantCommentVisitor.php
index 23f84d438010..0f0c9b3597eb 100644
--- a/typo3/sysext/core/Classes/TypoScript/AST/Visitor/AstConstantCommentVisitor.php
+++ b/typo3/sysext/core/Classes/TypoScript/AST/Visitor/AstConstantCommentVisitor.php
@@ -455,7 +455,7 @@ final class AstConstantCommentVisitor implements AstVisitorInterface
             ) {
                 return;
             }
-            $categoryKey = $customCategoryArray[1];
+            $categoryKey = strtolower($customCategoryArray[1]);
             $categoryLabel = $customCategoryArray[2];
             if (!isset($this->categories[$categoryKey])) {
                 $this->categories[$categoryKey] = [
@@ -474,7 +474,7 @@ final class AstConstantCommentVisitor implements AstVisitorInterface
             ) {
                 return;
             }
-            $subCategoryKey = $customSubCategoryArray[1];
+            $subCategoryKey = strtolower($customSubCategoryArray[1]);
             $subCategoryLabel = $customSubCategoryArray[2];
             if (!isset($this->subCategories[$subCategoryKey])) {
                 $this->subCategories[$subCategoryKey] = [
diff --git a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
index 7344bfaa3e4b..b19ec1faaa09 100644
--- a/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
+++ b/typo3/sysext/tstemplate/Classes/Controller/ConstantEditorController.php
@@ -172,7 +172,7 @@ class ConstantEditorController extends AbstractTemplateModuleController
         foreach ($constants as $constant) {
             if ($constant['cat'] === $selectedCategory) {
                 $displayConstants[$constant['subcat_sorting_first']]['label'] = $constant['subcat_label'];
-                $displayConstants[$constant['subcat_sorting_first']]['items'][$constant['subcat_sorting_second']] = $constant;
+                $displayConstants[$constant['subcat_sorting_first']]['items'][$constant['subcat_sorting_second']][] = $constant;
             }
         }
         ksort($displayConstants);
diff --git a/typo3/sysext/tstemplate/Resources/Private/Partials/ConstantEditorFields.html b/typo3/sysext/tstemplate/Resources/Private/Partials/ConstantEditorFields.html
index f7857337b757..77d4e4c28b2b 100644
--- a/typo3/sysext/tstemplate/Resources/Private/Partials/ConstantEditorFields.html
+++ b/typo3/sysext/tstemplate/Resources/Private/Partials/ConstantEditorFields.html
@@ -7,172 +7,174 @@
 <form action="{f:be.uri(route: 'web_typoscript_constanteditor', parameters: '{id: pageUid}')}" method="post" id="TypoScriptConstantEditorController">
     <f:for each="{displayConstants}" as="mainCategory" key="mainCategoryKey">
         <h2>{mainCategory.label}</h2>
-        <f:for each="{mainCategory.items}" as="constantItem">
-            <fieldset class="form-section">
-                <div class="form-group">
-                    <label class="form-label t3js-formengine-label">
-                        <span>{constantItem.label}</span>
-                        <code>[{constantItem.name}]</code>
-                    </label>
-                    <f:if condition="{constantItem.description}"><p>{constantItem.description}</p></f:if>
-                    <f:if condition="{constantItem.typeHint}"><span class="text-body-secondary">{constantItem.typeHint}</span></f:if>
-                    <input
-                        type="hidden"
-                        name="check[{constantItem.name}]"
-                        id="check-{constantItem.idName}"
-                        value="checked"
-                        checked
-                        {f:if(condition: '!{constantItem.isInCurrentTemplate}', then: 'disabled')}
-                    >
-                    <div class="input-group userTS" id="userTS-{constantItem.idName}" style="{f:if(condition: constantItem.isInCurrentTemplate, else: 'display:none;')}">
-                        <button
-                            type="button"
-                            class="btn btn-default t3js-toggle"
-                            data-bs-toggle="undo"
-                            rel="{constantItem.idName}"
-                            title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deleteTitle')}">
-                            <core:icon identifier="actions-edit-undo" />
-                        </button>
-                        <f:switch expression="{constantItem.type}">
-                            <f:case value="int+">
-                                <input
-                                    class="form-control"
-                                    id="{constantItem.idName}"
-                                    type="number"
-                                    name="data[{constantItem.name}]"
-                                    value="{constantItem.value}"
-                                    {f:if(condition: '{constantItem.typeIntPlusMin} || {constantItem.typeIntPlusMin == 0}', then: 'min="{constantItem.typeIntPlusMin}"')}
-                                    {f:if(condition: constantItem.typeIntPlusMax, then: 'max="{constantItem.typeIntPlusMax}"')}
-                                >
-                            </f:case>
-                            <f:case value="int">
-                                <input
-                                    class="form-control"
-                                    id="{constantItem.idName}"
-                                    type="number"
-                                    name="data[{constantItem.name}]"
-                                    value="{constantItem.value}"
-                                    {f:if(condition: '{constantItem.typeIntMin} || {constantItem.typeIntMin == 0}', then: 'min="{constantItem.typeIntMin}"')}
-                                    {f:if(condition: '{constantItem.typeIntMax} || {constantItem.typeIntMax == 0}', then: 'max="{constantItem.typeIntMax}"')}
-                                >
-                            </f:case>
-                            <f:case value="string">
-                                <input
-                                    class="form-control"
-                                    id="{constantItem.idName}"
-                                    type="text"
-                                    name="data[{constantItem.name}]"
-                                    value="{constantItem.value}"
-                                />
-                            </f:case>
-                            <f:case value="color">
-                                <input
-                                    class="form-control t3js-color-input"
-                                    type="text"
-                                    id="{constantItem.idName}"
-                                    rel="{constantItem.idName}"
-                                    name="data[{constantItem.name}]"
-                                    value="{constantItem.value}"
-                                />
-                            </f:case>
-                            <f:case value="wrap">
-                                <input
-                                    class="form-control form-control-adapt"
-                                    type="text"
-                                    id="{constantItem.idName}"
-                                    name="data[{constantItem.name}][left]"
-                                    value="{constantItem.wrapStart}"
-                                />
-                                <span class="input-group-addon input-group-icon">|</span>
-                                <input
-                                    class="form-control form-control-adapt"
-                                    type="text"
-                                    name="data[{constantItem.name}][right]"
-                                    value="{constantItem.wrapEnd}"
-                                />
-                            </f:case>
-                            <f:case value="offset">
-                                <f:for each="{constantItem.labelValueArray}" as="labelAndValue" iteration="iterator">
-                                    <span class="input-group-addon input-group-icon">{labelAndValue.label}</span>
+        <f:for each="{mainCategory.items}" as="constantItems">
+            <f:for each="{constantItems}" as="constantItem">
+                <fieldset class="form-section">
+                    <div class="form-group">
+                        <label class="form-label t3js-formengine-label">
+                            <span>{constantItem.label}</span>
+                            <code>[{constantItem.name}]</code>
+                        </label>
+                        <f:if condition="{constantItem.description}"><p>{constantItem.description}</p></f:if>
+                        <f:if condition="{constantItem.typeHint}"><span class="text-body-secondary">{constantItem.typeHint}</span></f:if>
+                        <input
+                            type="hidden"
+                            name="check[{constantItem.name}]"
+                            id="check-{constantItem.idName}"
+                            value="checked"
+                            checked
+                            {f:if(condition: '!{constantItem.isInCurrentTemplate}', then: 'disabled')}
+                        >
+                        <div class="input-group userTS" id="userTS-{constantItem.idName}" style="{f:if(condition: constantItem.isInCurrentTemplate, else: 'display:none;')}">
+                            <button
+                                type="button"
+                                class="btn btn-default t3js-toggle"
+                                data-bs-toggle="undo"
+                                rel="{constantItem.idName}"
+                                title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deleteTitle')}">
+                                <core:icon identifier="actions-edit-undo" />
+                            </button>
+                            <f:switch expression="{constantItem.type}">
+                                <f:case value="int+">
                                     <input
+                                        class="form-control"
+                                        id="{constantItem.idName}"
+                                        type="number"
+                                        name="data[{constantItem.name}]"
+                                        value="{constantItem.value}"
+                                        {f:if(condition: '{constantItem.typeIntPlusMin} || {constantItem.typeIntPlusMin == 0}', then: 'min="{constantItem.typeIntPlusMin}"')}
+                                        {f:if(condition: constantItem.typeIntPlusMax, then: 'max="{constantItem.typeIntPlusMax}"')}
+                                    >
+                                </f:case>
+                                <f:case value="int">
+                                    <input
+                                        class="form-control"
+                                        id="{constantItem.idName}"
+                                        type="number"
+                                        name="data[{constantItem.name}]"
+                                        value="{constantItem.value}"
+                                        {f:if(condition: '{constantItem.typeIntMin} || {constantItem.typeIntMin == 0}', then: 'min="{constantItem.typeIntMin}"')}
+                                        {f:if(condition: '{constantItem.typeIntMax} || {constantItem.typeIntMax == 0}', then: 'max="{constantItem.typeIntMax}"')}
+                                    >
+                                </f:case>
+                                <f:case value="string">
+                                    <input
+                                        class="form-control"
+                                        id="{constantItem.idName}"
+                                        type="text"
+                                        name="data[{constantItem.name}]"
+                                        value="{constantItem.value}"
+                                    />
+                                </f:case>
+                                <f:case value="color">
+                                    <input
+                                        class="form-control t3js-color-input"
                                         type="text"
+                                        id="{constantItem.idName}"
+                                        rel="{constantItem.idName}"
+                                        name="data[{constantItem.name}]"
+                                        value="{constantItem.value}"
+                                    />
+                                </f:case>
+                                <f:case value="wrap">
+                                    <input
                                         class="form-control form-control-adapt"
-                                        name="data[{constantItem.name}][{iterator.index}]"
-                                        value="{labelAndValue.value}"
+                                        type="text"
+                                        id="{constantItem.idName}"
+                                        name="data[{constantItem.name}][left]"
+                                        value="{constantItem.wrapStart}"
                                     />
-                                </f:for>
-                            </f:case>
-                            <f:case value="options">
-                                <select
-                                    class="form-select"
-                                    id="{constantItem.idName}"
-                                    name="data[{constantItem.name}]"
-                                >
-                                    <f:for each="{constantItem.labelValueArray}" as="labelAndValue">
-                                        <option value="{labelAndValue.value}" {f:if(condition: labelAndValue.selected, then: 'selected')}>
-                                        {labelAndValue.label}
-                                        </option>
-                                    </f:for>
-                                </select>
-                            </f:case>
-                            <f:case value="boolean">
-                                <input
-                                    type="hidden"
-                                    name="data[{constantItem.name}]"
-                                    value="0"
-                                />
-                                <div class="input-group-text">
-                                    <div class="form-check form-check-type-toggle">
+                                    <span class="input-group-addon input-group-icon">|</span>
+                                    <input
+                                        class="form-control form-control-adapt"
+                                        type="text"
+                                        name="data[{constantItem.name}][right]"
+                                        value="{constantItem.wrapEnd}"
+                                    />
+                                </f:case>
+                                <f:case value="offset">
+                                    <f:for each="{constantItem.labelValueArray}" as="labelAndValue" iteration="iterator">
+                                        <span class="input-group-addon input-group-icon">{labelAndValue.label}</span>
                                         <input
-                                            type="checkbox"
-                                            name="data[{constantItem.name}]"
-                                            id="{constantItem.idName}"
-                                            class="form-check-input"
-                                            value="{constantItem.trueValue}"
-                                            {f:if(condition: '{constantItem.value} == {constantItem.trueValue}', then: 'checked')}
+                                            type="text"
+                                            class="form-control form-control-adapt"
+                                            name="data[{constantItem.name}][{iterator.index}]"
+                                            value="{labelAndValue.value}"
                                         />
+                                    </f:for>
+                                </f:case>
+                                <f:case value="options">
+                                    <select
+                                        class="form-select"
+                                        id="{constantItem.idName}"
+                                        name="data[{constantItem.name}]"
+                                    >
+                                        <f:for each="{constantItem.labelValueArray}" as="labelAndValue">
+                                            <option value="{labelAndValue.value}" {f:if(condition: labelAndValue.selected, then: 'selected')}>
+                                            {labelAndValue.label}
+                                            </option>
+                                        </f:for>
+                                    </select>
+                                </f:case>
+                                <f:case value="boolean">
+                                    <input
+                                        type="hidden"
+                                        name="data[{constantItem.name}]"
+                                        value="0"
+                                    />
+                                    <div class="input-group-text">
+                                        <div class="form-check form-check-type-toggle">
+                                            <input
+                                                type="checkbox"
+                                                name="data[{constantItem.name}]"
+                                                id="{constantItem.idName}"
+                                                class="form-check-input"
+                                                value="{constantItem.trueValue}"
+                                                {f:if(condition: '{constantItem.value} == {constantItem.trueValue}', then: 'checked')}
+                                            />
+                                        </div>
                                     </div>
-                                </div>
-                            </f:case>
-                            <f:case value="comment">
-                                <input
-                                    type="hidden"
-                                    name="data[{constantItem.name}]"
-                                    value="0"
-                                />
-                                <div class="input-group-text">
-                                    <div class="form-check form-check-type-toggle">
-                                        <input
-                                            type="checkbox"
-                                            name="data[{constantItem.name}]"
-                                            id="{constantItem.idName}"
-                                            class="form-check-input mt-0"
-                                            value="1"
-                                            {f:if(condition: '!{constantItem.value}', then: 'checked')}
-                                        />
+                                </f:case>
+                                <f:case value="comment">
+                                    <input
+                                        type="hidden"
+                                        name="data[{constantItem.name}]"
+                                        value="0"
+                                    />
+                                    <div class="input-group-text">
+                                        <div class="form-check form-check-type-toggle">
+                                            <input
+                                                type="checkbox"
+                                                name="data[{constantItem.name}]"
+                                                id="{constantItem.idName}"
+                                                class="form-check-input mt-0"
+                                                value="1"
+                                                {f:if(condition: '!{constantItem.value}', then: 'checked')}
+                                            />
+                                        </div>
                                     </div>
-                                </div>
-                            </f:case>
-                            <f:case value="user">
-                                <input
-                                    type="hidden"
-                                    name="data[{constantItem.name}]"
-                                    value="0"
-                                />
-                                {constantItem.html -> f:format.raw()}
-                            </f:case>
-                        </f:switch>
-                    </div>
-                    <div class="input-group defaultTS" id="defaultTS-{constantItem.idName}" style="{f:if(condition: constantItem.isInCurrentTemplate, then: 'display:none;')}">
-                        <button type="button" class="btn btn-default t3js-toggle" data-bs-toggle="edit" rel="{constantItem.idName}">
-                            <span title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editTitle')}">
-                                <core:icon identifier="actions-open" />
-                            </span>
-                        </button>
-                        <input class="form-control" type="number" placeholder="{constantItem.default_value}" disabled readonly>
+                                </f:case>
+                                <f:case value="user">
+                                    <input
+                                        type="hidden"
+                                        name="data[{constantItem.name}]"
+                                        value="0"
+                                    />
+                                    {constantItem.html -> f:format.raw()}
+                                </f:case>
+                            </f:switch>
+                        </div>
+                        <div class="input-group defaultTS" id="defaultTS-{constantItem.idName}" style="{f:if(condition: constantItem.isInCurrentTemplate, then: 'display:none;')}">
+                            <button type="button" class="btn btn-default t3js-toggle" data-bs-toggle="edit" rel="{constantItem.idName}">
+                                <span title="{f:translate(key:'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editTitle')}">
+                                    <core:icon identifier="actions-open" />
+                                </span>
+                            </button>
+                            <input class="form-control" type="number" placeholder="{constantItem.default_value}" disabled readonly>
+                        </div>
                     </div>
-                </div>
-            </fieldset>
+                </fieldset>
+            </f:for>
         </f:for>
     </f:for>
 </form>
-- 
GitLab