From ceddf8681be2c5ea8ae823ac44be953c0cecf016 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 12 Aug 2024 12:41:37 +0200 Subject: [PATCH] Fix date conversion on the server-side (#16841) --- .../Extensions/ObjectExtensions.cs | 18 ++++++ .../DatePickerValueConverter.cs | 18 +++--- .../CoreThings/ObjectExtensionsTests.cs | 62 +++++++++++++++++++ 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index f0f10d8cc6..2ef1f9194f 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -219,6 +219,24 @@ public static class ObjectExtensions } } + if (target == typeof(DateTime) && input is DateTimeOffset dateTimeOffset) + { + // IMPORTANT: for compatability with various editors, we must discard any Offset information and assume UTC time here + return Attempt.Succeed((object?)new DateTime( + new DateOnly(dateTimeOffset.Year, dateTimeOffset.Month, dateTimeOffset.Day), + new TimeOnly(dateTimeOffset.Hour, dateTimeOffset.Minute, dateTimeOffset.Second, dateTimeOffset.Millisecond, dateTimeOffset.Microsecond), + DateTimeKind.Utc)); + } + + if (target == typeof(DateTimeOffset) && input is DateTime dateTime) + { + // IMPORTANT: for compatability with various editors, we must discard any DateTimeKind information and assume UTC time here + return Attempt.Succeed((object?)new DateTimeOffset( + new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day), + new TimeOnly(dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond, dateTime.Microsecond), + TimeSpan.Zero)); + } + TypeConverter? inputConverter = GetCachedSourceTypeConverter(inputType, target); if (inputConverter != null) { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs index 740e1b8f8e..6f9238f01f 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/DatePickerValueConverter.cs @@ -20,23 +20,19 @@ public class DatePickerValueConverter : PropertyValueConverterBase internal static DateTime ParseDateTimeValue(object? source) { - if (source == null) + if (source is null) { return DateTime.MinValue; } - // in XML a DateTime is: string - format "yyyy-MM-ddTHH:mm:ss" - // Actually, not always sometimes it is formatted in UTC style with 'Z' suffixed on the end but that is due to this bug: - // http://issues.umbraco.org/issue/U4-4145, http://issues.umbraco.org/issue/U4-3894 - // We should just be using TryConvertTo instead. - if (source is string sourceString) + if (source is DateTime dateTimeValue) { - Attempt attempt = sourceString.TryConvertTo(); - return attempt.Success == false ? DateTime.MinValue : attempt.Result; + return dateTimeValue; } - // in the database a DateTime is: DateTime - // default value is: DateTime.MinValue - return source is DateTime dateTimeValue ? dateTimeValue : DateTime.MinValue; + Attempt attempt = source.TryConvertTo(); + return attempt.Success + ? attempt.Result + : DateTime.MinValue; } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs index bfbe581f2e..7aae5e54ee 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/CoreThings/ObjectExtensionsTests.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Tests.Common.TestHelpers; using Umbraco.Extensions; +using DateTimeOffset = System.DateTimeOffset; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.CoreThings; @@ -332,6 +333,50 @@ public class ObjectExtensionsTests Assert.AreEqual("This is a string", conv.Result); } + [Test] + public void CanConvertDateTimeOffsetToDateTime() + { + var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero); + var result = dateTimeOffset.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result); + Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind); + }); + } + + [Test] + public void CanConvertDateTimeToDateTimeOffset() + { + var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Utc); + var result = dateTime.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result); + } + + [Test] + public void DiscardsOffsetWhenConvertingDateTimeOffsetToDateTime() + { + var dateTimeOffset = new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.FromHours(2)); + var result = dateTimeOffset.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03)), result.Result); + Assert.AreEqual(DateTimeKind.Utc, result.Result.Kind); + }); + } + + [Test] + public void DiscardsDateTimeKindWhenConvertingDateTimeToDateTimeOffset() + { + var dateTime = new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), DateTimeKind.Local); + var result = dateTime.TryConvertTo(); + Assert.IsTrue(result.Success); + Assert.AreEqual(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30, 01, 02, 03), TimeSpan.Zero), result.Result); + } + [Test] public void Value_Editor_Can_Convert_Decimal_To_Decimal_Clr_Type() { @@ -342,6 +387,23 @@ public class ObjectExtensionsTests Assert.AreEqual(12.34d, result.Result); } + [Test] + public void Value_Editor_Can_Convert_DateTimeOffset_To_DateTime_Clr_Type() + { + var valueEditor = MockedValueEditors.CreateDataValueEditor(ValueTypes.Date); + + var result = valueEditor.TryConvertValueToCrlType(new DateTimeOffset(new DateOnly(2024, 07, 05), new TimeOnly(12, 30), TimeSpan.Zero)); + Assert.IsTrue(result.Success); + Assert.IsTrue(result.Result is DateTime); + + var dateTime = (DateTime)result.Result; + Assert.Multiple(() => + { + Assert.AreEqual(new DateTime(new DateOnly(2024, 07, 05), new TimeOnly(12, 30)), dateTime); + Assert.AreEqual(DateTimeKind.Utc, dateTime.Kind); + }); + } + private class MyTestObject { public override string ToString() => "Hello world";