From 97b3023e149ec8858c064a3efc6346fc6faad7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 10 Apr 2025 15:56:14 +0200 Subject: [PATCH 1/9] make sure only to prepend relative URLs (#18998) --- .../input-image-cropper/image-cropper-field.element.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts index b8d403ba09..a82cf8dc81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper-field.element.ts @@ -78,7 +78,12 @@ export class UmbInputImageCropperFieldElement extends UmbLitElement { get source(): string { if (this.src) { - return `${this._serverUrl}${this.src}`; + // Test that URL is relative: + if (this.src.startsWith('/')) { + return `${this._serverUrl}${this.src}`; + } else { + return this.src; + } } return this.fileDataUrl ?? ''; From 7b10d39d66e1ac62683f5cc7da89a39e4782deb1 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 11 Apr 2025 09:32:05 +0200 Subject: [PATCH 2/9] Ensure dates read from the database are treated as local when constructing entities (#18989) * Ensure dates read from the database are treated as local when constructing entities. * Fixed typos in comments. --- .../DocumentVersionPresentationFactory.cs | 2 +- src/Umbraco.Core/Models/ContentVersionMeta.cs | 4 +- .../Factories/ContentBaseFactory.cs | 43 ++++++++++++------- .../Persistence/Factories/UserFactory.cs | 19 +++++--- .../Repositories/Implement/AuditRepository.cs | 10 ++--- .../Implement/DocumentRepository.cs | 6 ++- .../Implement/DocumentVersionRepository.cs | 11 +++++ 7 files changed, 65 insertions(+), 30 deletions(-) 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; From f9496e8067d0d7a5f2f349f7fdb5c4b8ab371ecb Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 14 Apr 2025 10:55:36 +0200 Subject: [PATCH 3/9] Ensure dates read from the database are treated as local when constructing entities (2) (#19013) * Revert "Ensure dates read from the database are treated as local when constructing entities (#18989)" This reverts commit 7b10d39d66e1ac62683f5cc7da89a39e4782deb1. * Avoid system dates stored with local server time being defaulted to UTC on database read. * ContentScheduleDto.Date is UTC --------- Co-authored-by: kjac --- .../DocumentVersionPresentationFactory.cs | 2 +- src/Umbraco.Core/Models/ContentVersionMeta.cs | 4 +- .../Persistence/Dtos/AccessDto.cs | 4 +- .../Persistence/Dtos/AccessRuleDto.cs | 4 +- .../Persistence/Dtos/AuditEntryDto.cs | 2 +- .../Persistence/Dtos/ConsentDto.cs | 2 +- .../Persistence/Dtos/ContentScheduleDto.cs | 1 + .../Dtos/ContentVersionCleanupPolicyDto.cs | 2 +- .../Dtos/ContentVersionCultureVariationDto.cs | 2 +- .../Persistence/Dtos/ContentVersionDto.cs | 2 +- .../Dtos/CreatedPackageSchemaDto.cs | 2 +- .../Dtos/DocumentPublishedReadOnlyDto.cs | 2 +- .../Persistence/Dtos/ExternalLoginDto.cs | 2 +- .../Persistence/Dtos/ExternalLoginTokenDto.cs | 2 +- .../Persistence/Dtos/KeyValueDto.cs | 2 +- .../Persistence/Dtos/LogDto.cs | 2 +- .../Persistence/Dtos/MemberDto.cs | 8 ++-- .../Persistence/Dtos/NodeDto.cs | 2 +- .../Persistence/Dtos/RelationDto.cs | 2 +- .../Persistence/Dtos/ServerRegistrationDto.cs | 4 +- .../Persistence/Dtos/UserDto.cs | 14 +++--- .../Persistence/Dtos/UserGroupDto.cs | 4 +- .../Persistence/Dtos/WebhookLogDto.cs | 4 +- .../Factories/ContentBaseFactory.cs | 43 +++++++------------ .../Persistence/Factories/UserFactory.cs | 19 +++----- .../Repositories/Implement/AuditRepository.cs | 10 ++--- .../Implement/DocumentRepository.cs | 6 +-- .../Implement/DocumentVersionRepository.cs | 11 ----- 28 files changed, 65 insertions(+), 99 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentVersionPresentationFactory.cs index 6166d38d2d..2c3b80fd3c 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), + new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework contentVersion.CurrentPublishedVersion, contentVersion.CurrentDraftVersion, contentVersion.PreventCleanup); diff --git a/src/Umbraco.Core/Models/ContentVersionMeta.cs b/src/Umbraco.Core/Models/ContentVersionMeta.cs index b9b1be6080..cf95257716 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; private set; } + public DateTime VersionDate { get; } public bool CurrentPublishedVersion { get; } @@ -47,7 +47,5 @@ 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/Dtos/AccessDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs index 0821232826..c5bfbc8c01 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessDto.cs @@ -27,11 +27,11 @@ internal class AccessDto [ForeignKey(typeof(NodeDto), Name = "FK_umbracoAccess_umbracoNode_id2")] public int NoAccessNodeId { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs index 3aba928bda..ec843c1c54 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AccessRuleDto.cs @@ -25,11 +25,11 @@ internal class AccessRuleDto [Column("ruleType")] public string? RuleType { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs index 38e63ffc20..c94a680a1f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AuditEntryDto.cs @@ -30,7 +30,7 @@ internal class AuditEntryDto [Length(Constants.Audit.IpLength)] public string? PerformingIp { get; set; } - [Column("eventDateUtc")] + [Column("eventDateUtc", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime EventDateUtc { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs index c6f9006b29..67db05a3cf 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConsentDto.cs @@ -29,7 +29,7 @@ public class ConsentDto [Length(512)] public string? Action { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs index ad4c03ac53..8194448ce4 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentScheduleDto.cs @@ -24,6 +24,7 @@ internal class ContentScheduleDto [NullSetting(NullSetting = NullSettings.Null)] // can be invariant public int? LanguageId { get; set; } + // NOTE: this date is explicitly stored and treated as UTC despite the lack of "Utc" postfix. [Column("date")] public DateTime Date { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs index 4dd1a14fb5..cdc36ad077 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCleanupPolicyDto.cs @@ -27,6 +27,6 @@ internal class ContentVersionCleanupPolicyDto [NullSetting(NullSetting = NullSettings.Null)] public int? KeepLatestVersionPerDayForDays { get; set; } - [Column("updated")] + [Column("updated", ForceToUtc = false)] public DateTime Updated { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs index b7d675f9a8..d77872ae20 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionCultureVariationDto.cs @@ -33,7 +33,7 @@ internal class ContentVersionCultureVariationDto [Column("name")] public string? Name { get; set; } - [Column("date")] // TODO: db rename to 'updateDate' + [Column("date", ForceToUtc = false)] // TODO: db rename to 'updateDate' public DateTime UpdateDate { get; set; } [Column("availableUserId")] // TODO: db rename to 'updateDate' diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs index 07fe6d9a84..e05bcf0d05 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentVersionDto.cs @@ -22,7 +22,7 @@ public class ContentVersionDto [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId,preventCleanup")] public int NodeId { get; set; } - [Column("versionDate")] // TODO: db rename to 'updateDate' + [Column("versionDate", ForceToUtc = false)] // TODO: db rename to 'updateDate' [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime VersionDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs index 060c9d5a23..07ac53d552 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -27,7 +27,7 @@ public class CreatedPackageSchemaDto [NullSetting(NullSetting = NullSettings.NotNull)] public string Value { get; set; } = null!; - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs index 2f0b2ed5f5..0087298645 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentPublishedReadOnlyDto.cs @@ -20,6 +20,6 @@ internal class DocumentPublishedReadOnlyDto [Column("newest")] public bool Newest { get; set; } - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] public DateTime VersionDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 05c94ed3db..a34c1100a2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -42,7 +42,7 @@ internal class ExternalLoginDto [Index(IndexTypes.NonClustered, ForColumns = "loginProvider,providerKey", Name = "IX_" + TableName + "_ProviderKey")] public string ProviderKey { get; set; } = null!; - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs index b9ae050960..9699a6ef13 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginTokenDto.cs @@ -31,7 +31,7 @@ internal class ExternalLoginTokenDto [NullSetting(NullSetting = NullSettings.NotNull)] public string Value { get; set; } = null!; - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs index 5c985a0174..6bab9b91be 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/KeyValueDto.cs @@ -23,7 +23,7 @@ internal class KeyValueDto [NullSetting(NullSetting = NullSettings.Null)] public string? Value { get; set; } - [Column("updated")] + [Column("updated", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs index 62f8232da5..36f637fef3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LogDto.cs @@ -35,7 +35,7 @@ internal class LogDto [NullSetting(NullSetting = NullSettings.Null)] public string? EntityType { get; set; } - [Column("Datestamp")] + [Column("Datestamp", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_datestamp", ForColumns = "Datestamp,userId,NodeId")] public DateTime Datestamp { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs index 77b500eef5..5a025c412e 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/MemberDto.cs @@ -45,7 +45,7 @@ internal class MemberDto [Length(255)] public string? SecurityStampToken { get; set; } - [Column("emailConfirmedDate")] + [Column("emailConfirmedDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? EmailConfirmedDate { get; set; } @@ -62,15 +62,15 @@ internal class MemberDto [Constraint(Default = 1)] public bool IsApproved { get; set; } - [Column("lastLoginDate")] + [Column("lastLoginDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } - [Column("lastLockoutDate")] + [Column("lastLockoutDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLockoutDate { get; set; } - [Column("lastPasswordChangeDate")] + [Column("lastPasswordChangeDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastPasswordChangeDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index c136f45fd4..e140b3ec5d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -70,7 +70,7 @@ public class NodeDto [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")] public Guid? NodeObjectType { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs index 59484734dc..51c36126d5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationDto.cs @@ -27,7 +27,7 @@ internal class RelationDto [ForeignKey(typeof(RelationTypeDto))] public int RelationType { get; set; } - [Column("datetime")] + [Column("datetime", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime Datetime { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs index 66a8c2bd07..a91bfc6bd5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ServerRegistrationDto.cs @@ -23,11 +23,11 @@ internal class ServerRegistrationDto [Index(IndexTypes.UniqueNonClustered, Name = "IX_computerName")] // server identity is unique public string? ServerIdentity { get; set; } - [Column("registeredDate")] + [Column("registeredDate", ForceToUtc = false)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime DateRegistered { get; set; } - [Column("lastNotifiedDate")] + [Column("lastNotifiedDate", ForceToUtc = false)] public DateTime DateAccessed { get; set; } [Column("isActive")] diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index fa8011be29..4972332566 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -73,32 +73,32 @@ public class UserDto [NullSetting(NullSetting = NullSettings.Null)] public int? FailedLoginAttempts { get; set; } - [Column("lastLockoutDate")] + [Column("lastLockoutDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLockoutDate { get; set; } - [Column("lastPasswordChangeDate")] + [Column("lastPasswordChangeDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastPasswordChangeDate { get; set; } - [Column("lastLoginDate")] + [Column("lastLoginDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? LastLoginDate { get; set; } - [Column("emailConfirmedDate")] + [Column("emailConfirmedDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? EmailConfirmedDate { get; set; } - [Column("invitedDate")] + [Column("invitedDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.Null)] public DateTime? InvitedDate { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } = DateTime.Now; - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } = DateTime.Now; diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index 548d2ff57d..cf66725f36 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -44,12 +44,12 @@ public class UserGroupDto [Obsolete("Is not used anymore Use UserGroup2PermissionDtos instead. This will be removed in Umbraco 18.")] public string? DefaultPermissions { get; set; } - [Column("createDate")] + [Column("createDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime CreateDate { get; set; } - [Column("updateDate")] + [Column("updateDate", ForceToUtc = false)] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs index 8e409cd0b3..d5de4bd008 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs @@ -1,4 +1,4 @@ -using NPoco; +using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; @@ -24,7 +24,7 @@ internal class WebhookLogDto [NullSetting(NullSetting = NullSettings.NotNull)] public string StatusCode { get; set; } = string.Empty; - [Column(Name = "date")] + [Column(Name = "date", ForceToUtc = false)] [Index(IndexTypes.NonClustered, Name = "IX_" + Constants.DatabaseSchema.Tables.WebhookLog + "_date")] [NullSetting(NullSetting = NullSettings.NotNull)] public DateTime Date { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs index ffcc6447eb..fed80e753f 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ContentBaseFactory.cs @@ -39,11 +39,8 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - - // 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.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; content.Published = dto.Published; content.Edited = dto.Edited; @@ -55,7 +52,7 @@ internal class ContentBaseFactory content.PublishedVersionId = publishedVersionDto.Id; if (dto.Published) { - content.PublishDate = DateTime.SpecifyKind(publishedVersionDto.ContentVersionDto.VersionDate, DateTimeKind.Local); + content.PublishDate = publishedVersionDto.ContentVersionDto.VersionDate; content.PublishName = publishedVersionDto.ContentVersionDto.Text; content.PublisherId = publishedVersionDto.ContentVersionDto.UserId; } @@ -74,7 +71,7 @@ internal class ContentBaseFactory } /// - /// Builds a Media item from a dto and content type. + /// Builds an IMedia item from a dto and content type. /// public static Core.Models.Media BuildEntity(ContentDto dto, IMediaType? contentType) { @@ -100,8 +97,8 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); - content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -114,7 +111,7 @@ internal class ContentBaseFactory } /// - /// Builds a Member item from a dto and member type. + /// Builds an IMedia item from a dto and content type. /// public static Member BuildEntity(MemberDto dto, IMemberType? contentType) { @@ -129,9 +126,7 @@ internal class ContentBaseFactory content.Id = dto.NodeId; content.SecurityStamp = dto.SecurityStampToken; - content.EmailConfirmedDate = dto.EmailConfirmedDate.HasValue - ? DateTime.SpecifyKind(dto.EmailConfirmedDate.Value, DateTimeKind.Local) - : null; + content.EmailConfirmedDate = dto.EmailConfirmedDate; content.PasswordConfiguration = dto.PasswordConfig; content.Key = nodeDto.UniqueId; content.VersionId = contentVersionDto.Id; @@ -145,20 +140,14 @@ internal class ContentBaseFactory content.CreatorId = nodeDto.UserId ?? Constants.Security.UnknownUserId; content.WriterId = contentVersionDto.UserId ?? Constants.Security.UnknownUserId; - content.CreateDate = DateTime.SpecifyKind(nodeDto.CreateDate, DateTimeKind.Local); - content.UpdateDate = DateTime.SpecifyKind(contentVersionDto.VersionDate, DateTimeKind.Local); + content.CreateDate = nodeDto.CreateDate; + content.UpdateDate = contentVersionDto.VersionDate; content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default; content.IsLockedOut = dto.IsLockedOut; content.IsApproved = dto.IsApproved; - 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; + content.LastLoginDate = dto.LastLoginDate; + content.LastLockoutDate = dto.LastLockoutDate; + content.LastPasswordChangeDate = dto.LastPasswordChangeDate; // reset dirty initial properties (U4-1946) content.ResetDirtyProperties(false); @@ -197,7 +186,7 @@ internal class ContentBaseFactory new ContentScheduleDto { Action = x.Action.ToString(), - Date = DateTime.SpecifyKind(x.Date, DateTimeKind.Local), + Date = x.Date, NodeId = entity.Id, LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false), Id = x.Id, @@ -272,7 +261,7 @@ internal class ContentBaseFactory UserId = entity.CreatorId, Text = entity.Name, NodeObjectType = objectType, - CreateDate = DateTime.SpecifyKind(entity.CreateDate, DateTimeKind.Local), + CreateDate = entity.CreateDate, }; return dto; @@ -286,7 +275,7 @@ internal class ContentBaseFactory { Id = entity.VersionId, NodeId = entity.Id, - VersionDate = DateTime.SpecifyKind(entity.UpdateDate, DateTimeKind.Local), + VersionDate = entity.UpdateDate, 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 60ec173ca5..9a8ae11386 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -39,25 +39,16 @@ 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 e112e360d0..d7dc4f8161 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, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); + 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(); } 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, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local))).ToList(); + 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(); // map the DateStamp for (var i = 0; i < items.Count; i++) { - items[i].CreateDate = DateTime.SpecifyKind(page.Items[i].Datestamp, DateTimeKind.Local); + items[i].CreateDate = page.Items[i].Datestamp; } 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, DateTime.SpecifyKind(dto.Datestamp, DateTimeKind.Local)); + : new AuditItem(dto.NodeId, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters, dto.Datestamp); } 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, DateTime.SpecifyKind(x.Datestamp, DateTimeKind.Local))).ToList(); + 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(); } 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 1b4f2b1efd..72459bd755 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentRepository.cs @@ -400,17 +400,15 @@ public class DocumentRepository : ContentRepositoryBase 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) { foreach (ContentVariation v in contentVariation) { - content.SetPublishInfo(v.Culture, v.Name, DateTime.SpecifyKind(v.Date, DateTimeKind.Local)); + content.SetPublishInfo(v.Culture, v.Name, v.Date); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs index ef9dd67520..e922ed3cdb 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DocumentVersionRepository.cs @@ -1,4 +1,3 @@ -using System.Data; using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -99,16 +98,6 @@ 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; From 7495c3c7b2fb66bac75609c7a84d562d41c7f067 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 14 Apr 2025 14:50:02 +0200 Subject: [PATCH 4/9] Treat content schedule dates as UTC (#19028) --- .../Extensions/ContentExtensions.cs | 4 +- .../UmbracoIntegrationTestWithContent.cs | 2 +- .../Services/ContentServiceTests.cs | 18 +-- .../Services/DocumentUrlServiceTests.cs | 6 +- ...umentUrlServiceTests_HideTopLevel_False.cs | 6 +- .../Services/PublishStatusServiceTest.cs | 2 +- .../Services/PublishedUrlInfoProviderTests.cs | 2 +- ...ishedUrlInfoProvider_hidetoplevel_false.cs | 2 +- ...ontentPublishingServiceTests.Scheduling.cs | 6 +- .../Services/EntityServiceTests.cs | 2 +- .../ContentExtensionsTests.GetStatus.cs | 136 ++++++++++++++++++ .../Extensions/ContentExtensionsTests.cs | 8 ++ .../Umbraco.Tests.UnitTests.csproj | 3 + 13 files changed, 172 insertions(+), 25 deletions(-) create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.GetStatus.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.cs diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index dde4ffb397..35b9d945d8 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -245,13 +245,13 @@ public static class ContentExtensions } IEnumerable expires = contentSchedule.GetSchedule(culture, ContentScheduleAction.Expire); - if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date)) + if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.UtcNow > x.Date)) { return ContentStatus.Expired; } IEnumerable release = contentSchedule.GetSchedule(culture, ContentScheduleAction.Release); - if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now)) + if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.UtcNow)) { return ContentStatus.AwaitingRelease; } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index 77c65ab10b..70957f4c83 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -57,7 +57,7 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 Subpage = ContentBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Id); Subpage.Key = new Guid(SubPageKey); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(Subpage, -1, contentSchedule); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index f75a1c8943..8cab864f37 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -214,7 +214,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent ctVariant.Variations = ContentVariation.Culture; ContentTypeService.Save(ctVariant); - var now = DateTime.Now; + var now = DateTime.UtcNow; // 10x invariant content, half is scheduled to be published in 5 seconds, the other half is scheduled to be unpublished in 5 seconds var invariant = new List(); @@ -321,7 +321,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // Act var content = ContentService.CreateAndSave("Test", Constants.System.Root, "umbTextpage"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddHours(2)); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddHours(2)); ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); Assert.AreEqual(1, contentSchedule.FullSchedule.Count); @@ -676,7 +676,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var root = ContentService.GetById(Textpage.Id); ContentService.Publish(root!, root!.AvailableCultures.ToArray()); var content = ContentService.GetById(Subpage.Id); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddSeconds(1)); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddSeconds(1)); ContentService.PersistContentSchedule(content!, contentSchedule); ContentService.Publish(content, content.AvailableCultures.ToArray()); @@ -1386,7 +1386,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Subpage.Id); // This Content expired 5min ago - var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.Now.AddMinutes(-5)); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddMinutes(-5)); ContentService.Save(content, contentSchedule: contentSchedule); var parent = ContentService.GetById(Textpage.Id); @@ -1416,7 +1416,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = ContentBuilder.CreateBasicContent(contentType); content.SetCultureName("Hello", "en-US"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.Now.AddMinutes(-5)); + var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.UtcNow.AddMinutes(-5)); ContentService.Save(content, contentSchedule: contentSchedule); var published = ContentService.Publish(content, new[] { "en-US" }); @@ -1431,7 +1431,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent { // Arrange var content = ContentService.GetById(Subpage.Id); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null); ContentService.Save(content, Constants.Security.SuperUserId, contentSchedule); var parent = ContentService.GetById(Textpage.Id); @@ -1488,7 +1488,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent content.Properties[0].SetValue("Foo", string.Empty); contentService.Save(content); contentService.PersistContentSchedule(content, - ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); + ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null)); // Act var result = contentService.Publish(content, Array.Empty(), userId: Constants.Security.SuperUserId); @@ -1540,7 +1540,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent contentService.Publish(content, Array.Empty()); contentService.PersistContentSchedule(content, - ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddHours(2), null)); + ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddHours(2), null)); contentService.Save(content); // Act @@ -1568,7 +1568,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent var content = ContentBuilder.CreateBasicContent(contentType); content.SetCultureName("Hello", "en-US"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.Now.AddHours(2), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddHours(2), null); ContentService.Save(content, contentSchedule: contentSchedule); var published = ContentService.Publish(content, new[] { "en-US" }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs index 3f015e5a94..afea9f21ff 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests.cs @@ -230,7 +230,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent // Create a subpage var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, documentName, Subpage.Id); subsubpage.Key = Guid.Parse(documentKey); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(subsubpage, -1, contentSchedule); if (loadDraft is false) @@ -248,7 +248,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); if (loadDraft is false) @@ -266,7 +266,7 @@ public class DocumentUrlServiceTests : UmbracoIntegrationTestWithContent { // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); // Create a child of second root diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests_HideTopLevel_False.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests_HideTopLevel_False.cs index 5158e421c1..aefd600170 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests_HideTopLevel_False.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTests_HideTopLevel_False.cs @@ -62,7 +62,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest // Create a subpage var subsubpage = ContentBuilder.CreateSimpleContent(ContentType, "Sub Page 1", Subpage.Id); subsubpage.Key = new Guid("DF49F477-12F2-4E33-8563-91A7CC1DCDBB"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(subsubpage, -1, contentSchedule); if (loadDraft is false) @@ -81,7 +81,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); secondRoot.Key = new Guid("8E21BCD4-02CA-483D-84B0-1FC92702E198"); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); if (loadDraft is false) @@ -100,7 +100,7 @@ public class DocumentUrlServiceTests_HideTopLevel_False : UmbracoIntegrationTest { // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); // Create a child of second root diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs index 3fbf0b4628..e0f0bdbc2f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs @@ -164,7 +164,7 @@ public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent { var grandchild = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild", Subpage2.Id); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(grandchild, -1, contentSchedule); var publishResults = ContentService.PublishBranch(Textpage, PublishBranchFilter.IncludeUnpublished, ["*"]); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs index 2fa0fccbac..4ed55017ee 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProviderTests.cs @@ -12,7 +12,7 @@ public class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTestsBase { // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); // Create a child of second root diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs index cc68a4bdb0..087d4c9c22 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishedUrlInfoProvider_hidetoplevel_false.cs @@ -19,7 +19,7 @@ public class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrlInfoProvi { // Create a second root var secondRoot = ContentBuilder.CreateSimpleContent(ContentType, "Second Root", null); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(secondRoot, -1, contentSchedule); // Create a child of second root diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Scheduling.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Scheduling.cs index 5fad7f9d17..61c26bc873 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Scheduling.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Scheduling.cs @@ -14,7 +14,7 @@ public partial class ContentPublishingServiceTests var result = await ContentPublishingService.PublishAsync( Textpage.Key, - MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)), + MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)), Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); @@ -25,7 +25,7 @@ public partial class ContentPublishingServiceTests [Test] public async Task Publish_Single_Item_Does_Not_Publish_Children_In_The_Future() { - await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)), Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)), Constants.Security.SuperUserKey); VerifyIsNotPublished(Textpage.Key); VerifyIsNotPublished(Subpage.Key); @@ -36,7 +36,7 @@ public partial class ContentPublishingServiceTests { await ContentPublishingService.PublishAsync(Textpage.Key, MakeModel(_allCultures), Constants.Security.SuperUserKey); - var result = await ContentPublishingService.PublishAsync(Subpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.Now.AddDays(1), null)), Constants.Security.SuperUserKey); + var result = await ContentPublishingService.PublishAsync(Subpage.Key, MakeModel(ContentScheduleCollection.CreateWithEntry("*", DateTime.UtcNow.AddDays(1), null)), Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); Assert.AreEqual(ContentPublishingOperationStatus.Success, result.Status); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index b6b7f2cb64..372a220c23 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -968,7 +968,7 @@ public class EntityServiceTests : UmbracoIntegrationTest // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 _subpage = ContentBuilder.CreateSimpleContent(_contentType, "Text Page 1", _textpage.Id); - var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null); + var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-5), null); ContentService.Save(_subpage, -1, contentSchedule); // Create and Save Content "Text Page 2" based on "umbTextpage" -> 1055 diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.GetStatus.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.GetStatus.cs new file mode 100644 index 0000000000..1f2600299e --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.GetStatus.cs @@ -0,0 +1,136 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; + +public partial class ContentExtensionsTests +{ + [Test] + public void GetStatus_WhenTrashed_ReturnsTrashed() + { + var contentMock = new Mock(); + contentMock.SetupGet(c => c.Trashed).Returns(true); + var result = contentMock.Object.GetStatus(new ContentScheduleCollection()); + Assert.AreEqual(ContentStatus.Trashed, result); + } + + [TestCase(true, ContentStatus.Published)] + [TestCase(false, ContentStatus.Unpublished)] + public void GetStatus_WithEmptySchedule_ReturnsPublishState(bool published, ContentStatus expectedStatus) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + mock.SetupGet(c => c.Published).Returns(published); + + var result = mock.Object.GetStatus(new ContentScheduleCollection()); + Assert.AreEqual(expectedStatus, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPendingExpiry_ForInvariant_ReturnsExpired(int minutesFromExpiry) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + + var schedule = ContentScheduleCollection.CreateWithEntry(null, DateTime.UtcNow.AddMinutes(-1 * minutesFromExpiry)); + var result = mock.Object.GetStatus(schedule); + Assert.AreEqual(ContentStatus.Expired, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPendingRelease_ForInvariant_ReturnsAwaitingRelease(int minutesUntilRelease) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + + var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(minutesUntilRelease), null); + var result = mock.Object.GetStatus(schedule); + Assert.AreEqual(ContentStatus.AwaitingRelease, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPastReleaseAndFutureExpiry_ForInvariant_ReturnsPublishedState(int minutes) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + mock.SetupGet(c => c.Published).Returns(true); + + var schedule = ContentScheduleCollection.CreateWithEntry(DateTime.UtcNow.AddMinutes(-1 * minutes), DateTime.UtcNow.AddMinutes(minutes)); + var result = mock.Object.GetStatus(schedule); + Assert.AreEqual(ContentStatus.Published, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPendingExpiry_ForVariant_ReturnsExpired(int minutesFromExpiry) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + + var schedule = ContentScheduleCollection.CreateWithEntry("en-US", null, DateTime.UtcNow.AddMinutes(-1 * minutesFromExpiry)); + var result = mock.Object.GetStatus(schedule, "en-US"); + Assert.AreEqual(ContentStatus.Expired, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPendingRelease_ForVariant_ReturnsAwaitingRelease(int minutesUntilRelease) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + + var schedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddMinutes(minutesUntilRelease), null); + var result = mock.Object.GetStatus(schedule, "en-US"); + Assert.AreEqual(ContentStatus.AwaitingRelease, result); + } + + [TestCase(1)] + [TestCase(10)] + [TestCase(60)] + [TestCase(120)] + [TestCase(1000)] + public void GetStatus_WithPastReleaseAndFutureExpiry_ForVariant_ReturnsPublishedState(int minutes) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + var mock = new Mock(); + mock.SetupGet(c => c.ContentType).Returns(contentTypeMock.Object); + mock.SetupGet(c => c.Published).Returns(true); + + var schedule = ContentScheduleCollection.CreateWithEntry("en-US", DateTime.UtcNow.AddMinutes(-1 * minutes), DateTime.UtcNow.AddMinutes(minutes)); + var result = mock.Object.GetStatus(schedule, "en-US"); + Assert.AreEqual(ContentStatus.Published, result); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.cs new file mode 100644 index 0000000000..3ffc906a05 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ContentExtensionsTests.cs @@ -0,0 +1,8 @@ +using NUnit.Framework; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions; + +[TestFixture] +public partial class ContentExtensionsTests +{ +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 0a1c88c5b8..bc6314297f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -46,5 +46,8 @@ BackOfficeExternalLoginServiceTests.cs + + ContentExtensionsTests.cs + From a9fc88d7e6d558dfbe8e1097c222e3e01e0cefad Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 14 Apr 2025 17:45:57 +0200 Subject: [PATCH 5/9] Make sure not to early return when verifying ancestor path is published without completing scope (#19029) * Make sure not to early return when verifying ancestor path is published without completing scope. * Added comment. --- .../Services/DocumentCacheService.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index 21758a1fcd..f78863e64f 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -115,12 +115,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService // When unpublishing a node, a payload with RefreshBranch is published, so we don't have to worry about this. // Similarly, when a branch is published, next time the content is requested, the parent will be published, // this works because we don't cache null values. - if (preview is false && contentCacheNode is not null) + if (preview is false && contentCacheNode is not null && HasPublishedAncestorPath(contentCacheNode.Key) is false) { - if (HasPublishedAncestorPath(contentCacheNode.Key) is false) - { - return null; - } + // Careful not to early return here. We need to complete the scope even if returning null. + contentCacheNode = null; } scope.Complete(); From 432dda8c47e19c69cfc07878b13797760bb4e7bf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 15 Apr 2025 08:03:10 +0200 Subject: [PATCH 6/9] Adds some missing mime types to ensure uploaded audio and video displays with preview. (#19039) --- .../media/media/components/input-upload-field/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts index 8f65e11141..c566013167 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/utils.ts @@ -461,6 +461,7 @@ export function getMimeTypeFromExtension(extension: string): string | null { '.onetoc2': 'application/onenote', '.opf': 'application/oebps-package+xml', '.oprc': 'application/vnd.palm', + '.opus': 'audio/ogg', '.org': 'application/vnd.lotus-organizer', '.osf': 'application/vnd.yamaha.openscoreformat', '.osfpvg': 'application/vnd.yamaha.openscoreformat.osfpvg+xml', @@ -744,6 +745,8 @@ export function getMimeTypeFromExtension(extension: string): string | null { '.wbxml': 'application/vnd.wap.wbxml', '.wcm': 'application/vnd.ms-works', '.wdb': 'application/vnd.ms-works', + '.weba': 'audio/webm', + '.webm': 'video/webm', '.webp': 'image/webp', '.wiz': 'application/msword', '.wks': 'application/vnd.ms-works', From 981f173a79b9cdc98085605427af23b0176eb6f4 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 15 Apr 2025 08:34:58 +0200 Subject: [PATCH 7/9] Clarified "too many" entries validation message. (#19040) --- src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 2 +- src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml | 2 +- src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 2 +- src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index cf9c546fd3..a45789182a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -377,7 +377,7 @@ Value cannot be empty Value is invalid, it does not match the correct pattern %1% more.]]> - %1% too many.]]> + %1% too many.]]> The string length exceeds the maximum length of %0% characters, %1% too many. The content amount requirements are not met for one or more areas. Invalid member group name diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index cd65b07490..d7a56e1d1d 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -394,7 +394,7 @@ The value %0% is not expected to contain a range The value %0% is not expected to have a to value less than the from value %1% more.]]> - %1% too many.]]> + %1% too many.]]> The string length exceeds the maximum length of %0% characters, %1% too many. The content amount requirements are not met for one or more areas. The chosen media type is invalid. diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 79a42ed9e5..f73909ccc5 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -2097,7 +2097,7 @@ export default { duplicateUsername: "Username '%0%' is already taken", customValidation: 'Custom validation', entriesShort: 'Minimum %0% entries, requires %1% more.', - entriesExceed: 'Maximum %0% entries, %1% too many.', + entriesExceed: 'Maximum %0% entries, you have entered %1% too many.', entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.', }, healthcheck: { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 7c9603f402..9363c1f09a 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2168,7 +2168,7 @@ export default { invalidPattern: 'Value is invalid, it does not match the correct pattern', customValidation: 'Custom validation', entriesShort: 'Minimum %0% entries, requires %1% more.', - entriesExceed: 'Maximum %0% entries, %1% too many.', + entriesExceed: 'Maximum %0% entries, you have entered %1% too many.', entriesAreasMismatch: 'The content amount requirements are not met for one or more areas.', invalidMemberGroupName: 'Invalid member group name', invalidUserGroupName: 'Invalid user group name', From 6edffd9f087cad850405de48b2dace5b830982dc Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:23:37 +0200 Subject: [PATCH 8/9] docs: fix import path --- .../property-editor-ui-stylesheet-picker.stories.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts index 8363c00a0c..f17dfa6919 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts @@ -2,7 +2,7 @@ import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type import { html } from '@umbraco-cms/backoffice/external/lit'; import type { Meta } from '@storybook/web-components'; -import './property-editor-ui-tiny-mce-stylesheets-configuration.element.js'; +import './property-editor-ui-stylesheet-picker.element.js'; import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; const dataTypeData = umbDataTypeMockDb.read('dt-richTextEditor') as unknown as UmbDataTypeDetailModel; @@ -10,7 +10,7 @@ const dataTypeData = umbDataTypeMockDb.read('dt-richTextEditor') as unknown as U export default { title: 'Property Editor UIs/Stylesheet Picker', component: 'umb-property-editor-ui-stylesheet-picker', - id: 'umb-property-editor-ui-sstylesheet-picker', + id: 'umb-property-editor-ui-stylesheet-picker', } as Meta; export const AAAOverview = ({ value }: any) => From cc9c33bfe63a7c0b873357f43b305e4ec28ed106 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 15 Apr 2025 12:57:57 +0200 Subject: [PATCH 9/9] fix: adds missing export --- .../src/packages/core/validation/context/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts index 5df50e5c85..d27015417a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/index.ts @@ -1,4 +1,5 @@ export * from './validation.context.js'; export * from './validation.context-token.js'; +export * from './validation-messages.manager.js'; export * from './server-model-validator.context-token.js'; export * from './server-model-validator.context.js';