Repository: contao/contao
Pull request #8190: Handle arrays when converting basic entities
Author: login: leofeyer
Labels: name: bug
Created: 2025-03-11T16:54:06Z | Merged: 2025-03-20T16:06:46Z

Fixes #8183


Linked issue #8183: Eval basicEntities does not work for serialized fields/widgets (like keyValueWizard)
### Affected version(s)

5.5.x

### Description

example: (if a add this code)
`$GLOBALS['TL_DCA']['tl_content']['fields']['data']['eval']['basicEntities'] = true;`


I will get an error:

```
ErrorException:
Warning: Array to string conversion

  at vendor/contao/core-bundle/contao/library/Contao/StringUtil.php:231
  at Contao\StringUtil::convertBasicEntities(array(array('key' => 'LOHNSCHNITT', 'value' => 'Präzise und individuell nach Deinen Vorgaben'), array('key' => 'OBST[-]BAUM[-]SCHNITT', 'value' => 'Exakte Verarbeitung für hochwertige Produkte'), array('key' => 'LAMELLEN SCHNEIDEN', 'value' => 'Perfekt für Verleimungen oder Spezialanfertigungen'), array('key' => 'INDIVIDUELLE WÜNSCHE?', 'value' => 'Kein Problem – sprich uns an!')))
     (vendor/contao/core-bundle/contao/library/Contao/Widget.php:1270)
  at Contao\Widget::getAttributesFromDca(array('inputType' => 'keyValueWizard', 'sql' => 'text NULL', 'eval' => array('basicEntities' => true, 'required' => false), 'attributes_callback' => array(array('contao.listener.data_container.description_list_label', '__invoke')), 'label' => array('Template-Daten', 'Sie können über <code>$this->keys</code> auf die Werte im Template zugreifen.')), 'data', 'a:4:{i:0;a:2:{s:3:"key";s:11:"LOHNSCHNITT";s:5:"value";s:45:"Präzise und individuell nach Deinen Vorgaben";}i:1;a:2:{s:3:"key";s:21:"OBST[-]BAUM[-]SCHNITT";s:5:"value";s:45:"Exakte Verarbeitung für hochwertige Produkte";}i:2;a:2:{s:3:"key";s:18:"LAMELLEN SCHNEIDEN";s:5:"value";s:51:"Perfekt für Verleimungen oder Spezialanfertigungen";}i:3;a:2:{s:3:"key";s:22:"INDIVIDUELLE WÜNSCHE?";s:5:"value";s:31:"Kein Problem – sprich uns an!";}}', 'data', 'tl_content', object(DC_Table))
     (vendor/contao/core-bundle/contao/classes/DataContainer.php:498)
  at Contao\DataContainer->row()
     (vendor/contao/core-bundle/contao/drivers/DC_Table.php:2169)
  at Contao\DC_Table->edit()
     (vendor/contao/core-bundle/contao/classes/Backend.php:467)
  at Contao\Backend->getBackendModule('article', null)
     (vendor/contao/core-bundle/contao/controllers/BackendMain.php:144)
  at Contao\BackendMain->run()
     (vendor/contao/core-bundle/src/Controller/BackendController.php:44)
  at Contao\CoreBundle\Controller\BackendController->mainAction()
     (vendor/symfony/http-kernel/HttpKernel.php:183)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/http-kernel/HttpKernel.php:76)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/http-kernel/Kernel.php:182)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (public/index.php:42)   
```

Diff:
diff --git a/core-bundle/contao/library/Contao/StringUtil.php b/core-bundle/contao/library/Contao/StringUtil.php
index 4fe9712a7bd..1cd9c6a0a87 100644
--- a/core-bundle/contao/library/Contao/StringUtil.php
+++ b/core-bundle/contao/library/Contao/StringUtil.php
@@ -228,7 +228,24 @@ public static function decodeEntities($strString, $strQuoteStyle=ENT_QUOTES)
 	 */
 	public static function convertBasicEntities($strBuffer)
 	{
-		return str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $strBuffer);
+		$replace = static function (&$value)
+		{
+			if (\is_string($value))
+			{
+				$value = str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $value);
+			}
+		};
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, $replace);
+		}
+		else
+		{
+			$replace($strBuffer);
+		}
+
+		return $strBuffer;
 	}
 
 	/**
@@ -240,7 +257,24 @@ public static function convertBasicEntities($strBuffer)
 	 */
 	public static function restoreBasicEntities($strBuffer)
 	{
-		return str_replace(array('[&]', '[&amp;]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), array('&amp;', '&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), $strBuffer);
+		$replace = static function (&$value)
+		{
+			if (\is_string($value))
+			{
+				$value = str_replace(array('[&]', '[&amp;]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), array('&amp;', '&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), $value);
+			}
+		};
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, $replace);
+		}
+		else
+		{
+			$replace($strBuffer);
+		}
+
+		return $strBuffer;
 	}
 
 	/**
diff --git a/core-bundle/src/Migration/Version500/AbstractBasicEntitiesMigration.php b/core-bundle/src/Migration/Version500/AbstractBasicEntitiesMigration.php
index d26279aa2e4..5059028563b 100644
--- a/core-bundle/src/Migration/Version500/AbstractBasicEntitiesMigration.php
+++ b/core-bundle/src/Migration/Version500/AbstractBasicEntitiesMigration.php
@@ -88,13 +88,10 @@ public function run(): MigrationResult
             ");
 
             foreach ($values as $id => $value) {
-                $value = StringUtil::deserialize($value);
+                $value = StringUtil::restoreBasicEntities(StringUtil::deserialize($value));
 
                 if (\is_array($value)) {
-                    array_walk_recursive($value, static fn (&$v) => $v = StringUtil::restoreBasicEntities($v));
                     $value = serialize($value);
-                } else {
-                    $value = StringUtil::restoreBasicEntities($value);
                 }
 
                 $this->connection->update($table, [$column => $value], ['id' => (int) $id]);
diff --git a/core-bundle/tests/Contao/StringUtilTest.php b/core-bundle/tests/Contao/StringUtilTest.php
index a0ad06eec55..95730cb0a70 100644
--- a/core-bundle/tests/Contao/StringUtilTest.php
+++ b/core-bundle/tests/Contao/StringUtilTest.php
@@ -626,4 +626,78 @@ public function testInsertTagToSrc(): void
             StringUtil::insertTagToSrc('Foo <img src="{{file::##simple-token##|urlattr}}" /> Bar'),
         );
     }
+
+    /**
+     * @dataProvider basicEntitiesProvider
+     */
+    public function testConvertsBasicEntities(array|string $htmlEntities, array|string $basicEntities): void
+    {
+        $this->assertSame($basicEntities, StringUtil::convertBasicEntities($htmlEntities));
+        $this->assertSame($htmlEntities, StringUtil::restoreBasicEntities($basicEntities));
+    }
+
+    public static function basicEntitiesProvider(): iterable
+    {
+        yield 'String value' => [
+            'foo&amp;bar',
+            'foo[&]bar',
+        ];
+
+        yield 'InputUnit field' => [
+            [
+                'unit' => 'h2',
+                'value' => '&lt;strong&gt;',
+            ],
+            [
+                'unit' => 'h2',
+                'value' => '[lt]strong[gt]',
+            ],
+        ];
+
+        yield 'KeyValue wizard' => [
+            [
+                [
+                    'key' => 'sum',
+                    'value' => '10&nbsp;€',
+                ],
+                [
+                    'key' => 'name',
+                    'value' => 'Con&shy;tao',
+                ],
+            ],
+            [
+                [
+                    'key' => 'sum',
+                    'value' => '10[nbsp]€',
+                ],
+                [
+                    'key' => 'name',
+                    'value' => 'Con[-]tao',
+                ],
+            ],
+        ];
+
+        yield 'Non-string values' => [
+            [
+                [
+                    'key' => 'sum',
+                    'value' => 42,
+                ],
+                [
+                    'key' => 'name',
+                    'value' => true,
+                ],
+            ],
+            [
+                [
+                    'key' => 'sum',
+                    'value' => 42,
+                ],
+                [
+                    'key' => 'name',
+                    'value' => true,
+                ],
+            ],
+        ];
+    }
 }


Reviews:
user: login: ausi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T16:05:07Z
user: login: leofeyer
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T12:21:15Z
user: login: ausi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:15:00Z
user: login: ausi
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T15:15:40Z
user: login: ausi
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-20T15:40:19Z
user: login: ausi
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T15:53:17Z

Review comments:
diff_hunk: @@ -228,7 +228,19 @@ public static function decodeEntities($strString, $strQuoteStyle=ENT_QUOTES)
 	 */
 	public static function convertBasicEntities($strBuffer)
 	{
-		return str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $strBuffer);
+		$search = array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;');
+		$replace = array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]');
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, static fn(&$value) => $value = str_replace($search, $replace, $value));
path: core-bundle/contao/library/Contao/StringUtil.php
user: login: ausi
body: ```suggestion
			array_walk_recursive($strBuffer, static fn(&$value) => $value = \is_string($value) ? str_replace($search, $replace, $value) : $value);
```
created_at: 2025-03-12T16:05:06Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,19 @@ public static function decodeEntities($strString, $strQuoteStyle=ENT_QUOTES)
 	 */
 	public static function convertBasicEntities($strBuffer)
 	{
-		return str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $strBuffer);
+		$search = array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;');
+		$replace = array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]');
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, static fn(&$value) => $value = str_replace($search, $replace, $value));
path: core-bundle/contao/library/Contao/StringUtil.php
user: login: leofeyer
body: See d42d207edc5c7ddc8011c4369afeff7c56209aa0
created_at: 2025-03-14T12:21:15Z
author_association: MEMBER
diff_hunk: @@ -228,7 +228,24 @@ public static function decodeEntities($strString, $strQuoteStyle=ENT_QUOTES)
 	 */
 	public static function convertBasicEntities($strBuffer)
 	{
-		return str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $strBuffer);
+		$replace = static function (&$value)
+		{
+			if (\is_string($value))
+			{
+				$value = str_replace(array('&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), array('[&]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), $value);
+			}
+		};
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, static fn(&$value) => $replace($value));
path: core-bundle/contao/library/Contao/StringUtil.php
user: login: ausi
body: ```suggestion
			array_walk_recursive($strBuffer, $replace);
```
created_at: 2025-03-20T15:15:00Z
author_association: MEMBER
diff_hunk: @@ -240,7 +257,24 @@ public static function convertBasicEntities($strBuffer)
 	 */
 	public static function restoreBasicEntities($strBuffer)
 	{
-		return str_replace(array('[&]', '[&amp;]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), array('&amp;', '&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), $strBuffer);
+		$replace = static function (&$value)
+		{
+			if (\is_string($value))
+			{
+				$value = str_replace(array('[&]', '[&amp;]', '[lt]', '[gt]', '[nbsp]', '[-]', '[zwsp]'), array('&amp;', '&amp;', '&lt;', '&gt;', '&nbsp;', '&shy;', '&ZeroWidthSpace;'), $value);
+			}
+		};
+
+		if (\is_array($strBuffer))
+		{
+			array_walk_recursive($strBuffer, static fn(&$value) => $replace($value));
path: core-bundle/contao/library/Contao/StringUtil.php
user: login: ausi
body: ```suggestion
			array_walk_recursive($strBuffer, $replace);
```
created_at: 2025-03-20T15:15:39Z
author_association: MEMBER

PR comments:
user: login: ausi
created_at: 2025-03-11T19:24:02Z
body: Should we support arrays in `StringUtil::restoreBasicEntities()` for consistency too?
author_association: MEMBER
user: login: leofeyer
created_at: 2025-03-12T14:46:57Z
body: Yes, we have to. The phpDoc says `string|array` there, too.
author_association: MEMBER
user: login: leofeyer
created_at: 2025-03-14T12:29:16Z
body: Might be a bit over-engineered now but it sure works 🙈 
author_association: MEMBER
user: login: aschempp
created_at: 2025-03-24T07:15:38Z
body: maybe a good time to reopen https://github.com/contao/contao/pull/7372 now? 🙄 
author_association: MEMBER
user: login: leofeyer
created_at: 2025-03-24T08:25:18Z
body: Yes, the first commit https://github.com/contao/contao/pull/7372/commits/687d71619da8478b2b391a2becb0ac04da199290 was actually correct (apart from @ausi's remarks). But the PR needs to be rebased on the `5.3` branch.
author_association: MEMBER