From 4a18072ad0ef6d3cb25a6221a414067a16d40f90 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stefan=20B=C3=BCrk?= <stefan@buerk.tech>
Date: Mon, 22 Aug 2022 22:37:00 +0200
Subject: [PATCH] [BUGFIX] Avoid invalid property access when persisting
 extbase objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

With #95819 the detection of Domain Object properties was changed to use
ClassSchema in order to support uninitialized properties is models.
This was not possible with `DomainObjectInterface::_getProperties()`
which used `get_object_vars()` under the hood.

However, with mentioned change, other properties which had been
ignored by `get_object_vars()` were now propagated to persistence,
even if those properties were not configured in TCA. The change in
behavior is considered a regression and is now addressed:

The retrieval of the property value is delayed until after it is
ensured that the property is a persistable property (which asserts
that we are not reading from private properties).

Resolves: #98148
Related: #95819
Releases: main, 11.5
Change-Id: I94d41715a49953045d32361a00bc274c15112a1e
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/75534
Reviewed-by: Benjamin Franzke <bfr@qbus.de>
Reviewed-by: theline <typo3@root-access.de>
Reviewed-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benjamin Franzke <bfr@qbus.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: theline <typo3@root-access.de>
Tested-by: Alexander Schnitzler <git@alexanderschnitzler.de>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Stefan Bürk <stefan@buerk.tech>
---
 .../Classes/Persistence/Generic/Backend.php    | 12 +++++++++---
 .../Generic/Storage/Typo3DbBackend.php         |  3 +--
 .../Domain/Model/DateTimeImmutableExample.php  | 18 ++++++++++++++++++
 3 files changed, 28 insertions(+), 5 deletions(-)

diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Backend.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Backend.php
index 8c6348cc4eb5..1447dd9946be 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Backend.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Backend.php
@@ -310,8 +310,11 @@ class Backend implements BackendInterface, SingletonInterface
         $classSchema = $this->reflectionService->getClassSchema($className);
         foreach ($classSchema->getDomainObjectProperties() as $property) {
             $propertyName = $property->getName();
+            if (!$dataMap->isPersistableProperty($propertyName)) {
+                continue;
+            }
             $propertyValue = $object->_getProperty($propertyName);
-            if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
+            if ($this->propertyValueIsLazyLoaded($propertyValue)) {
                 continue;
             }
             $columnMap = $dataMap->getColumnMap($propertyName);
@@ -601,8 +604,11 @@ class Backend implements BackendInterface, SingletonInterface
         $classSchema = $this->reflectionService->getClassSchema($className);
         foreach ($classSchema->getDomainObjectProperties() as $property) {
             $propertyName = $property->getName();
+            if (!$dataMap->isPersistableProperty($propertyName)) {
+                continue;
+            }
             $propertyValue = $object->_getProperty($propertyName);
-            if (!$dataMap->isPersistableProperty($propertyName) || $this->propertyValueIsLazyLoaded($propertyValue)) {
+            if ($this->propertyValueIsLazyLoaded($propertyValue)) {
                 continue;
             }
             $columnMap = $dataMap->getColumnMap($propertyName);
@@ -906,11 +912,11 @@ class Backend implements BackendInterface, SingletonInterface
         $classSchema = $this->reflectionService->getClassSchema($className);
         foreach ($classSchema->getDomainObjectProperties() as $property) {
             $propertyName = $property->getName();
-            $propertyValue = $object->_getProperty($propertyName);
             $columnMap = $dataMap->getColumnMap($propertyName);
             if ($columnMap === null) {
                 continue;
             }
+            $propertyValue = $object->_getProperty($propertyName);
             if ($property->getCascadeValue() === 'remove') {
                 if ($columnMap->getTypeOfRelation() === ColumnMap::RELATION_HAS_MANY) {
                     foreach ($propertyValue as $containedObject) {
diff --git a/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php b/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
index 9ba598cd4fbd..2eed541d44e0 100644
--- a/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
+++ b/typo3/sysext/extbase/Classes/Persistence/Generic/Storage/Typo3DbBackend.php
@@ -383,10 +383,9 @@ class Typo3DbBackend implements BackendInterface, SingletonInterface
         $classSchema = $this->reflectionService->getClassSchema($className);
         foreach ($classSchema->getDomainObjectProperties() as $property) {
             $propertyName = $property->getName();
-            $propertyValue = $object->_getProperty($propertyName);
-
             // @todo We couple the Backend to the Entity implementation (uid, isClone); changes there breaks this method
             if ($dataMap->isPersistableProperty($propertyName) && $propertyName !== AbstractDomainObject::PROPERTY_UID && $propertyName !== AbstractDomainObject::PROPERTY_PID && $propertyName !== 'isClone') {
+                $propertyValue = $object->_getProperty($propertyName);
                 $fieldName = $dataMap->getColumnMap($propertyName)->getColumnName();
                 if ($propertyValue === null) {
                     $whereClause[] = $queryBuilder->expr()->isNull($fieldName);
diff --git a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php
index e1f8f7633e60..71405d4bc695 100644
--- a/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php
+++ b/typo3/sysext/extbase/Tests/Functional/Fixtures/Extensions/blog_example/Classes/Domain/Model/DateTimeImmutableExample.php
@@ -21,6 +21,24 @@ use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
 
 class DateTimeImmutableExample extends AbstractEntity
 {
+    /**
+     * Static value which is not part of an "entity".
+     * (this property has to be ignored by Extbase when persisting this entity)
+     */
+    public static string $publicStaticValue;
+
+    /**
+     * Transient value, having a name starting with `_`.
+     * (this property has to be ignored by Extbase when persisting this entity)
+     */
+    public string $_publicTransientValue;
+
+    /**
+     * Transient value without any getter or setter.
+     * (this property has to be ignored by Extbase when persisting this entity)
+     */
+    private string $privateTransientValue; // @phpstan-ignore-line since it is unused on purpose
+
     /**
      * A datetimeImmutable stored in a text field
      */
-- 
GitLab