Repository: pimcore/pimcore (language: php)

Issue:
[Data Objects] [PX] Links in Blocks will not be cleaned up when Target Document is deleted
### Pimcore version

10.x

### Steps to reproduce

This is exakt the same issue as described here: https://github.com/pimcore/pimcore/issues/7369 but on PX (can be reproduced on demo.pimcore.fun):

- create an object with a link element
- add document to link
- save object
- delete linked document
- save object again:

![image](https://user-images.githubusercontent.com/700119/237030110-afcee477-36ee-4178-a009-759859c4a08f.png)


### Expected / actual behavior

Omit exception in link element if document / asset has been deleted


Discussion / hints:

Fix patch:
diff --git a/models/DataObject/ClassDefinition/Data/Block.php b/models/DataObject/ClassDefinition/Data/Block.php
index 276457fc63a..d6260ab17bf 100644
--- a/models/DataObject/ClassDefinition/Data/Block.php
+++ b/models/DataObject/ClassDefinition/Data/Block.php
@@ -1125,6 +1125,9 @@ public function checkValidity($data, $omitMandatoryCheck = false, $params = [])
                                 $fd->performMultipleAssignmentCheck($data);
                             }
 
+                            if ($fd instanceof Link) {
+                                $params['resetInvalidFields'] = true;
+                            }
                             $fd->checkValidity($data, false, $params);
                         } catch (Model\Element\ValidationException $ve) {
                             $ve->addContext($this->getName() . '-' . $idx);
diff --git a/models/DataObject/ClassDefinition/Data/Link.php b/models/DataObject/ClassDefinition/Data/Link.php
index d8efc035a25..80821b36ef3 100644
--- a/models/DataObject/ClassDefinition/Data/Link.php
+++ b/models/DataObject/ClassDefinition/Data/Link.php
@@ -82,12 +82,8 @@ public function getDataForResource($data, $object = null, $params = [])
                 }
             }
 
-            try {
-                $this->checkValidity($data, true, $params);
-            } catch (\Exception $e) {
-                $data->setInternalType(null);
-                $data->setInternal(null);
-            }
+            $params['resetInvalidFields'] = true;
+            $this->checkValidity($data, true, $params);
 
             return Serialize::serialize($data);
         }
@@ -115,12 +111,8 @@ public function getDataFromResource($data, $object = null, $params = [])
                 $link->_setOwnerLanguage($params['language'] ?? null);
             }
 
-            try {
-                $this->checkValidity($link, true, $params);
-            } catch (\Exception) {
-                $link->setInternalType(null);
-                $link->setInternal(null);
-            }
+            $params['resetInvalidFields'] = true;
+            $this->checkValidity($link, true, $params);
 
             return $link;
         }
@@ -231,12 +223,22 @@ public function checkValidity($data, $omitMandatoryCheck = false, $params = [])
                 if ($data->getInternalType() == 'document') {
                     $doc = Document::getById($data->getInternal());
                     if (!$doc instanceof Document) {
-                        throw new Element\ValidationException('invalid internal link, referenced document with id [' . $data->getInternal() . '] does not exist');
+                        if (isset($params['resetInvalidFields']) && $params['resetInvalidFields']) {
+                            $data->setInternalType(null);
+                            $data->setInternal(null);
+                        } else {
+                            throw new Element\ValidationException('invalid internal link, referenced document with id [' . $data->getInternal() . '] does not exist');
+                        }
                     }
                 } elseif ($data->getInternalType() == 'asset') {
                     $asset = Asset::getById($data->getInternal());
                     if (!$asset instanceof Asset) {
-                        throw new Element\ValidationException('invalid internal link, referenced asset with id [' . $data->getInternal() . '] does not exist');
+                        if (isset($params['resetInvalidFields']) && $params['resetInvalidFields']) {
+                            $data->setInternalType(null);
+                            $data->setInternal(null);
+                        } else {
+                            throw new Element\ValidationException('invalid internal link, referenced document with id [' . $data->getInternal() . '] does not exist');
+                        }
                     }
                 }
             }
diff --git a/models/DataObject/Concrete.php b/models/DataObject/Concrete.php
index e40e6999793..d3635d06784 100644
--- a/models/DataObject/Concrete.php
+++ b/models/DataObject/Concrete.php
@@ -23,6 +23,7 @@
 use Pimcore\Model;
 use Pimcore\Model\DataObject;
 use Pimcore\Model\DataObject\ClassDefinition\Data\LazyLoadingSupportInterface;
+use Pimcore\Model\DataObject\ClassDefinition\Data\Link;
 use Pimcore\Model\DataObject\ClassDefinition\Data\Relations\AbstractRelations;
 use Pimcore\Model\DataObject\Exception\InheritanceParentNotFoundException;
 use Pimcore\Model\Element\DirtyIndicatorInterface;
@@ -139,6 +140,9 @@ protected function update($isUpdate = null, $params = [])
 
                     //check throws Exception
                     try {
+                        if ($fd instanceof Link) {
+                            $params['resetInvalidFields'] = true;
+                        }
                         $fd->checkValidity($value, $omitMandatoryCheck, $params);
                     } catch (\Exception $e) {
                         if ($this->getClass()->getAllowInherit() && $fd->supportsInheritance() && $fd->isEmpty($value)) {


Test patch:
diff --git a/tests/Model/DataType/LinkTest.php b/tests/Model/DataType/LinkTest.php
index 1320b874090..7be1be55440 100644
--- a/tests/Model/DataType/LinkTest.php
+++ b/tests/Model/DataType/LinkTest.php
@@ -15,6 +15,8 @@
 
 namespace Pimcore\Tests\Model\DataType;
 
+use Pimcore\Model\Asset;
+use Pimcore\Model\DataObject\ClassDefinition\Data;
 use Pimcore\Model\DataObject\Data\Link;
 use Pimcore\Model\DataObject\Service;
 use Pimcore\Model\DataObject\unittestLink;
@@ -29,6 +31,12 @@
  */
 class LinkTest extends ModelTestCase
 {
+    protected Asset $testAsset;
+
+    protected Link $link;
+
+    protected Data $linkDefinition;
+
     public function setUp(): void
     {
         parent::setUp();
@@ -46,6 +54,25 @@ protected function setUpTestClasses()
         $this->tester->setupPimcoreClass_Link();
     }
 
+    /**
+     * Prepares objects for internal link tests
+     *
+     * @throws \Exception
+     */
+    protected function setupInternalLinkObjects()
+    {
+        $this->testAsset = TestHelper::createImageAsset();
+
+        $link = new Link();
+        $link->setInternal($this->testAsset->getId());
+        $link->setInternalType('asset');
+        $this->link = $link;
+
+        $linkObject = $this->createLinkObject();
+        $linkObject->setTestlink($link);
+        $this->linkDefinition = $linkObject->getClass()->getFieldDefinition('testlink');
+    }
+
     /**
      * @return unittestLink
      *
@@ -81,6 +108,36 @@ public function testSave()
         $this->assertEquals($link->getDirect(), $linkObjectReloaded->getLtestlink()->getDirect());
     }
 
+    /**
+     * Verifies that checkValidity method throws correct exception if invalid data is provided
+     *
+     */
+    public function testInternalCheckValidity()
+    {
+        $this->setupInternalLinkObjects();
+        $this->testAsset->delete();
+
+        //Should return validation exception as asset was deleted
+        $this->expectException(ValidationException::class);
+        $this->linkDefinition->checkValidity($this->link);
+    }
+
+    /**
+     * Verifies that checkValidity method sanitize the link data if invalid data is provided
+     *
+     */
+    public function testInternalCheckValidityParam()
+    {
+        $this->setupInternalLinkObjects();
+        $this->testAsset->delete();
+        //Should not return validation exception as parameter is set
+        $this->linkDefinition->checkValidity($this->link, true, ['resetInvalidFields' => true]);
+
+        //Should return sanitized link data
+        $this->assertTrue($this->link->getInternal() === null);
+        $this->assertTrue($this->link->getInternalType() === null);
+    }
+
     /**
      * Verifies that Link data throws correct exceptions if invalid data is given
      *