From b57d36c5c2d591a467b7d8b7ec1172387247bab0 Mon Sep 17 00:00:00 2001
From: Imko Schumacher <okmiim@live.de>
Date: Sun, 11 Dec 2022 17:43:53 +0100
Subject: [PATCH] [BUGFIX] Allow 1970-01-01 as native datetime input

The TCA processing temporary converts native datetime inputs to
timestamps for some checks.
The start of the unix epoch (1970-01-01 00:00) corresponds to the
0 timestamp. Conditionally checks interpreted that as false,
resulting the value to be the default value.
This fix also prevents empty date values from being set to
MySQL-specific values (0000-00-00 00:00:00).

Resolves: #92900
Releases: main, 12.4
Change-Id: I979d59f4abe3dd73117a9e135a74ae3036192d9b
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79812
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
---
 .../DatabaseRowDateTimeFieldsTest.php         | 37 +++++++-
 .../core/Classes/DataHandling/DataHandler.php | 31 ++++---
 .../Classes/Database/Query/QueryHelper.php    |  2 +-
 .../Unit/DataHandling/DataHandlerTest.php     | 85 +++++++++++++++++++
 4 files changed, 138 insertions(+), 17 deletions(-)

diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRowDateTimeFieldsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRowDateTimeFieldsTest.php
index 113bc8dfb518..2d82edf16853 100644
--- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRowDateTimeFieldsTest.php
+++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRowDateTimeFieldsTest.php
@@ -154,7 +154,7 @@ final class DatabaseRowDateTimeFieldsTest extends UnitTestCase
             ],
         ];
         $expected = $input;
-        $expected['databaseRow']['aField'] = 0;
+        $expected['databaseRow']['aField'] = '00:00:00';
         self::assertEquals($expected, (new DatabaseRowDateTimeFields())->addData($input));
     }
 
@@ -279,7 +279,7 @@ final class DatabaseRowDateTimeFieldsTest extends UnitTestCase
     /**
      * @test
      */
-    public function addDataConvertsMidnightTimeStringOfNullableFieldToTimestamp(): void
+    public function addDataConvertsMidnightTimeStringOfNullableFieldToDefaultValue(): void
     {
         $oldTimezone = date_default_timezone_get();
         date_default_timezone_set('UTC');
@@ -301,7 +301,38 @@ final class DatabaseRowDateTimeFieldsTest extends UnitTestCase
             ],
         ];
         $expected = $input;
-        $expected['databaseRow']['aField'] = 0;
+        $expected['databaseRow']['aField'] = '00:00:00';
+
+        self::assertEquals($expected, (new DatabaseRowDateTimeFields())->addData($input));
+        date_default_timezone_set($oldTimezone);
+    }
+
+    /**
+     * @test
+     */
+    public function addDataConvertsMidnightTimeStringOfNullableFieldToNull(): void
+    {
+        $oldTimezone = date_default_timezone_get();
+        date_default_timezone_set('UTC');
+        $input = [
+            'tableName' => 'aTable',
+            'processedTca' => [
+                'columns' => [
+                    'aField' => [
+                        'config' => [
+                            'type' => 'datetime',
+                            'dbType' => 'time',
+                            'nullable' => true,
+                        ],
+                    ],
+                ],
+            ],
+            'databaseRow' => [
+                'aField' => null,
+            ],
+        ];
+        $expected = $input;
+        $expected['databaseRow']['aField'] = null;
 
         self::assertEquals($expected, (new DatabaseRowDateTimeFields())->addData($input));
         date_default_timezone_set($oldTimezone);
diff --git a/typo3/sysext/core/Classes/DataHandling/DataHandler.php b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
index 8c44eca99eb5..a3a5ef08e97e 100644
--- a/typo3/sysext/core/Classes/DataHandling/DataHandler.php
+++ b/typo3/sysext/core/Classes/DataHandling/DataHandler.php
@@ -2137,12 +2137,14 @@ class DataHandler implements LoggerAwareInterface
         $isNativeDateTimeField = false;
         $nativeDateTimeFieldFormat = '';
         $nativeDateTimeFieldEmptyValue = '';
+        $nativeDateTimeFieldResetValue = '';
         $nativeDateTimeType = $tcaFieldConf['dbType'] ?? '';
         if (in_array($nativeDateTimeType, QueryHelper::getDateTimeTypes(), true)) {
             $isNativeDateTimeField = true;
             $dateTimeFormats = QueryHelper::getDateTimeFormats();
             $nativeDateTimeFieldFormat = $dateTimeFormats[$nativeDateTimeType]['format'];
             $nativeDateTimeFieldEmptyValue = $dateTimeFormats[$nativeDateTimeType]['empty'];
+            $nativeDateTimeFieldResetValue = $dateTimeFormats[$nativeDateTimeType]['reset'];
             if (empty($value)) {
                 $value = null;
             } else {
@@ -2184,35 +2186,38 @@ class DataHandler implements LoggerAwareInterface
             }
         }
 
-        // Set the value to null if we have an empty value for a native field
-        $res['value'] = $isNativeDateTimeField && !$value ? null : $value;
-
         // Skip range validation, if the default value equals 0 and the input value is 0, "0" or an empty string.
         // This is needed for timestamp date fields with ['range']['lower'] set.
         $skipRangeValidation =
-            isset($tcaFieldConf['default'], $res['value'])
+            isset($tcaFieldConf['default'], $value)
             && (int)$tcaFieldConf['default'] === 0
-            && ($res['value'] === '' || $res['value'] === '0' || $res['value'] === 0);
+            && ($value === '' || $value === '0' || $value === 0);
 
         // Checking range of value:
-        if (!$skipRangeValidation && isset($tcaFieldConf['range']) && is_array($tcaFieldConf['range'])) {
-            if (isset($tcaFieldConf['range']['upper']) && ceil($res['value']) > (int)$tcaFieldConf['range']['upper']) {
-                $res['value'] = (int)$tcaFieldConf['range']['upper'];
+        if (!$skipRangeValidation && is_array($tcaFieldConf['range'] ?? null)) {
+            if (isset($tcaFieldConf['range']['upper']) && ceil($value) > (int)$tcaFieldConf['range']['upper']) {
+                $value = (int)$tcaFieldConf['range']['upper'];
             }
-            if (isset($tcaFieldConf['range']['lower']) && floor($res['value']) < (int)$tcaFieldConf['range']['lower']) {
-                $res['value'] = (int)$tcaFieldConf['range']['lower'];
+            if (isset($tcaFieldConf['range']['lower']) && floor($value) < (int)$tcaFieldConf['range']['lower']) {
+                $value = (int)$tcaFieldConf['range']['lower'];
             }
         }
 
         // Handle native date/time fields
         if ($isNativeDateTimeField) {
-            // Convert the timestamp back to a date/time
-            $res['value'] = $res['value'] ? gmdate($nativeDateTimeFieldFormat, $res['value']) : $nativeDateTimeFieldEmptyValue;
+            if ($tcaFieldConf['nullable'] ?? false) {
+                // Convert the timestamp back to a date/time if not null
+                $value = $value !== null ? gmdate($nativeDateTimeFieldFormat, $value) : null;
+            } else {
+                // Convert the timestamp back to a date/time
+                $value = $value !== null ? gmdate($nativeDateTimeFieldFormat, $value) : $nativeDateTimeFieldResetValue;
+            }
         } else {
             // Ensure value is always an int if no native field is used
-            $res['value'] = (int)($res['value'] ?? 0);
+            $value = (int)$value;
         }
 
+        $res['value'] = $value;
         return $res;
     }
 
diff --git a/typo3/sysext/core/Classes/Database/Query/QueryHelper.php b/typo3/sysext/core/Classes/Database/Query/QueryHelper.php
index 2af2d9349c37..7dee47b7b81e 100644
--- a/typo3/sysext/core/Classes/Database/Query/QueryHelper.php
+++ b/typo3/sysext/core/Classes/Database/Query/QueryHelper.php
@@ -196,7 +196,7 @@ class QueryHelper
             'time' => [
                 'empty' => '00:00:00',
                 'format' => 'H:i:s',
-                'reset' => 0,
+                'reset' => '00:00:00',
             ],
         ];
     }
diff --git a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
index 2dd79c7fb824..53ca88f55392 100644
--- a/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
+++ b/typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
@@ -536,6 +536,91 @@ final class DataHandlerTest extends UnitTestCase
         self::assertEquals($expectedOutput, $returnValue['value']);
     }
 
+    public static function inputValueCheckNativeDbTypeDataProvider(): array
+    {
+        return [
+            'Datetime at unix epoch' => [
+                '1970-01-01T00:00:00Z',
+                'datetime',
+                'datetime',
+                false,
+                '1970-01-01 00:00:00',
+            ],
+            'Default datetime' => [
+                '0000-00-00 00:00:00',
+                'datetime',
+                'datetime',
+                false,
+                null,
+            ],
+            'Default date' => [
+                '0000-00-00',
+                'date',
+                'date',
+                false,
+                null,
+            ],
+            'Default time' => [
+                '00:00:00',
+                'time',
+                'time',
+                false,
+                '00:00:00',
+            ],
+            'Null on nullable time' => [
+                null,
+                'time',
+                'time',
+                true,
+                null,
+            ],
+            'Null on not nullable time' => [
+                null,
+                'time',
+                'time',
+                false,
+                '00:00:00',
+            ],
+            'Minimum mysql datetime' => [
+                '1000-01-01 00:00:00',
+                'datetime',
+                'datetime',
+                false,
+                '1000-01-01 00:00:00',
+            ],
+            'Maximum mysql datetime' => [
+                '9999-12-31 23:59:59',
+                'datetime',
+                'datetime',
+                false,
+                '9999-12-31 23:59:59',
+            ],
+        ];
+    }
+
+    /**
+     * @param string|null $value
+     * @param string $dbType
+     * @param string $format
+     * @param bool $nullable
+     * @param mixed|null $expectedOutput
+     * @dataProvider inputValueCheckNativeDbTypeDataProvider
+     * @test
+     */
+    public function inputValueCheckNativeDbType(string|null $value, string $dbType, string $format, bool $nullable, $expectedOutput): void
+    {
+        $tcaFieldConf = [
+            'input' => [],
+            'dbType' => $dbType,
+            'format' => $dbType,
+            'nullable' => $nullable,
+        ];
+
+        $returnValue = $this->subject->_call('checkValueForDatetime', $value, $tcaFieldConf);
+
+        self::assertEquals($expectedOutput, $returnValue['value']);
+    }
+
     ///////////////////////////////////////////
     // Tests concerning checkModifyAccessList
     ///////////////////////////////////////////
-- 
GitLab