diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs index 2c3b80fd3c..6166d38d2d 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs @@ -26,7 +26,7 @@ internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPrese new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType) .Result), new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)), - new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework + new DateTimeOffset(contentVersion.VersionDate), contentVersion.CurrentPublishedVersion, contentVersion.CurrentDraftVersion, contentVersion.PreventCleanup); diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs index cf95257716..b9b1be6080 100644 --- a/src/Umbraco.Core/Models/ContentVersionMeta.cs +++ b/src/Umbraco.Core/Models/ContentVersionMeta.cs @@ -37,7 +37,7 @@ public class ContentVersionMeta public int UserId { get; } - public DateTime VersionDate { get; } + public DateTime VersionDate { get; private set; } public bool CurrentPublishedVersion { get; } @@ -47,5 +47,7 @@ public class ContentVersionMeta public string? Username { get; } + public void SpecifyVersionDateKind(DateTimeKind kind) => VersionDate = DateTime.SpecifyKind(VersionDate, kind); + public override string ToString() => $"ContentVersionMeta(versionId: {VersionId}, versionDate: {VersionDate:s}"; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index fed80e753f..ffcc6447eb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -39,8 +39,11 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); content.Published = dto.Published; content.Edited = dto.Edited; @@ -52,7 +55,7 @@ internal class ContentBaseFactory content.PublishedVersionId = publishedVersionDto.Id; if (dto.Published) { - content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; + content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local); content.PublishName = publishedVersionDto.ContentVersionDto.Text; content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } @@ -71,7 +74,7 @@ internal class ContentBaseFactory } /// - /// Builds an IMedia item from a dto and content type. + /// Builds a Media item from a dto and content type. /// public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) { @@ -97,8 +100,8 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -111,7 +114,7 @@ internal class ContentBaseFactory } /// - /// Builds an IMedia item from a dto and content type. + /// Builds a Member item from a dto and member type. /// public static Member BuildEntity(MemberDto dto, IMemberType? contentType) { @@ -126,7 +129,9 @@ internal class ContentBaseFactory content.Id = dto.NodeId; content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate; + content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue + ? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local) + : null; content.PasswordConfiguration = dto.PasswordConfig; content.Key = nodeDto.UniqueId; content.VersionId = contentVersionDto.Id; @@ -140,14 +145,20 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = nodeDto.CreateDate; - content.UpdateDate = contentVersionDto.VersionDate; + content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); + content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; content.IsLockedOut = dto.IsLockedOut; content.IsApproved = dto.IsApproved; - content.LastLoginDate = dto.LastLoginDate; - content.LastLockoutDate = dto.LastLockoutDate; - content.LastPasswordChangeDate = dto.LastPasswordChangeDate; + content.LastLockoutDate = dto.LastLockoutDate.HasValue + ? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local) + : null; + content.LastLoginDate = dto.LastLoginDate.HasValue + ? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local) + : null; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue + ? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local) + : null; // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -186,7 +197,7 @@ internal class ContentBaseFactory new ContentScheduleDto { Action = x.Action.ToString(), - Date = x.Date, + Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local), NodeId = entity.Id, LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), Id = x.Id, @@ -261,7 +272,7 @@ internal class ContentBaseFactory UserId = entity.CreatorId, Text = entity.Name, NodeObjectType = objectType, - CreateDate = entity.CreateDate, + CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local), }; return dto; @@ -275,7 +286,7 @@ internal class ContentBaseFactory { Id = entity.VersionId, NodeId = entity.Id, - VersionDate = entity.UpdateDate, + VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local), UserId = entity.WriterId, Current = true, // always building the current one Text = entity.Name, diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 9a8ae11386..60ec173ca5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -39,16 +39,25 @@ internal static class UserFactory user.Language = dto.UserLanguage; user.SecurityStamp = dto.SecurityStampToken; user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0; - user.LastLockoutDate = dto.LastLockoutDate; - user.LastLoginDate = dto.LastLoginDate; - user.LastPasswordChangeDate = dto.LastPasswordChangeDate; - user.CreateDate = dto.CreateDate; - user.UpdateDate = dto.UpdateDate; user.Avatar = dto.Avatar; user.EmailConfirmedDate = dto.EmailConfirmedDate; user.InvitedDate = dto.InvitedDate; user.Kind = (UserKind)dto.Kind; + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + user.LastLockoutDate = dto.LastLockoutDate.HasValue + ? DateTime.SpecifyKind(dto.LastLockoutDate.Value, DateTimeKind.Local) + : null; + user.LastLoginDate = dto.LastLoginDate.HasValue + ? DateTime.SpecifyKind(dto.LastLoginDate.Value, DateTimeKind.Local) + : null; + user.LastPasswordChangeDate = dto.LastPasswordChangeDate.HasValue + ? DateTime.SpecifyKind(dto.LastPasswordChangeDate.Value, DateTimeKind.Local) + : null; + user.CreateDate = DateTime.SpecifyKind(dto.CreateDate, DateTimeKind.Local); + user.UpdateDate = DateTime.SpecifyKind(dto.UpdateDate, DateTimeKind.Local); + // reset dirty initial properties (U4-1946) user.ResetDirtyProperties(false); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs index d7dc4f8161..e112e360d0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/AuditRepository.cs @@ -29,7 +29,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); } public void CleanLogs(int maximumAgeOfLogsInMinutes) @@ -104,12 +104,12 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) { - items[i].CreateDate = page.Items[i].Datestamp; + items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local); } return items; @@ -149,7 +149,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe LogDto? dto = Database.First(sql); return dto == null ? null - : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp); + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local)); } protected override IEnumerable PerformGetAll(params int[]? ids) => throw new NotImplementedException(); @@ -162,7 +162,7 @@ internal class AuditRepository : EntityRepositoryBase, IAuditRe List? dtos = Database.Fetch(sql); - return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, x.Datestamp)).ToList(); + return dtos.Select(x => new AuditItem(x.NodeId, Enum.Parse(x.Header), x.UserId ?? Constants.Security.UnknownUserId, x.EntityType, x.Comment, x.Parameters, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); } protected override Sql GetBaseQuery(bool isCount) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs index 72459bd755..1b4f2b1efd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -400,15 +400,17 @@ public class DocumentRepository : ContentRepositoryBase 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) { foreach (ContentVariation v in contentVariation) { - content.SetPublishInfo(v.Culture, v.Name, v.Date); + content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index e922ed3cdb..ef9dd67520 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,3 +1,4 @@ +using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -98,6 +99,16 @@ internal class DocumentVersionRepository : IDocumentVersionRepository Page? page = _scopeAccessor.AmbientScope?.Database.Page(pageIndex + 1, pageSize, query); + // Dates stored in the database are local server time, but for SQL Server, will be considered + // as DateTime.Kind = Utc. Fix this so we are consistent when later mapping to DataTimeOffset. + if (page is not null) + { + foreach (ContentVersionMeta item in page.Items) + { + item.SpecifyVersionDateKind(DateTimeKind.Local); + } + } + totalRecords = page?.TotalItems ?? 0; return page?.Items;