From 5ccdeb32d3bfc1d94e40a8b9881bb048bbf72a1f Mon Sep 17 00:00:00 2001
From: Christian Kuhn <lolli@schwarzbu.ch>
Date: Thu, 28 Apr 2016 19:09:04 +0200
Subject: [PATCH] [BUGFIX] Exception editing inline mm with deleted child child

Have an inline m:m record and delete one child child that has an
intermediate record pointing to it. Opening the parent throws
a DatabaseRecordException.
The patch extends this exception to add tableName and uid, then
catches the exception in the inline data provider, creates a
nice error message as flash message and continues displaying record.

Change-Id: I1792716b4e5454b11499cb2ba684bac403b3f13d
Resolves: #71719
Releases: master, 7.6
Reviewed-on: https://review.typo3.org/47959
Reviewed-by: Wouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Michael Oehlhof <typo3@oehlhof.de>
Tested-by: Michael Oehlhof <typo3@oehlhof.de>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
---
 .../Exception/DatabaseRecordException.php     | 45 +++++++++++++++++++
 .../AbstractDatabaseRecordProvider.php        |  5 ++-
 .../Form/FormDataProvider/TcaInline.php       | 33 ++++++++++++--
 .../Resources/Private/Language/locallang.xlf  |  6 +++
 .../Public/JavaScript/jsfunc.inline.js        |  2 +-
 .../FormDataProvider/DatabaseEditRowTest.php  | 21 +++++++++
 6 files changed, 107 insertions(+), 5 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Form/Exception/DatabaseRecordException.php b/typo3/sysext/backend/Classes/Form/Exception/DatabaseRecordException.php
index 59906adc208c..377815432ffb 100644
--- a/typo3/sysext/backend/Classes/Form/Exception/DatabaseRecordException.php
+++ b/typo3/sysext/backend/Classes/Form/Exception/DatabaseRecordException.php
@@ -21,4 +21,49 @@ use TYPO3\CMS\Backend\Form\Exception;
  */
 class DatabaseRecordException extends Exception
 {
+    /**
+     * @var string Table name
+     */
+    protected $tableName;
+
+    /**
+     * @var int Table row uid
+     */
+    protected $uid;
+
+    /**
+     * Constructor overwrites default constructor.
+     *
+     * @param string $message Human readable error message
+     * @param int $code Exception code timestamp
+     * @param \Exception $previousException Possible exception from database layer
+     * @param string $tableName Table name query was working on
+     * @param int $uid Table row uid
+     */
+    public function __construct($message, $code, \Exception $previousException = null, $tableName, $uid)
+    {
+        parent::__construct($message, $code, $previousException);
+        $this->tableName = $tableName;
+        $this->uid = $uid;
+    }
+
+    /**
+     * Return table name
+     *
+     * @return string
+     */
+    public function getTableName()
+    {
+        return $this->tableName;
+    }
+
+    /**
+     * Return row uid
+     *
+     * @return int
+     */
+    public function getUid()
+    {
+        return $this->uid;
+    }
 }
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php
index 04ef531361f0..234775f47081 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php
@@ -57,7 +57,10 @@ abstract class AbstractDatabaseRecordProvider
             // and transformed to a message to the user or something
             throw new DatabaseRecordException(
                 'Record with uid ' . $uid . ' from table ' . $tableName . ' not found',
-                1437656081
+                1437656081,
+                null,
+                $tableName,
+                (int)$uid
             );
         }
         if (!is_array($row)) {
diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
index 126c552e19ed..a8220c6889eb 100644
--- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
+++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
@@ -14,6 +14,7 @@ namespace TYPO3\CMS\Backend\Form\FormDataProvider;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Form\Exception\DatabaseRecordException;
 use TYPO3\CMS\Backend\Form\FormDataCompiler;
 use TYPO3\CMS\Backend\Form\FormDataGroup\OnTheFly;
 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
@@ -22,8 +23,11 @@ use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
 use TYPO3\CMS\Core\Database\RelationHandler;
+use TYPO3\CMS\Core\Messaging\FlashMessage;
+use TYPO3\CMS\Core\Messaging\FlashMessageService;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Versioning\VersionState;
+use TYPO3\CMS\Lang\LanguageService;
 
 /**
  * Resolve and prepare inline data.
@@ -313,11 +317,26 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
         ];
 
         // For foreign_selector with useCombination $mainChild is the mm record
-        // and $combinationChild is the child-child. For "normal" relations, $mainChild
-        // is just the normal child record and $combinationChild is empty.
+        // and $combinationChild is the child-child. For 1:n "normal" relations,
+        // $mainChild is just the normal child record and $combinationChild is empty.
         $mainChild = $formDataCompiler->compile($formDataCompilerInput);
         if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
-            $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig);
+            try {
+                $mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig);
+            } catch (DatabaseRecordException $e) {
+                // The child could not be compiled, probably it was deleted and a dangling mm record
+                // exists. This is a data inconsistency, we catch this exception and create a flash message
+                $message = vsprintf(
+                    $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang.xlf:formEngine.databaseRecordErrorInlineChildChild'),
+                    [ $e->getTableName(), $e->getUid(), $childTableName, (int)$childUid ]
+                );
+                $flashMessage = GeneralUtility::makeInstance(FlashMessage::class,
+                    $message,
+                    '',
+                    FlashMessage::ERROR
+                );
+                GeneralUtility::makeInstance(FlashMessageService::class)->getMessageQueueByIdentifier()->enqueue($flashMessage);
+            }
         }
         return $mainChild;
     }
@@ -449,4 +468,12 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
     {
         return $GLOBALS['BE_USER'];
     }
+
+    /**
+     * @return LanguageService
+     */
+    protected function getLanguageService()
+    {
+        return $GLOBALS['LANG'];
+    }
 }
diff --git a/typo3/sysext/backend/Resources/Private/Language/locallang.xlf b/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
index a40bd931e387..b91a1cc3b9f0 100644
--- a/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
+++ b/typo3/sysext/backend/Resources/Private/Language/locallang.xlf
@@ -91,6 +91,12 @@ Have a nice day.</source>
 			<trans-unit id="button.hidePageTsConfig">
 				<source>Hide PageTS-Config</source>
 			</trans-unit>
+			<trans-unit id="formEngine.databaseRecordErrorInlineChildChild">
+				<source>The record with uid %2$s from table %1$s could not be retrieved from the database. This data incosistency can occur if
+				a base record has been deleted but the intermediate record from table %3$s with uid %4$s still points to it. To fix
+				this situation, either delete the intermediate record, or recover the deleted record using the recycler module.
+				</source>
+			</trans-unit>
 		</body>
 	</file>
 </xliff>
diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js b/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js
index ff5eb7fb977e..1284193a4c58 100644
--- a/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js
+++ b/typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js
@@ -305,7 +305,7 @@ var inline = {
 
 	showAjaxFailure: function (method, xhr) {
 		inline.unlockAjaxMethod(method);
-		alert('Error: ' + xhr.status + "\n" + xhr.statusText);
+		top.TYPO3.Notification.error('Error ' + xhr.status, xhr.statusText, 0);
 	},
 
 	// foreign_selector: used by selector box (type='select')
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseEditRowTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseEditRowTest.php
index f0d083570e4f..e273f602b9fb 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseEditRowTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseEditRowTest.php
@@ -138,6 +138,27 @@ class DatabaseEditRowTest extends UnitTestCase
         $this->subject->addData($input);
     }
 
+    /**
+     * @test
+     */
+    public function addDataThrowsExceptionDatabaseRecordExceptionWithAdditionalInformationSet()
+    {
+        $input = [
+            'tableName' => 'tt_content',
+            'command' => 'edit',
+            'vanillaUid' => 10,
+        ];
+        $this->dbProphecy->quoteStr(Argument::cetera())->willReturn($input['tableName']);
+        $this->dbProphecy->exec_SELECTgetSingleRow(Argument::cetera())->willReturn(false);
+
+        try {
+            $this->subject->addData($input);
+        } catch (DatabaseRecordException $e) {
+            $this->assertSame('tt_content', $e->getTableName());
+            $this->assertSame(10, $e->getUid());
+        }
+    }
+
     /**
      * @test
      */
-- 
GitLab