[BUGFIX] Exception editing inline mm with deleted child child 59/47959/5
authorChristian Kuhn <lolli@schwarzbu.ch>
Thu, 28 Apr 2016 17:09:04 +0000 (19:09 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Fri, 29 Apr 2016 11:36:03 +0000 (13:36 +0200)
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>
typo3/sysext/backend/Classes/Form/Exception/DatabaseRecordException.php
typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractDatabaseRecordProvider.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
typo3/sysext/backend/Resources/Private/Language/locallang.xlf
typo3/sysext/backend/Resources/Public/JavaScript/jsfunc.inline.js
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseEditRowTest.php

index 59906ad..3778154 100644 (file)
@@ -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;
+    }
 }
index 04ef531..234775f 100644 (file)
@@ -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)) {
index 126c552..a8220c6 100644 (file)
@@ -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'];
+    }
 }
index a40bd93..b91a1cc 100644 (file)
@@ -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>
index ff5eb7f..1284193 100644 (file)
@@ -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')
index f0d0835..e273f60 100644 (file)
@@ -141,6 +141,27 @@ class DatabaseEditRowTest extends UnitTestCase
     /**
      * @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
+     */
     public function addDataThrowsExceptionIfDatabaseFetchingReturnsInvalidRowResultData()
     {
         $input = [