From 77dbaf03f21e0bf3be80d33d7d359e640a73a34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Krch?= Date: Mon, 21 Oct 2024 16:32:19 +0200 Subject: [PATCH 1/2] #239 ability to create custom migrations for widget properties --- .../Handlers/MigratePagesCommandHandler.cs | 2 +- .../Helpers/PageBuilderWidgetsPatcher.cs | 19 +- .../KsCoreDiExtensions.cs | 1 + .../Mappers/ContentItemMapper.cs | 270 +----------- .../PageTemplateConfigurationMapper.cs | 272 +------------ .../Services/PageBuilderPatcher.cs | 262 ++++++++++++ .../Model/EditableAreasConfiguration.cs | 383 +++++++++--------- .../DefaultMigrations/WidgetFileMigration.cs | 73 ++++ .../WidgetPageSelectorMigration.cs | 36 ++ .../WidgetPathSelectorMigration.cs | 33 ++ Migration.Tool.Extensions/README.md | 18 +- .../ServiceCollectionExtensions.cs | 6 +- .../DependencyInjectionExtensions.cs | 3 +- .../Services/CmsClass/IWidgetMigration.cs | 18 + .../CmsClass/WidgetMigrationService.cs | 19 + README.md | 3 +- 16 files changed, 683 insertions(+), 735 deletions(-) create mode 100644 KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs rename {KVA/Migration.Tool.Source/Services => Migration.Tool.Common}/Model/EditableAreasConfiguration.cs (95%) create mode 100644 Migration.Tool.Extensions/DefaultMigrations/WidgetFileMigration.cs create mode 100644 Migration.Tool.Extensions/DefaultMigrations/WidgetPageSelectorMigration.cs create mode 100644 Migration.Tool.Extensions/DefaultMigrations/WidgetPathSelectorMigration.cs create mode 100644 Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs create mode 100644 Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 9303b33f..66f3b4b8 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -22,6 +22,7 @@ using Migration.Tool.Common.Abstractions; using Migration.Tool.Common.Helpers; using Migration.Tool.Common.MigrationProtocol; +using Migration.Tool.Common.Model; using Migration.Tool.KXP.Models; using Migration.Tool.Source.Contexts; using Migration.Tool.Source.Helpers; @@ -29,7 +30,6 @@ using Migration.Tool.Source.Model; using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; -using Migration.Tool.Source.Services.Model; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs b/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs index a818e8dd..29b44d5a 100644 --- a/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs +++ b/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs @@ -1,6 +1,5 @@ using Migration.Tool.Common.Helpers; -using Migration.Tool.Source.Services.Model; - +using Migration.Tool.Common.Model; using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Helpers; @@ -28,7 +27,7 @@ public static EditableAreasConfiguration DeferredPatchConfiguration(EditableArea return configuration; } - private static void DeferredPatchWidget(WidgetConfiguration configurationZoneWidget, TreePathConvertor convertor, out bool anythingChanged) + private static void DeferredPatchWidget(WidgetConfiguration? configurationZoneWidget, TreePathConvertor convertor, out bool anythingChanged) { anythingChanged = false; if (configurationZoneWidget == null) @@ -39,11 +38,14 @@ private static void DeferredPatchWidget(WidgetConfiguration configurationZoneWid var list = configurationZoneWidget.Variants ?? []; for (int i = 0; i < list.Count; i++) { - var variant = JObject.FromObject(list[i]); - DeferredPatchProperties(variant, convertor, out bool anythingChangedTmp); + if (list[i] is { } variantJson) + { + var variant = JObject.FromObject(variantJson); + DeferredPatchProperties(variant, convertor, out bool anythingChangedTmp); - list[i] = variant.ToObject(); - anythingChanged = anythingChanged || anythingChangedTmp; + list[i] = variant.ToObject(); + anythingChanged = anythingChanged || anythingChangedTmp; + } } } @@ -56,9 +58,8 @@ public static void DeferredPatchProperties(JObject propertyContainer, TreePathCo { switch (key) { - case "TreePath": + case "TreePath" when value?.Value() is { } nodeAliasPath: { - string? nodeAliasPath = value?.Value(); string treePath = convertor.GetConvertedOrUnchangedAssumingChannel(nodeAliasPath); if (!TreePathConvertor.TreePathComparer.Equals(nodeAliasPath, treePath)) { diff --git a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs index a7f199be..244bfac3 100644 --- a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs +++ b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs @@ -54,6 +54,7 @@ public static IServiceCollection UseKsToolCore(this IServiceCollection services, services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index f431de27..399ac595 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -5,20 +5,15 @@ using CMS.Core.Internal; using CMS.DataEngine; using CMS.FormEngine; -using CMS.MediaLibrary; using CMS.Websites; using CMS.Websites.Internal; using Kentico.Xperience.UMT.Model; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; using Migration.Tool.Common.Builders; -using Migration.Tool.Common.Enumerations; using Migration.Tool.Common.Helpers; using Migration.Tool.Common.Services; -using Migration.Tool.Common.Services.Ipc; -using Migration.Tool.KXP.Api; using Migration.Tool.KXP.Api.Auxiliary; using Migration.Tool.KXP.Api.Services.CmsClass; using Migration.Tool.Source.Auxiliary; @@ -27,8 +22,6 @@ using Migration.Tool.Source.Model; using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; -using Migration.Tool.Source.Services.Model; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Mappers; @@ -48,22 +41,19 @@ ICmsSite SourceSite public class ContentItemMapper( ILogger logger, CoupledDataService coupledDataService, - ClassService classService, IAttachmentMigrator attachmentMigrator, CmsRelationshipService relationshipService, - SourceInstanceContext sourceInstanceContext, FieldMigrationService fieldMigrationService, - KxpMediaFileFacade mediaFileFacade, ModelFacade modelFacade, ReusableSchemaService reusableSchemaService, DeferredPathService deferredPathService, SpoiledGuidContext spoiledGuidContext, - EntityIdentityFacade entityIdentityFacade, IAssetFacade assetFacade, MediaLinkServiceFactory mediaLinkServiceFactory, ToolConfiguration configuration, - ClassMappingProvider classMappingProvider -) : UmtMapperBase + ClassMappingProvider classMappingProvider, + PageBuilderPatcher pageBuilderPatcher + ) : UmtMapperBase { private const string CLASS_FIELD_CONTROL_NAME = "controlname"; @@ -220,7 +210,7 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source break; } - PatchJsonDefinitions(source.CmsTree.NodeSiteID, ref contentItemCommonDataPageTemplateConfiguration, ref contentItemCommonDataPageBuilderWidgets, out ndp); + (contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets, ndp) = pageBuilderPatcher.PatchJsonDefinitions(source.CmsTree.NodeSiteID, contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets).GetAwaiter().GetResult(); } var documentGuid = spoiledGuidContext.EnsureDocumentGuid( @@ -372,44 +362,6 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source }; } - private void PatchJsonDefinitions(int sourceSiteId, ref string? pageTemplateConfiguration, ref string? pageBuilderWidgets, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - if (sourceInstanceContext.HasInfo) - { - if (pageTemplateConfiguration != null) - { - var pageTemplateConfigurationObj = JsonConvert.DeserializeObject(pageTemplateConfiguration); - if (pageTemplateConfigurationObj?.Identifier != null) - { - logger.LogTrace("Walk page template configuration {Identifier}", pageTemplateConfigurationObj.Identifier); - - var pageTemplateConfigurationFcs = - sourceInstanceContext.GetPageTemplateFormComponents(sourceSiteId, pageTemplateConfigurationObj?.Identifier); - if (pageTemplateConfigurationObj.Properties is { Count: > 0 }) - { - WalkProperties(sourceSiteId, pageTemplateConfigurationObj.Properties, pageTemplateConfigurationFcs, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - - pageTemplateConfiguration = JsonConvert.SerializeObject(pageTemplateConfigurationObj); - } - } - - if (pageBuilderWidgets != null) - { - var areas = JsonConvert.DeserializeObject(pageBuilderWidgets); - if (areas?.EditableAreas is { Count: > 0 }) - { - WalkAreas(sourceSiteId, areas.EditableAreas, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - - pageBuilderWidgets = JsonConvert.SerializeObject(areas); - } - } - } - private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, ICmsTree cmsTree, string sourceFormClassDefinition, string targetFormDefinition, Guid contentItemGuid, Guid contentLanguageGuid, ICmsClass sourceNodeClass, WebsiteChannelInfo websiteChannelInfo, ICmsSite sourceSite, IClassMapping mapping) { @@ -421,7 +373,7 @@ private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, { string? pageTemplateConfiguration = adapter.DocumentPageTemplateConfiguration; string? pageBuildWidgets = adapter.DocumentPageBuilderWidgets; - PatchJsonDefinitions(checkoutVersion.NodeSiteID, ref pageTemplateConfiguration, ref pageBuildWidgets, out bool ndp); + (pageTemplateConfiguration, pageBuildWidgets, bool ndp) = pageBuilderPatcher.PatchJsonDefinitions(checkoutVersion.NodeSiteID, pageTemplateConfiguration, pageBuildWidgets).GetAwaiter().GetResult(); #region Find existing guid @@ -778,217 +730,5 @@ private static IEnumerable UnpackReusableFieldSchemas(IEnumerable } } - #region "Page template & page widget walkers" - - private void WalkAreas(int siteId, List areas, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - foreach (var area in areas) - { - logger.LogTrace("Walk area {Identifier}", area.Identifier); - - if (area.Sections is { Count: > 0 }) - { - WalkSections(siteId, area.Sections, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - } - } - - private void WalkSections(int siteId, List sections, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - foreach (var section in sections) - { - logger.LogTrace("Walk section {TypeIdentifier}|{Identifier}", section.TypeIdentifier, section.Identifier); - - var sectionFcs = sourceInstanceContext.GetSectionFormComponents(siteId, section.TypeIdentifier); - WalkProperties(siteId, section.Properties, sectionFcs, out bool ndp1); - needsDeferredPatch = ndp1 || needsDeferredPatch; - - if (section.Zones is { Count: > 0 }) - { - WalkZones(siteId, section.Zones, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - } - } - - private void WalkZones(int siteId, List zones, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - foreach (var zone in zones) - { - logger.LogTrace("Walk zone {Name}|{Identifier}", zone.Name, zone.Identifier); - - if (zone.Widgets is { Count: > 0 }) - { - WalkWidgets(siteId, zone.Widgets, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - } - } - - private void WalkWidgets(int siteId, List widgets, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - foreach (var widget in widgets) - { - logger.LogTrace("Walk widget {TypeIdentifier}|{Identifier}", widget.TypeIdentifier, widget.Identifier); - - var widgetFcs = sourceInstanceContext.GetWidgetPropertyFormComponents(siteId, widget.TypeIdentifier); - foreach (var variant in widget.Variants) - { - logger.LogTrace("Walk widget variant {Name}|{Identifier}", variant.Name, variant.Identifier); - - if (variant.Properties is { Count: > 0 }) - { - WalkProperties(siteId, variant.Properties, widgetFcs, out bool ndp); - needsDeferredPatch = ndp || needsDeferredPatch; - } - } - } - } - - private void WalkProperties(int siteId, JObject properties, List? formControlModels, out bool needsDeferredPatch) - { - needsDeferredPatch = false; - foreach ((string key, var value) in properties) - { - logger.LogTrace("Walk property {Name}|{Identifier}", key, value?.ToString()); - - var editingFcm = formControlModels?.FirstOrDefault(x => x.PropertyName.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - if (editingFcm != null) - { - if (FieldMappingInstance.BuiltInModel.NotSupportedInKxpLegacyMode - .SingleOrDefault(x => x.OldFormComponent == editingFcm.FormComponentIdentifier) is var (oldFormComponent, newFormComponent)) - { - logger.LogTrace("Editing form component found {FormComponentName} => no longer supported {Replacement}", editingFcm.FormComponentIdentifier, newFormComponent); - - switch (oldFormComponent) - { - case Kx13FormComponents.Kentico_MediaFilesSelector: - { - var mfis = new List(); - if (value?.ToObject>() is { Count: > 0 } items) - { - foreach (var mfsi in items) - { - if (configuration.MigrateMediaToMediaLibrary) - { - if (entityIdentityFacade.Translate(mfsi.FileGuid, siteId) is { } mf && mediaFileFacade.GetMediaFile(mf.Identity) is { } mfi) - { - mfis.Add(new Kentico.Components.Web.Mvc.FormComponents.MediaFilesSelectorItem { FileGuid = mfi.FileGUID }); - } - } - else - { - var sourceMediaFile = modelFacade.SelectWhere("FileGUID = @mediaFileGuid AND FileSiteID = @fileSiteID", new SqlParameter("mediaFileGuid", mfsi.FileGuid), new SqlParameter("fileSiteID", siteId)) - .FirstOrDefault(); - if (sourceMediaFile != null) - { - var (ownerContentItemGuid, _) = assetFacade.GetRef(sourceMediaFile); - mfis.Add(new ContentItemReference { Identifier = ownerContentItemGuid }); - } - } - } - - - properties[key] = JToken.FromObject(items.Select(x => new Kentico.Components.Web.Mvc.FormComponents.MediaFilesSelectorItem { FileGuid = entityIdentityFacade.Translate(x.FileGuid, siteId).Identity }) - .ToList()); - } - - break; - } - case Kx13FormComponents.Kentico_PathSelector: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - properties[key] = JToken.FromObject(items.Select(x => new Kentico.Components.Web.Mvc.FormComponents.PathSelectorItem { TreePath = x.NodeAliasPath }).ToList()); - } - - break; - } - case Kx13FormComponents.Kentico_AttachmentSelector when newFormComponent == FormComponents.AdminAssetSelectorComponent: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - var nv = new List(); - foreach (var asi in items) - { - var attachment = modelFacade.SelectWhere("AttachmentSiteID = @attachmentSiteId AND AttachmentGUID = @attachmentGUID", - new SqlParameter("attachmentSiteID", siteId), - new SqlParameter("attachmentGUID", asi.FileGuid) - ) - .FirstOrDefault(); - if (attachment != null) - { - switch (attachmentMigrator.MigrateAttachment(attachment).GetAwaiter().GetResult()) - { - case MigrateAttachmentResultMediaFile { Success: true, MediaFileInfo: { } x }: - { - nv.Add(new AssetRelatedItem { Identifier = x.FileGUID, Dimensions = new AssetDimensions { Height = x.FileImageHeight, Width = x.FileImageWidth }, Name = x.FileName, Size = x.FileSize }); - break; - } - case MigrateAttachmentResultContentItem { Success: true, ContentItemGuid: { } contentItemGuid }: - { - nv.Add(new ContentItemReference { Identifier = contentItemGuid }); - break; - } - default: - { - logger.LogWarning("Attachment '{AttachmentGUID}' failed to migrate", asi.FileGuid); - break; - } - } - } - else - { - logger.LogWarning("Attachment '{AttachmentGUID}' not found", asi.FileGuid); - } - } - - properties[key] = JToken.FromObject(nv); - } - - logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); - break; - } - case Kx13FormComponents.Kentico_PageSelector when newFormComponent == FormComponents.Kentico_Xperience_Admin_Websites_WebPageSelectorComponent: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - properties[key] = JToken.FromObject(items.Select(x => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(x.NodeGuid, siteId) }).ToList()); - } - - logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); - break; - } - - default: - break; - } - } - else if (FieldMappingInstance.BuiltInModel.SupportedInKxpLegacyMode.Contains(editingFcm.FormComponentIdentifier)) - { - // OK - logger.LogTrace("Editing form component found {FormComponentName} => supported in legacy mode", editingFcm.FormComponentIdentifier); - } - else - { - // unknown control, probably custom - logger.LogTrace("Editing form component found {FormComponentName} => custom or inlined component, don't forget to migrate code accordingly", editingFcm.FormComponentIdentifier); - } - } - - if ("NodeAliasPath".Equals(key, StringComparison.InvariantCultureIgnoreCase)) - { - needsDeferredPatch = true; - properties["TreePath"] = value; - properties.Remove(key); - } - } - } - #endregion } diff --git a/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs b/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs index 077007f0..2fe610d2 100644 --- a/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs @@ -1,24 +1,10 @@ -using AngleSharp.Text; -using CMS.ContentEngine; -using CMS.MediaLibrary; using CMS.Websites; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; -using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.Enumerations; using Migration.Tool.Common.MigrationProtocol; -using Migration.Tool.Common.Services.Ipc; -using Migration.Tool.KXP.Api; -using Migration.Tool.KXP.Api.Auxiliary; -using Migration.Tool.KXP.Api.Services.CmsClass; -using Migration.Tool.Source.Auxiliary; using Migration.Tool.Source.Contexts; using Migration.Tool.Source.Model; using Migration.Tool.Source.Services; -using Migration.Tool.Source.Services.Model; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Mappers; @@ -26,15 +12,8 @@ public class PageTemplateConfigurationMapper( ILogger logger, PrimaryKeyMappingContext pkContext, IProtocol protocol, - SourceInstanceContext sourceInstanceContext, - EntityIdentityFacade entityIdentityFacade, - SpoiledGuidContext spoiledGuidContext, - ModelFacade modelFacade, - IAttachmentMigrator attachmentMigrator, - IAssetFacade assetFacade, - ToolConfiguration configuration, - KxpMediaFileFacade mediaFileFacade - ) + PageBuilderPatcher pageBuilderPatcher +) : EntityMapperBase(logger, pkContext, protocol) { protected override PageTemplateConfigurationInfo? CreateNewInstance(ICmsPageTemplateConfiguration source, MappingHelper mappingHelper, AddFailure addFailure) @@ -61,253 +40,18 @@ protected override PageTemplateConfigurationInfo MapInternal(ICmsPageTemplateCon target.PageTemplateConfigurationGUID = source.PageTemplateConfigurationGUID; } - if (sourceInstanceContext.HasInfo) - { - if (source.PageTemplateConfigurationTemplate != null) - { - var pageTemplateConfiguration = JsonConvert.DeserializeObject(source.PageTemplateConfigurationTemplate); - if (pageTemplateConfiguration?.Identifier != null) - { - logger.LogTrace("Walk page template configuration {Identifier}", pageTemplateConfiguration.Identifier); - - - var pageTemplateConfigurationFcs = - sourceInstanceContext.GetPageTemplateFormComponents(source.PageTemplateConfigurationSiteID, pageTemplateConfiguration.Identifier); - if (pageTemplateConfiguration.Properties is { Count: > 0 }) - { - WalkProperties(source.PageTemplateConfigurationSiteID, pageTemplateConfiguration.Properties, pageTemplateConfigurationFcs); - } - - target.PageTemplateConfigurationTemplate = JsonConvert.SerializeObject(pageTemplateConfiguration); - } - } + // bool needsDeferredPatch = false; + string? configurationTemplate = source.PageTemplateConfigurationTemplate; + string? configurationWidgets = source.PageTemplateConfigurationWidgets; - if (source.PageTemplateConfigurationWidgets != null) - { - var areas = JsonConvert.DeserializeObject(source.PageTemplateConfigurationWidgets); - if (areas?.EditableAreas is { Count: > 0 }) - { - WalkAreas(source.PageTemplateConfigurationSiteID, areas.EditableAreas); - } + (configurationTemplate, configurationWidgets, bool _) = pageBuilderPatcher.PatchJsonDefinitions(source.PageTemplateConfigurationSiteID, configurationTemplate, configurationWidgets).GetAwaiter().GetResult(); - target.PageTemplateConfigurationWidgets = JsonConvert.SerializeObject(areas); - } - } - else - { - // simply copy if no info is available - target.PageTemplateConfigurationTemplate = source.PageTemplateConfigurationTemplate; - target.PageTemplateConfigurationWidgets = source.PageTemplateConfigurationWidgets; - } + target.PageTemplateConfigurationTemplate = configurationTemplate; + target.PageTemplateConfigurationWidgets = configurationWidgets; return target; } return null; } - - #region "Page template & page widget walkers" - - private void WalkAreas(int siteId, List areas) - { - foreach (var area in areas) - { - logger.LogTrace("Walk area {Identifier}", area.Identifier); - - if (area.Sections is { Count: > 0 }) - { - WalkSections(siteId, area.Sections); - } - } - } - - private void WalkSections(int siteId, List sections) - { - foreach (var section in sections) - { - logger.LogTrace("Walk section {TypeIdentifier}|{Identifier}", section.TypeIdentifier, section.Identifier); - - var sectionFcs = sourceInstanceContext.GetSectionFormComponents(siteId, section.TypeIdentifier); - WalkProperties(siteId, section.Properties, sectionFcs); - - if (section.Zones is { Count: > 0 }) - { - WalkZones(siteId, section.Zones); - } - } - } - - private void WalkZones(int siteId, List zones) - { - foreach (var zone in zones) - { - logger.LogTrace("Walk zone {Name}|{Identifier}", zone.Name, zone.Identifier); - - if (zone.Widgets is { Count: > 0 }) - { - WalkWidgets(siteId, zone.Widgets); - } - } - } - - private void WalkWidgets(int siteId, List widgets) - { - foreach (var widget in widgets) - { - logger.LogTrace("Walk widget {TypeIdentifier}|{Identifier}", widget.TypeIdentifier, widget.Identifier); - - var widgetFcs = sourceInstanceContext.GetWidgetPropertyFormComponents(siteId, widget.TypeIdentifier); - foreach (var variant in widget.Variants) - { - logger.LogTrace("Walk widget variant {Name}|{Identifier}", variant.Name, variant.Identifier); - - if (variant.Properties is { Count: > 0 }) - { - WalkProperties(siteId, variant.Properties, widgetFcs); - } - } - } - } - - private void WalkProperties(int siteId, JObject properties, List? formControlModels) - { - foreach ((string key, var value) in properties) - { - logger.LogTrace("Walk property {Name}|{Identifier}", key, value?.ToString()); - - var editingFcm = formControlModels?.FirstOrDefault(x => x.PropertyName.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - if (editingFcm != null) - { - if (FieldMappingInstance.BuiltInModel.NotSupportedInKxpLegacyMode - .SingleOrDefault(x => x.OldFormComponent == editingFcm.FormComponentIdentifier) is var (oldFormComponent, newFormComponent)) - { - Protocol.Append(HandbookReferences.FormComponentNotSupportedInLegacyMode(oldFormComponent, newFormComponent)); - logger.LogTrace("Editing form component found {FormComponentName} => no longer supported {Replacement}", - editingFcm.FormComponentIdentifier, newFormComponent); - - switch (oldFormComponent) - { - case Kx13FormComponents.Kentico_MediaFilesSelector: - { - var mfis = new List(); - if (value?.ToObject>() is { Count: > 0 } items) - { - foreach (var mfsi in items) - { - if (configuration.MigrateMediaToMediaLibrary) - { - if (entityIdentityFacade.Translate(mfsi.FileGuid, siteId) is { } mf && mediaFileFacade.GetMediaFile(mf.Identity) is { } mfi) - { - mfis.Add(new Kentico.Components.Web.Mvc.FormComponents.MediaFilesSelectorItem { FileGuid = mfi.FileGUID }); - } - } - else - { - var sourceMediaFile = modelFacade.SelectWhere("FileGUID = @mediaFileGuid AND FileSiteID = @fileSiteID", new SqlParameter("mediaFileGuid", mfsi.FileGuid), new SqlParameter("fileSiteID", siteId)) - .FirstOrDefault(); - if (sourceMediaFile != null) - { - var (ownerContentItemGuid, _) = assetFacade.GetRef(sourceMediaFile); - mfis.Add(new ContentItemReference { Identifier = ownerContentItemGuid }); - } - } - } - - - properties[key] = JToken.FromObject(items.Select(x => new Kentico.Components.Web.Mvc.FormComponents.MediaFilesSelectorItem { FileGuid = entityIdentityFacade.Translate(x.FileGuid, siteId).Identity }) - .ToList()); - } - - break; - } - case Kx13FormComponents.Kentico_PathSelector: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - properties[key] = JToken.FromObject(items.Select(x => new Kentico.Components.Web.Mvc.FormComponents.PathSelectorItem { TreePath = x.NodeAliasPath }).ToList()); - } - - break; - } - case Kx13FormComponents.Kentico_AttachmentSelector when newFormComponent == FormComponents.AdminAssetSelectorComponent: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - var nv = new List(); - foreach (var asi in items) - { - var attachment = modelFacade.SelectWhere("AttachmentSiteID = @attachmentSiteID AND AttachmentGUID = @attachmentGUID", new SqlParameter("attachmentSiteID", siteId), - new SqlParameter("attachmentGUID", asi.FileGuid)) - .FirstOrDefault(); - if (attachment != null) - { - switch (attachmentMigrator.MigrateAttachment(attachment).GetAwaiter().GetResult()) - { - case MigrateAttachmentResultMediaFile { Success: true, MediaFileInfo: { } x }: - { - nv.Add(new AssetRelatedItem { Identifier = x.FileGUID, Dimensions = new AssetDimensions { Height = x.FileImageHeight, Width = x.FileImageWidth }, Name = x.FileName, Size = x.FileSize }); - break; - } - case MigrateAttachmentResultContentItem { Success: true, ContentItemGuid: { } contentItemGuid }: - { - nv.Add(new ContentItemReference { Identifier = contentItemGuid }); - break; - } - default: - { - logger.LogWarning("Attachment '{AttachmentGUID}' failed to migrate", asi.FileGuid); - break; - } - } - } - else - { - logger.LogWarning("Attachment '{AttachmentGUID}' not found", asi.FileGuid); - } - } - - properties[key] = JToken.FromObject(nv); - } - - logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); - break; - } - case Kx13FormComponents.Kentico_PageSelector when newFormComponent == FormComponents.Kentico_Xperience_Admin_Websites_WebPageSelectorComponent: - { - if (value?.ToObject>() is { Count: > 0 } items) - { - properties[key] = JToken.FromObject(items.Select(x => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(x.NodeGuid, siteId) }).ToList()); - } - - logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); - break; - } - case Kx13FormComponents.Kentico_FileUploader: - { - break; - } - - default: - break; - } - } - else if (FieldMappingInstance.BuiltInModel.SupportedInKxpLegacyMode.Contains(editingFcm.FormComponentIdentifier)) - { - // OK - logger.LogTrace("Editing form component found {FormComponentName} => supported in legacy mode", - editingFcm.FormComponentIdentifier); - } - else - { - // unknown control, probably custom - Protocol.Append(HandbookReferences.FormComponentCustom(editingFcm.FormComponentIdentifier)); - logger.LogTrace( - "Editing form component found {FormComponentName} => custom or inlined component, don't forget to migrate code accordingly", - editingFcm.FormComponentIdentifier); - } - } - } - } - - #endregion } diff --git a/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs new file mode 100644 index 00000000..1c3c064c --- /dev/null +++ b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs @@ -0,0 +1,262 @@ +using CMS.ContentEngine; +using CMS.MediaLibrary; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Enumerations; +using Migration.Tool.Common.Model; +using Migration.Tool.Common.Services.Ipc; +using Migration.Tool.KXP.Api.Auxiliary; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source.Contexts; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Services.Model; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Source.Services; + +public record PageBuilderPatchResult(string? Configuration, string? Widgets, bool NeedsDeferredPatch); + +public class PageBuilderPatcher( + ILogger logger, + SourceInstanceContext sourceInstanceContext, + WidgetMigrationService widgetMigrationService, + ModelFacade modelFacade, + IAttachmentMigrator attachmentMigrator +) +{ + public async Task PatchJsonDefinitions(int sourceSiteId, string? pageTemplateConfiguration, string? pageBuilderWidgets) + { + bool needsDeferredPatch = false; + if (sourceInstanceContext.HasInfo) + { + if (pageTemplateConfiguration != null) + { + var pageTemplateConfigurationObj = JsonConvert.DeserializeObject(pageTemplateConfiguration); + if (pageTemplateConfigurationObj?.Identifier != null) + { + logger.LogTrace("Walk page template configuration {Identifier}", pageTemplateConfigurationObj.Identifier); + + var pageTemplateConfigurationFcs = + sourceInstanceContext.GetPageTemplateFormComponents(sourceSiteId, pageTemplateConfigurationObj.Identifier); + if (pageTemplateConfigurationObj.Properties is { Count: > 0 }) + { + bool ndp = await MigrateProperties(sourceSiteId, pageTemplateConfigurationObj.Properties, pageTemplateConfigurationFcs); + needsDeferredPatch = ndp || needsDeferredPatch; + } + + pageTemplateConfiguration = JsonConvert.SerializeObject(pageTemplateConfigurationObj); + } + } + + if (pageBuilderWidgets != null) + { + var areas = JsonConvert.DeserializeObject(pageBuilderWidgets); + if (areas?.EditableAreas is { Count: > 0 }) + { + bool ndp = await WalkAreas(sourceSiteId, areas.EditableAreas); + needsDeferredPatch = ndp || needsDeferredPatch; + } + + pageBuilderWidgets = JsonConvert.SerializeObject(areas); + } + } + + return new PageBuilderPatchResult(pageTemplateConfiguration, pageBuilderWidgets, needsDeferredPatch); + } + + #region "Page template & page widget walkers" + + private async Task WalkAreas(int siteId, List areas) + { + bool needsDeferredPatch = false; + foreach (var area in areas) + { + logger.LogTrace("Walk area {Identifier}", area.Identifier); + + if (area.Sections is { Count: > 0 }) + { + bool ndp = await WalkSections(siteId, area.Sections); + needsDeferredPatch = ndp || needsDeferredPatch; + } + } + + return needsDeferredPatch; + } + + private async Task WalkSections(int siteId, List sections) + { + bool needsDeferredPatch = false; + foreach (var section in sections) + { + logger.LogTrace("Walk section {TypeIdentifier}|{Identifier}", section.TypeIdentifier, section.Identifier); + + var sectionFcs = sourceInstanceContext.GetSectionFormComponents(siteId, section.TypeIdentifier); + bool ndp1 = await MigrateProperties(siteId, section.Properties, sectionFcs); + needsDeferredPatch = ndp1 || needsDeferredPatch; + + if (section.Zones is { Count: > 0 }) + { + bool ndp = await WalkZones(siteId, section.Zones); + needsDeferredPatch = ndp || needsDeferredPatch; + } + } + + return needsDeferredPatch; + } + + private async Task WalkZones(int siteId, List zones) + { + bool needsDeferredPatch = false; + foreach (var zone in zones) + { + logger.LogTrace("Walk zone {Name}|{Identifier}", zone.Name, zone.Identifier); + + if (zone.Widgets is { Count: > 0 }) + { + bool ndp = await WalkWidgets(siteId, zone.Widgets); + needsDeferredPatch = ndp || needsDeferredPatch; + } + } + + return needsDeferredPatch; + } + + private async Task WalkWidgets(int siteId, List widgets) + { + bool needsDeferredPatch = false; + foreach (var widget in widgets) + { + logger.LogTrace("Walk widget {TypeIdentifier}|{Identifier}", widget.TypeIdentifier, widget.Identifier); + var widgetCompos = sourceInstanceContext.GetWidgetPropertyFormComponents(siteId, widget.TypeIdentifier); + + foreach (var variant in widget.Variants) + { + logger.LogTrace("Migrating widget variant {Name}|{Identifier}", variant.Name, variant.Identifier); + + if (variant.Properties is { Count: > 0 } properties) + { + foreach ((string key, var value) in properties) + { + logger.LogTrace("Migrating widget property {Name}|{Identifier}", key, value?.ToString()); + await MigrateProperties(siteId, properties, widgetCompos); + } + } + } + } + + return needsDeferredPatch; + } + + private async Task MigrateProperties(int siteId, JObject properties, List? formControlModels) + { + bool needsDeferredPatch = false; + foreach ((string key, var value) in properties) + { + logger.LogTrace("Walk property {Name}|{Identifier}", key, value?.ToString()); + + var editingFcm = formControlModels?.FirstOrDefault(x => x.PropertyName.Equals(key, StringComparison.InvariantCultureIgnoreCase)); + if (editingFcm != null) + { + var context = new WidgetPropertyMigrationContext(siteId, editingFcm); + var widgetPropertyMigration = widgetMigrationService.GetWidgetPropertyMigrations(context, key); + bool allowDefaultMigrations = true; + bool customMigrationApplied = false; + if (widgetPropertyMigration != null) + { + (var migratedValue, bool ndp, allowDefaultMigrations) = await widgetPropertyMigration.MigrateWidgetProperty(key, value, context); + needsDeferredPatch = ndp || needsDeferredPatch; + properties[key] = migratedValue; + customMigrationApplied = true; + logger.LogTrace("Migration {Migration} applied to {Value}, resulting in {Result}", widgetPropertyMigration.GetType().FullName, value?.ToString() ?? "", migratedValue?.ToString() ?? ""); + } + + if (allowDefaultMigrations) + { + if (FieldMappingInstance.BuiltInModel.NotSupportedInKxpLegacyMode + .SingleOrDefault(x => x.OldFormComponent == editingFcm.FormComponentIdentifier) is var (oldFormComponent, newFormComponent)) + { + logger.LogTrace("Editing form component found {FormComponentName} => no longer supported {Replacement}", editingFcm.FormComponentIdentifier, newFormComponent); + + switch (oldFormComponent) + { + case Kx13FormComponents.Kentico_AttachmentSelector when newFormComponent == FormComponents.AdminAssetSelectorComponent: + { + if (value?.ToObject>() is { Count: > 0 } items) + { + var nv = new List(); + foreach (var asi in items) + { + var attachment = modelFacade.SelectWhere("AttachmentSiteID = @attachmentSiteId AND AttachmentGUID = @attachmentGUID", + new SqlParameter("attachmentSiteID", siteId), + new SqlParameter("attachmentGUID", asi.FileGuid) + ) + .FirstOrDefault(); + if (attachment != null) + { + switch (attachmentMigrator.MigrateAttachment(attachment).GetAwaiter().GetResult()) + { + case MigrateAttachmentResultMediaFile { Success: true, MediaFileInfo: { } x }: + { + nv.Add(new AssetRelatedItem { Identifier = x.FileGUID, Dimensions = new AssetDimensions { Height = x.FileImageHeight, Width = x.FileImageWidth }, Name = x.FileName, Size = x.FileSize }); + break; + } + case MigrateAttachmentResultContentItem { Success: true, ContentItemGuid: { } contentItemGuid }: + { + nv.Add(new ContentItemReference { Identifier = contentItemGuid }); + break; + } + default: + { + logger.LogWarning("Attachment '{AttachmentGUID}' failed to migrate", asi.FileGuid); + break; + } + } + } + else + { + logger.LogWarning("Attachment '{AttachmentGUID}' not found", asi.FileGuid); + } + } + + properties[key] = JToken.FromObject(nv); + } + + logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); + break; + } + + default: + break; + } + } + else if (!customMigrationApplied) + { + if (FieldMappingInstance.BuiltInModel.SupportedInKxpLegacyMode.Contains(editingFcm.FormComponentIdentifier)) + { + // OK + logger.LogTrace("Editing form component found {FormComponentName} => supported in legacy mode", editingFcm.FormComponentIdentifier); + } + else + { + // unknown control, probably custom + logger.LogTrace("Editing form component found {FormComponentName} => custom or inlined component, don't forget to migrate code accordingly", editingFcm.FormComponentIdentifier); + } + } + + + if ("NodeAliasPath".Equals(key, StringComparison.InvariantCultureIgnoreCase)) + { + needsDeferredPatch = true; + properties["TreePath"] = value; + properties.Remove(key); + } + } + } + } + + return needsDeferredPatch; + } + + #endregion +} diff --git a/KVA/Migration.Tool.Source/Services/Model/EditableAreasConfiguration.cs b/Migration.Tool.Common/Model/EditableAreasConfiguration.cs similarity index 95% rename from KVA/Migration.Tool.Source/Services/Model/EditableAreasConfiguration.cs rename to Migration.Tool.Common/Model/EditableAreasConfiguration.cs index 3e3aea28..4bc635a0 100644 --- a/KVA/Migration.Tool.Source/Services/Model/EditableAreasConfiguration.cs +++ b/Migration.Tool.Common/Model/EditableAreasConfiguration.cs @@ -1,192 +1,191 @@ -using System.Runtime.Serialization; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Migration.Tool.Source.Services.Model; - -#region Copied from Kentico assembly - -[DataContract(Name = "Configuration", Namespace = "")] -public sealed class EditableAreasConfiguration -{ - /// - /// Creates an instance of class. - /// - public EditableAreasConfiguration() => EditableAreas = []; - - /// Editable areas within the page. - [DataMember] - [JsonProperty("editableAreas")] - public List EditableAreas { get; private set; } -} - -/// -/// Represents configuration of editable area within the instance. -/// -[DataContract(Name = "EditableArea", Namespace = "")] -public sealed class EditableAreaConfiguration -{ - /// - /// Creates an instance of class. - /// - public EditableAreaConfiguration() => Sections = []; - - /// Identifier of the editable area. - [DataMember] - [JsonProperty("identifier")] - public string Identifier { get; set; } - - /// Sections within editable area. - [DataMember] - [JsonProperty("sections")] - public List Sections { get; private set; } - - /// - /// A flag indicating whether the output of the individual widgets within the editable area can be cached. The default - /// value is false. - /// - public bool AllowWidgetOutputCache { get; set; } - - /// - /// An absolute expiration date for the cached output of the individual widgets. - /// - public DateTimeOffset? WidgetOutputCacheExpiresOn { get; set; } - - /// - /// The length of time from the first request to cache the output of the individual widgets. - /// - public TimeSpan? WidgetOutputCacheExpiresAfter { get; set; } - - /// - /// The time after which the cached output of the individual widgets should be evicted if it has not been accessed. - /// - public TimeSpan? WidgetOutputCacheExpiresSliding { get; set; } -} - -/// -/// Represents configuration of section within the -/// instance. -/// -[DataContract(Name = "Section", Namespace = "")] -public sealed class SectionConfiguration -{ - /// - /// Creates an instance of class. - /// - public SectionConfiguration() => Zones = []; - - /// Identifier of the section. - [DataMember] - [JsonProperty("identifier")] - public Guid Identifier { get; set; } - - /// Type section identifier. - [DataMember] - [JsonProperty("type")] - public string TypeIdentifier { get; set; } - - /// Section properties. - [DataMember] - [JsonProperty("properties")] - // public ISectionProperties Properties { get; set; } - public JObject Properties { get; set; } - - /// Zones within the section. - [DataMember] - [JsonProperty("zones")] - public List Zones { get; private set; } -} - -/// -/// Represents the zone within the -/// configuration class. -/// -[DataContract(Name = "Zone", Namespace = "")] -public sealed class ZoneConfiguration -{ - /// - /// Creates an instance of class. - /// - public ZoneConfiguration() => Widgets = []; - - /// Identifier of the widget zone. - [DataMember] - [JsonProperty("identifier")] - public Guid Identifier { get; set; } - - /// Name of the widget zone. - [DataMember] - [JsonProperty("name")] - public string Name { get; set; } - - /// List of widgets within the zone. - [DataMember] - [JsonProperty("widgets")] - public List Widgets { get; private set; } -} - -/// -/// Represents the configuration of a widget within the -/// list. -/// -[DataContract(Name = "Widget", Namespace = "")] -public sealed class WidgetConfiguration -{ - /// - /// Creates an instance of class. - /// - public WidgetConfiguration() => Variants = []; - - /// Identifier of the widget instance. - [DataMember] - [JsonProperty("identifier")] - public Guid Identifier { get; set; } - - /// Type widget identifier. - [DataMember] - [JsonProperty("type")] - public string TypeIdentifier { get; set; } - - /// Personalization condition type identifier. - [DataMember] - [JsonProperty("conditionType")] - public string PersonalizationConditionTypeIdentifier { get; set; } - - /// List of widget variants. - [DataMember] - [JsonProperty("variants")] - public List Variants { get; set; } -} - -/// -/// Represents the configuration variant of a widget within the -/// list. -/// -[DataContract(Name = "Variant", Namespace = "")] -public sealed class WidgetVariantConfiguration -{ - /// Identifier of the variant instance. - [DataMember] - [JsonProperty("identifier")] - public Guid Identifier { get; set; } - - /// Widget variant name. - [DataMember] - [JsonProperty("name")] - public string Name { get; set; } - - /// Widget variant properties. - [DataMember] - [JsonProperty("properties")] - // public IWidgetProperties Properties { get; set; } - public JObject Properties { get; set; } - - /// Widget variant personalization condition type. - /// Only personalization condition type parameters are serialized to JSON. - [DataMember] - [JsonProperty("conditionTypeParameters")] - public JObject PersonalizationConditionType { get; set; } -} - -#endregion +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Common.Model; + +#region Copied from Kentico assembly + +[DataContract(Name = "Configuration", Namespace = "")] +public sealed class EditableAreasConfiguration +{ + /// + /// Creates an instance of class. + /// + public EditableAreasConfiguration() => EditableAreas = []; + + /// Editable areas within the page. + [DataMember] + [JsonProperty("editableAreas")] + public List EditableAreas { get; private set; } +} + +/// +/// Represents configuration of editable area within the instance. +/// +[DataContract(Name = "EditableArea", Namespace = "")] +public sealed class EditableAreaConfiguration +{ + /// + /// Creates an instance of class. + /// + public EditableAreaConfiguration() => Sections = []; + + /// Identifier of the editable area. + [DataMember] + [JsonProperty("identifier")] + public string Identifier { get; set; } + + /// Sections within editable area. + [DataMember] + [JsonProperty("sections")] + public List Sections { get; private set; } + + /// + /// A flag indicating whether the output of the individual widgets within the editable area can be cached. The default + /// value is false. + /// + public bool AllowWidgetOutputCache { get; set; } + + /// + /// An absolute expiration date for the cached output of the individual widgets. + /// + public DateTimeOffset? WidgetOutputCacheExpiresOn { get; set; } + + /// + /// The length of time from the first request to cache the output of the individual widgets. + /// + public TimeSpan? WidgetOutputCacheExpiresAfter { get; set; } + + /// + /// The time after which the cached output of the individual widgets should be evicted if it has not been accessed. + /// + public TimeSpan? WidgetOutputCacheExpiresSliding { get; set; } +} + +/// +/// Represents configuration of section within the +/// instance. +/// +[DataContract(Name = "Section", Namespace = "")] +public sealed class SectionConfiguration +{ + /// + /// Creates an instance of class. + /// + public SectionConfiguration() => Zones = []; + + /// Identifier of the section. + [DataMember] + [JsonProperty("identifier")] + public Guid Identifier { get; set; } + + /// Type section identifier. + [DataMember] + [JsonProperty("type")] + public string TypeIdentifier { get; set; } + + /// Section properties. + [DataMember] + [JsonProperty("properties")] + // public ISectionProperties Properties { get; set; } + public JObject Properties { get; set; } + + /// Zones within the section. + [DataMember] + [JsonProperty("zones")] + public List Zones { get; private set; } +} + +/// +/// Represents the zone within the +/// configuration class. +/// +[DataContract(Name = "Zone", Namespace = "")] +public sealed class ZoneConfiguration +{ + /// + /// Creates an instance of class. + /// + public ZoneConfiguration() => Widgets = []; + + /// Identifier of the widget zone. + [DataMember] + [JsonProperty("identifier")] + public Guid Identifier { get; set; } + + /// Name of the widget zone. + [DataMember] + [JsonProperty("name")] + public string Name { get; set; } + + /// List of widgets within the zone. + [DataMember] + [JsonProperty("widgets")] + public List Widgets { get; private set; } +} + +/// +/// Represents the configuration of a widget within the +/// list. +/// +[DataContract(Name = "Widget", Namespace = "")] +public sealed class WidgetConfiguration +{ + /// + /// Creates an instance of class. + /// + public WidgetConfiguration() => Variants = []; + + /// Identifier of the widget instance. + [DataMember] + [JsonProperty("identifier")] + public Guid Identifier { get; set; } + + /// Type widget identifier. + [DataMember] + [JsonProperty("type")] + public string TypeIdentifier { get; set; } + + /// Personalization condition type identifier. + [DataMember] + [JsonProperty("conditionType")] + public string PersonalizationConditionTypeIdentifier { get; set; } + + /// List of widget variants. + [DataMember] + [JsonProperty("variants")] + public List Variants { get; set; } +} + +/// +/// Represents the configuration variant of a widget within the +/// list. +/// +[DataContract(Name = "Variant", Namespace = "")] +public sealed class WidgetVariantConfiguration +{ + /// Identifier of the variant instance. + [DataMember] + [JsonProperty("identifier")] + public Guid Identifier { get; set; } + + /// Widget variant name. + [DataMember] + [JsonProperty("name")] + public string Name { get; set; } + + /// Widget variant properties. + [DataMember] + [JsonProperty("properties")] + // public IWidgetProperties Properties { get; set; } + public JObject Properties { get; set; } + + /// Widget variant personalization condition type. + /// Only personalization condition type parameters are serialized to JSON. + [DataMember] + [JsonProperty("conditionTypeParameters")] + public JObject PersonalizationConditionType { get; set; } +} + +#endregion diff --git a/Migration.Tool.Extensions/DefaultMigrations/WidgetFileMigration.cs b/Migration.Tool.Extensions/DefaultMigrations/WidgetFileMigration.cs new file mode 100644 index 00000000..fff79f87 --- /dev/null +++ b/Migration.Tool.Extensions/DefaultMigrations/WidgetFileMigration.cs @@ -0,0 +1,73 @@ +using CMS.ContentEngine; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common; +using Migration.Tool.Common.Enumerations; +using Migration.Tool.KXP.Api; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source; +using Migration.Tool.Source.Auxiliary; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Services; +using Migration.Tool.Source.Services.Model; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Extensions.DefaultMigrations; + +public class WidgetFileMigration(ILogger logger, EntityIdentityFacade entityIdentityFacade, ModelFacade modelFacade, IAssetFacade assetFacade, KxpMediaFileFacade mediaFileFacade, ToolConfiguration configuration) : IWidgetPropertyMigration +{ + private const string MigratedComponent = Kx13FormComponents.Kentico_MediaFilesSelector; + + public int Rank => 100_000; + + public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName) + => MigratedComponent.Equals(context.EditingFormControlModel?.FormComponentIdentifier, StringComparison.InvariantCultureIgnoreCase); + + public Task MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context) + { + (int siteId, _) = context; + + var refsToMedia = new List(); + if (value?.ToObject>() is { Count: > 0 } mediaSelectorItems) + { + foreach (var mediaSelectorItem in mediaSelectorItems) + { + if (configuration.MigrateMediaToMediaLibrary) + { + if (entityIdentityFacade.Translate(mediaSelectorItem.FileGuid, siteId) is var (_, identity) && mediaFileFacade.GetMediaFile(identity) is not null) + { + refsToMedia.Add(new Kentico.Components.Web.Mvc.FormComponents.MediaFilesSelectorItem { FileGuid = identity }); + } + else + { + logger.LogError("Media file not found, media guid {MediaGuid}", mediaSelectorItem.FileGuid); + } + } + else + { + var sourceMediaFile = modelFacade.SelectWhere("FileGUID = @mediaFileGuid AND FileSiteID = @fileSiteID", new SqlParameter("mediaFileGuid", mediaSelectorItem.FileGuid), new SqlParameter("fileSiteID", siteId)) + .FirstOrDefault(); + if (sourceMediaFile != null) + { + var (ownerContentItemGuid, _) = assetFacade.GetRef(sourceMediaFile); + refsToMedia.Add(new ContentItemReference { Identifier = ownerContentItemGuid }); + } + else + { + logger.LogError("Media file not found, media guid {MediaGuid}", mediaSelectorItem.FileGuid); + } + } + } + + var resultAsJToken = JToken.FromObject(refsToMedia); + return Task.FromResult(new WidgetPropertyMigrationResult(resultAsJToken)); + } + else + { + logger.LogError("Failed to parse '{ComponentName}' json {Json}", MigratedComponent, value?.ToString() ?? ""); + + // leave value as it is + return Task.FromResult(new WidgetPropertyMigrationResult(value)); + } + } +} diff --git a/Migration.Tool.Extensions/DefaultMigrations/WidgetPageSelectorMigration.cs b/Migration.Tool.Extensions/DefaultMigrations/WidgetPageSelectorMigration.cs new file mode 100644 index 00000000..ee31d85e --- /dev/null +++ b/Migration.Tool.Extensions/DefaultMigrations/WidgetPageSelectorMigration.cs @@ -0,0 +1,36 @@ +using CMS.Websites; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Enumerations; +using Migration.Tool.Common.Services; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source.Services.Model; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Extensions.DefaultMigrations; + +public class WidgetPageSelectorMigration(ILogger logger, ISpoiledGuidContext spoiledGuidContext) : IWidgetPropertyMigration +{ + private const string MigratedComponent = Kx13FormComponents.Kentico_PageSelector; + + public int Rank => 100_002; + public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName) + => MigratedComponent.Equals(context.EditingFormControlModel?.FormComponentIdentifier, StringComparison.InvariantCultureIgnoreCase); + + public Task MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context) + { + (int siteId, _) = context; + if (value?.ToObject>() is { Count: > 0 } items) + { + var result = items.Select(x => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(x.NodeGuid, siteId) }).ToList(); + var resultAsJToken = JToken.FromObject(result); + return Task.FromResult(new WidgetPropertyMigrationResult(resultAsJToken)); + } + else + { + logger.LogError("Failed to parse '{ComponentName}' json {Json}", MigratedComponent, value?.ToString() ?? ""); + + // leave value as it is + return Task.FromResult(new WidgetPropertyMigrationResult(value)); + } + } +} diff --git a/Migration.Tool.Extensions/DefaultMigrations/WidgetPathSelectorMigration.cs b/Migration.Tool.Extensions/DefaultMigrations/WidgetPathSelectorMigration.cs new file mode 100644 index 00000000..8a40c154 --- /dev/null +++ b/Migration.Tool.Extensions/DefaultMigrations/WidgetPathSelectorMigration.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Enumerations; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source.Services.Model; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Extensions.DefaultMigrations; + +public class WidgetPathSelectorMigration(ILogger logger) : IWidgetPropertyMigration +{ + private const string MigratedComponent = Kx13FormComponents.Kentico_PathSelector; + + public int Rank => 100_001; + public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName) + => MigratedComponent.Equals(context.EditingFormControlModel?.FormComponentIdentifier, StringComparison.InvariantCultureIgnoreCase); + + public Task MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context) + { + if (value?.ToObject>() is { Count: > 0 } items) + { + var result = items.Select(x => new Kentico.Components.Web.Mvc.FormComponents.PathSelectorItem { TreePath = x.NodeAliasPath }).ToList(); + var resultAsJToken = JToken.FromObject(result); + return Task.FromResult(new WidgetPropertyMigrationResult(resultAsJToken)); + } + else + { + logger.LogError("Failed to parse '{ComponentName}' json {Json}", MigratedComponent, value?.ToString() ?? ""); + + // leave value as it is + return Task.FromResult(new WidgetPropertyMigrationResult(value)); + } + } +} diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index f30baed3..a71cbf6a 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -78,4 +78,20 @@ serviceCollection.AddSingleton(m); ### Inject and use reusable schema -demonstrated in method `AddReusableSchemaIntegrationSample`, goal is to take single data class and assign reusable schema. \ No newline at end of file +demonstrated in method `AddReusableSchemaIntegrationSample`, goal is to take single data class and assign reusable schema. + + +## Custom widget property migrations + +To create custom widget property migration: +- create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration) +- implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetPropertyMigration` + - implement property rank, set number bellow 100 000 - for example 5000 + - implement method shall migrate (if method returns true, migration will be used) + - implement `MigrateWidgetProperty`, where objective is to convert old JToken representing json value to new converted JToken value +- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient()` + +Samples: +- [Path selector migration](./DefaultMigrations/WidgetPathSelectorMigration.cs) +- [Page selector migration](./DefaultMigrations/WidgetPageSelectorMigration.cs) +- [File selector migration](./DefaultMigrations/WidgetFileMigration.cs) \ No newline at end of file diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index f76e9e6f..61c751d3 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Migration.Tool.Extensions.CommunityMigrations; using Migration.Tool.Extensions.DefaultMigrations; using Migration.Tool.KXP.Api.Services.CmsClass; @@ -12,6 +12,10 @@ public static IServiceCollection UseCustomizations(this IServiceCollection servi services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + // services.AddClassMergeExample(); // services.AddSimpleRemodelingSample(); // services.AddReusableSchemaIntegrationSample(); diff --git a/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs b/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs index 086770bd..c282763b 100644 --- a/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs +++ b/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs @@ -18,8 +18,9 @@ public static IServiceCollection UseKxpApi(this IServiceCollection services, ICo SystemContext.WebApplicationPhysicalPath = applicationPhysicalPath; } + services.AddTransient(); + services.AddTransient(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(s => (s.GetService() as FieldMigrationService)!); diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs new file mode 100644 index 00000000..7173be71 --- /dev/null +++ b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs @@ -0,0 +1,18 @@ +using Migration.Tool.Common.Services.Ipc; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.KXP.Api.Services.CmsClass; + +public record WidgetPropertyMigrationContext(int SiteId, EditingFormControlModel? EditingFormControlModel); +public record WidgetPropertyMigrationResult(JToken? Value, bool NeedsDeferredPatch = false, bool AllowDefaultMigrations = true); + +public interface IWidgetPropertyMigration +{ + /// + /// custom migrations are sorted by this number, first encountered migration wins. Values higher than 100 000 are set to default migrations, set number bellow 100 000 for custom migrations + /// + int Rank { get; } + + bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName); + Task MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context); +} diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs b/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs new file mode 100644 index 00000000..5ec236ba --- /dev/null +++ b/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Migration.Tool.KXP.Api.Services.CmsClass; + +public class WidgetMigrationService +{ + private readonly List widgetPropertyMigrations; + + public WidgetMigrationService(IServiceProvider serviceProvider) + { + var migrations = serviceProvider.GetService>(); + widgetPropertyMigrations = migrations == null + ? [] + : migrations.OrderBy(wpm => wpm.Rank).ToList(); + } + + public IWidgetPropertyMigration? GetWidgetPropertyMigrations(WidgetPropertyMigrationContext context, string key) + => widgetPropertyMigrations.FirstOrDefault(wpm => wpm.ShallMigrate(context, key)); +} diff --git a/README.md b/README.md index 5dba5dae..fd304fd7 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,12 @@ The Kentico Migration Tool transfers content and other data from **Kentico Xperi ## Library Version Matrix | Xperience Version | Library Version | -| ----------------- | --------------- | +| ----------------- |-----------------| | == 29.1.0 | == 1.0.0 | | == 29.2.0 | == 1.1.0 | | == 29.3.3 | == 1.2.0 | | == 29.5.2 | == 1.3.0 | +| == 29.5.2 | == 1.4.0 | ## Dependencies From e342010f21ac96bb04192a13dab8365795bee05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Krch?= Date: Mon, 21 Oct 2024 16:53:25 +0200 Subject: [PATCH 2/2] format fix --- .gitattributes | 2 +- .../Handlers/MigratePagesCommandHandler.cs | 1438 ++++++++-------- .../Helpers/PageBuilderWidgetsPatcher.cs | 158 +- .../KsCoreDiExtensions.cs | 218 +-- .../Mappers/ContentItemMapper.cs | 1468 ++++++++--------- .../PageTemplateConfigurationMapper.cs | 114 +- .../ServiceCollectionExtensions.cs | 48 +- .../DependencyInjectionExtensions.cs | 64 +- 8 files changed, 1755 insertions(+), 1755 deletions(-) diff --git a/.gitattributes b/.gitattributes index d5245560..de90f22f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -# * text=auto +* text eol=crlf # Ensure binary files are not treated as text https://stackoverflow.com/a/32278635/939634 *.png binary diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 66f3b4b8..b62fdb33 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -1,719 +1,719 @@ -using System.Collections.Concurrent; -using System.Diagnostics; - -using CMS.ContentEngine; -using CMS.ContentEngine.Internal; -using CMS.Core; -using CMS.Core.Internal; -using CMS.DataEngine; -using CMS.DataEngine.Query; -using CMS.Websites; -using CMS.Websites.Internal; -using CMS.Websites.Routing.Internal; -using Kentico.Xperience.UMT.Model; -using Kentico.Xperience.UMT.Services; - -using MediatR; - -using Microsoft.Data.SqlClient; -using Microsoft.Extensions.Logging; - -using Migration.Tool.Common; -using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.Helpers; -using Migration.Tool.Common.MigrationProtocol; -using Migration.Tool.Common.Model; -using Migration.Tool.KXP.Models; -using Migration.Tool.Source.Contexts; -using Migration.Tool.Source.Helpers; -using Migration.Tool.Source.Mappers; -using Migration.Tool.Source.Model; -using Migration.Tool.Source.Providers; -using Migration.Tool.Source.Services; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Migration.Tool.Source.Handlers; -// ReSharper disable once UnusedType.Global -public class MigratePagesCommandHandler( - ILogger logger, - ToolConfiguration toolConfiguration, - IProtocol protocol, - IImporter importer, - IUmtMapper mapper, - ModelFacade modelFacade, - DeferredPathService deferredPathService, - SpoiledGuidContext spoiledGuidContext, - SourceInstanceContext sourceInstanceContext, - ClassMappingProvider classMappingProvider -) - : IRequestHandler -{ - private const string ClassCmsRoot = "CMS.Root"; - - private readonly ContentItemNameProvider contentItemNameProvider = new(new ContentItemNameValidator()); - - private readonly ConcurrentDictionary languages = new(StringComparer.InvariantCultureIgnoreCase); - - public async Task Handle(MigratePagesCommand request, CancellationToken cancellationToken) - { - var classEntityConfiguration = toolConfiguration.EntityConfigurations.GetEntityConfiguration(); - - var cultureCodeToLanguageGuid = modelFacade.SelectAll() - .ToDictionary(c => c.CultureCode, c => c.CultureGUID, StringComparer.InvariantCultureIgnoreCase); - - var sites = modelFacade.SelectAll(); - foreach (var ksSite in sites) - { - var channelInfo = ChannelInfoProvider.ProviderObject.Get(ksSite.SiteGUID); - if (channelInfo == null) - { - logger.LogError("Target channel for site '{SiteName}' not exists!", ksSite.SiteName); - continue; - } - - logger.LogInformation("Migrating pages for site '{SourceSiteName}' to target channel '{TargetChannelName}' as content items", ksSite.SiteName, channelInfo.ChannelName); - - var ksTrees = modelFacade.Select( - "NodeSiteId = @siteId", - "NodeLevel, NodeOrder", - new SqlParameter("siteId", ksSite.SiteID) - ); - - foreach (var ksTreeOriginal in ksTrees) - { - logger.LogDebug("Page '{NodeAliasPath}' migration", ksTreeOriginal.NodeAliasPath); - - protocol.FetchedSource(ksTreeOriginal); - - var ksNode = ksTreeOriginal; - var nodeLinkedNode = modelFacade.SelectById(ksNode.NodeLinkedNodeID); - var migratedDocuments = modelFacade - .SelectWhere("DocumentNodeID = @nodeId", new SqlParameter("nodeId", ksNode.NodeID)) - .ToList(); - - bool wasLinkedNode = nodeLinkedNode != null; - if (wasLinkedNode) - { - if (nodeLinkedNode?.NodeSiteID != ksNode.NodeSiteID) - { - // skip & write to protocol - logger.LogWarning("Linked node with NodeGuid {NodeGuid} is linked from different site - unable to migrate", ksTreeOriginal.NodeGUID); - protocol.Warning(HandbookReferences.CmsTreeTreeIsLinkFromDifferentSite, ksNode); - continue; - } - - // materialize linked node & write to protocol - var linkedNode = modelFacade.SelectWhere("NodeSiteID = @nodeSiteID AND NodeGUID = @nodeGuid", - new SqlParameter("nodeSiteID", ksNode.NodeSiteID), - new SqlParameter("nodeGuid", nodeLinkedNode.NodeGUID) - ).SingleOrDefault(); - - Debug.Assert(ksNode != null, nameof(ksNode) + " != null"); - Debug.Assert(linkedNode != null, nameof(linkedNode) + " != null"); - - migratedDocuments.Clear(); - - var linkedNodeDocuments = modelFacade - .SelectWhere("DocumentNodeID = @nodeId", new SqlParameter("nodeId", linkedNode.NodeID)) - .ToList(); - - for (int i = 0; i < linkedNodeDocuments.Count; i++) - { - var linkedDocument = linkedNodeDocuments[i]; - var fixedDocumentGuid = GuidHelper.CreateDocumentGuid($"{linkedDocument.DocumentID}|{ksNode.NodeID}|{ksNode.NodeSiteID}"); //Guid.NewGuid(); - var patchedNodeGuid = spoiledGuidContext.EnsureNodeGuid(ksNode.NodeGUID, ksNode.NodeSiteID, ksNode.NodeID); - if (ContentItemInfo.Provider.Get(patchedNodeGuid)?.ContentItemID is { } contentItemId) - { - if (cultureCodeToLanguageGuid.TryGetValue(linkedDocument.DocumentCulture, out var languageGuid) && - ContentLanguageInfoProvider.ProviderObject.Get(languageGuid) is { } languageInfo) - { - if (ContentItemCommonDataInfo.Provider.Get() - .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentItemID), contentItemId) - .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentLanguageID), languageInfo.ContentLanguageID) - .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataIsLatest), true) - .FirstOrDefault() is { } contentItemCommonDataInfo) - { - fixedDocumentGuid = contentItemCommonDataInfo.ContentItemCommonDataGUID; - logger.LogTrace("Page '{NodeAliasPath}' is linked => ContentItemCommonDataGUID copy to DocumentGuid", ksNode.NodeAliasPath); - } - } - } - - linkedNodeDocuments[i] = linkedDocument switch - { - CmsDocumentK11 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, - CmsDocumentK12 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, - CmsDocumentK13 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, - _ => linkedNodeDocuments[i] - }; - - migratedDocuments.Add(linkedNodeDocuments[i]); - ksNode = ksNode switch - { - CmsTreeK11 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, - CmsTreeK12 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, - CmsTreeK13 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, - _ => ksNode - }; - - logger.LogWarning("Linked node with NodeGuid {NodeGuid} was materialized (Xperience by Kentico doesn't support links), it no longer serves as link to original document. This affect also routing, this document will have own link generated from node alias path", ksNode.NodeGUID); - } - } - - var ksNodeClass = modelFacade.SelectById(ksNode.NodeClassID) ?? throw new InvalidOperationException($"Node with missing class, node id '{ksNode.NodeID}'"); - string nodeClassClassName = ksNodeClass.ClassName; - if (classEntityConfiguration.ExcludeCodeNames.Contains(nodeClassClassName, StringComparer.InvariantCultureIgnoreCase)) - { - protocol.Warning(HandbookReferences.EntityExplicitlyExcludedByCodeName(nodeClassClassName, "PageType"), ksNode); - logger.LogWarning("Page: page of class {ClassName} was skipped => it is explicitly excluded in configuration", nodeClassClassName); - continue; - } - - if (nodeClassClassName == ClassCmsRoot) - { - logger.LogInformation("Root node skipped, V27 has no support for root nodes"); - continue; - } - - Debug.Assert(migratedDocuments.Count > 0, "migratedDocuments.Count > 0"); - - if (ksTreeOriginal is { NodeSKUID: not null }) - { - logger.LogWarning("Page '{NodeAliasPath}' has SKU bound, SKU info will be discarded", ksTreeOriginal.NodeAliasPath); - protocol.Append(HandbookReferences.NotCurrentlySupportedSkip() - .WithMessage($"Page '{ksTreeOriginal.NodeAliasPath}' has SKU bound, SKU info will be discarded") - .WithIdentityPrint(ksTreeOriginal) - .WithData(new { ksTreeOriginal.NodeSKUID }) - ); - } - - string safeNodeName = await contentItemNameProvider.Get(ksNode.NodeName); - var ksNodeParent = modelFacade.SelectById(ksNode.NodeParentID); - var nodeParentGuid = ksNodeParent?.NodeAliasPath == "/" || ksNodeParent == null - ? (Guid?)null - : spoiledGuidContext.EnsureNodeGuid(ksNodeParent); - - DataClassInfo targetClass = null!; - var classMapping = classMappingProvider.GetMapping(ksNodeClass.ClassName); - targetClass = classMapping != null - ? DataClassInfoProvider.ProviderObject.Get(classMapping.TargetClassName) - : DataClassInfoProvider.ProviderObject.Get(ksNodeClass.ClassGUID); - - var results = mapper.Map(new CmsTreeMapperSource( - ksNode, - safeNodeName, - ksSite.SiteGUID, - nodeParentGuid, - cultureCodeToLanguageGuid, - targetClass?.ClassFormDefinition, - ksNodeClass.ClassFormDefinition, - migratedDocuments, - ksSite - )); - try - { - WebPageItemInfo? webPageItemInfo = null; - var commonDataInfos = new List(); - foreach (var umtModel in results) - { - var result = await importer.ImportAsync(umtModel); - if (result is { Success: false }) - { - logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); - } - - switch (result) - { - case { Success: true, Imported: ContentItemCommonDataInfo ccid }: - { - commonDataInfos.Add(ccid); - Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } - case { Success: true, Imported: ContentItemLanguageMetadataInfo cclm }: - { - Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } - case { Success: true, Imported: WebPageItemInfo wp }: - { - webPageItemInfo = wp; - break; - } - - default: - break; - } - } - - AsserVersionStatusRule(commonDataInfos); - - if (webPageItemInfo != null && targetClass is { ClassWebPageHasUrl: true }) - { - await GenerateDefaultPageUrlPath(ksNode, webPageItemInfo, wasLinkedNode); - - foreach (var migratedDocument in migratedDocuments) - { - var languageGuid = cultureCodeToLanguageGuid[migratedDocument.DocumentCulture]; - - await MigratePageUrlPaths(ksSite.SiteGUID, - languageGuid, - commonDataInfos, - migratedDocument, - ksNode, - migratedDocument.DocumentCulture, - wasLinkedNode, webPageItemInfo); - } - - MigrateFormerUrls(ksNode, webPageItemInfo); - - var urls = WebPageUrlPathInfo.Provider.Get() - .WhereEquals(nameof(WebPageUrlPathInfo.WebPageUrlPathWebPageItemID), webPageItemInfo.WebPageItemID); - - if (urls.Count < 1) - { - logger.LogWarning("No url for page {Page}", new { webPageItemInfo.WebPageItemName, webPageItemInfo.WebPageItemTreePath, webPageItemInfo.WebPageItemGUID }); - } - } - else - { - logger.LogTrace("No webpage item produced for '{NodeAliasPath}'", ksNode.NodeAliasPath); - } - } - - catch (Exception ex) - { - protocol.Append(HandbookReferences - .ErrorCreatingTargetInstance(ex) - .NeedsManualAction() - .WithIdentityPrint(ksNode) - ); - logger.LogError("Failed to import content item: {Exception}", ex); - } - } - } - - await ExecDeferredPageBuilderPatch(); - - return new GenericCommandResult(); - } - - [Conditional("DEBUG")] - private static void AsserVersionStatusRule(List commonDataInfos) - { - foreach (var contentItemCommonDataInfos in commonDataInfos.GroupBy(x => x.ContentItemCommonDataContentLanguageID)) - { - VersionStatus? versionStatus = null; - bool onlyOneStatus = contentItemCommonDataInfos.Aggregate(true, (acc, i) => - { - try - { - if (versionStatus.HasValue) - { - return versionStatus != i.ContentItemCommonDataVersionStatus; - } - - return true; - } - finally - { - versionStatus = i.ContentItemCommonDataVersionStatus; - } - }); - Debug.Assert(onlyOneStatus); - } - } - - public enum PageRoutingModeEnum // copy of enum from KX13 dll - { - /// - /// Routing based on custom routes of standard MVC support. - /// - Custom = 0, - /// - /// Routing based on system routes driven by content tree structure. - /// - BasedOnContentTree = 1, - } - - private readonly Dictionary cmsClassCache = []; - private ICmsClass GetCmsClass(int classId) - { - if (cmsClassCache.TryGetValue(classId, out var cmsClass)) - { - return cmsClass; - } - - cmsClass = modelFacade.SelectById(classId); - cmsClassCache[classId] = cmsClass ?? throw new InvalidOperationException($"CMS Class with class id {classId} not found => invalid data"); - return cmsClass; - } - - private async Task MigratePageUrlPaths(Guid webSiteChannelGuid, Guid languageGuid, - List contentItemCommonDataInfos, ICmsDocument? ksDocument, ICmsTree ksTree, string documentCulture, bool wasLinkedNode, WebPageItemInfo webPageItemInfo) - { - var languageInfo = ContentLanguageInfoProvider.ProviderObject.Get(languageGuid); - var webSiteChannel = WebsiteChannelInfoProvider.ProviderObject.Get(webSiteChannelGuid); - - #region Migration of custom routing model - - if (modelFacade.SelectVersion() is { Major: 13 } && KenticoHelper.GetSettingsKey(modelFacade, ksTree.NodeSiteID, "CMSRoutingMode") is (int)PageRoutingModeEnum.Custom) - { - if (modelFacade.SelectById(ksTree.NodeSiteID) is not { } site) - { - logger.LogError("Unable to find source site with ID '{SiteID}', fallback url will be used for node {NodeID}", ksTree.NodeSiteID, ksTree.NodeID); - } - // for ability to resolve macros we query source instance where we can resolve marcos in url pattern for particular page - else if (!sourceInstanceContext.HasInfo) - { - logger.LogWarning("Cannot migrate url for document '{DocumentID}' / node '{NodeID}', source instance context is not available or set-up correctly - default fallback will be used.", ksDocument?.DocumentID, ksTree.NodeID); - } - else if (GetCmsClass(ksTree.NodeClassID) is { } cmsClass && string.IsNullOrWhiteSpace(cmsClass.ClassURLPattern)) - { - logger.LogWarning("Cannot migrate url for document '{DocumentID}' / node '{NodeID}', class {ClassName} has no url pattern set - cannot migrate to former url.", ksDocument?.DocumentID, ksTree.NodeID, cmsClass.ClassName); - } - else if (sourceInstanceContext.GetNodeUrls(ksTree.NodeID, site.SiteName) is not { Count: > 0 } pageModels) - { - logger.LogError("No information could be found in source instance about node {NodeID} on site {SiteName}", ksTree.NodeID, site.SiteName); - } - else if (pageModels.FirstOrDefault(pm => pm.DocumentCulture == documentCulture) is not { } pageModel) - { - logger.LogWarning("Page url information for document {DocumentID} / node {NodeID} not found for culture {Culture}", ksDocument?.DocumentID, ksTree.NodeID, documentCulture); - } - else if (string.IsNullOrWhiteSpace(pageModel.CultureUrl)) - { - logger.LogWarning("Page url information for document {DocumentID} / node {NodeID} was found for culture {Culture}, but culture url is empty - unexpected", ksDocument?.DocumentID, ksTree.NodeID, documentCulture); - } - else - { - string patchedUrl = pageModel.CultureUrl.TrimStart(['~']).TrimStart(['/']); - string urlHash = modelFacade.HashPath(patchedUrl); - var webPageFormerUrlPathInfo = new WebPageFormerUrlPathInfo - { - WebPageFormerUrlPath = patchedUrl, - WebPageFormerUrlPathHash = urlHash, - WebPageFormerUrlPathWebPageItemID = webPageItemInfo.WebPageItemID, - WebPageFormerUrlPathWebsiteChannelID = webSiteChannel.WebsiteChannelID, - WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID, - WebPageFormerUrlPathLastModified = Service.Resolve().GetDateTimeNow() - }; - WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); - logger.LogEntitySetAction(true, webPageItemInfo); - return; - } - } - - #endregion - - if (modelFacade.IsAvailable()) - { - var ksPaths = modelFacade.SelectWhere("PageUrlPathNodeId = @nodeId AND PageUrlPathCulture = @culture", - new SqlParameter("nodeId", ksTree.NodeID), - new SqlParameter("culture", documentCulture) - ).ToList(); - - if (ksPaths.Count > 0) - { - foreach (var ksPath in ksPaths) - { - logger.LogTrace("Page url path: C={Culture} S={Site} P={Path}", ksPath.PageUrlPathCulture, ksPath.PageUrlPathSiteID, ksPath.PageUrlPathUrlPath); - - foreach (var contentItemCommonDataInfo in contentItemCommonDataInfos.Where(x => x.ContentItemCommonDataContentLanguageID == languageInfo.ContentLanguageID)) - { - logger.LogTrace("Page url path common data info: CIID={ContentItemId} CLID={Language} ID={Id}", contentItemCommonDataInfo.ContentItemCommonDataContentItemID, - contentItemCommonDataInfo.ContentItemCommonDataContentLanguageID, contentItemCommonDataInfo.ContentItemCommonDataID); - - Debug.Assert(!string.IsNullOrWhiteSpace(ksPath.PageUrlPathUrlPath), "!string.IsNullOrWhiteSpace(kx13PageUrlPath.PageUrlPathUrlPath)"); - - var webPageUrlPath = new WebPageUrlPathModel - { - WebPageUrlPathGUID = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus == VersionStatus.Draft - ? Guid.NewGuid() - : ksPath.PageUrlPathGUID, - WebPageUrlPath = ksPath.PageUrlPathUrlPath.TrimStart('/'), - WebPageUrlPathHash = ksPath.PageUrlPathUrlPathHash, - WebPageUrlPathWebPageItemGuid = webPageItemInfo.WebPageItemGUID, - WebPageUrlPathWebsiteChannelGuid = webSiteChannelGuid, - WebPageUrlPathContentLanguageGuid = languageGuid, - WebPageUrlPathIsLatest = contentItemCommonDataInfo.ContentItemCommonDataIsLatest, - WebPageUrlPathIsDraft = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus switch - { - VersionStatus.InitialDraft => false, - VersionStatus.Draft => true, - VersionStatus.Published => false, - VersionStatus.Unpublished => false, - _ => throw new ArgumentOutOfRangeException() - } - }; - - CheckPathAlreadyExists(webPageUrlPath, languageInfo, webSiteChannel, webPageItemInfo.WebPageItemID); - - var importResult = await importer.ImportAsync(webPageUrlPath); - - LogImportResult(importResult); - } - } - } - } - else - { - foreach (var contentItemCommonDataInfo in contentItemCommonDataInfos.Where(x => x.ContentItemCommonDataContentLanguageID == languageInfo.ContentLanguageID)) - { - logger.LogTrace("Page url path common data info: CIID={ContentItemId} CLID={Language} ID={Id}", contentItemCommonDataInfo.ContentItemCommonDataContentItemID, - contentItemCommonDataInfo.ContentItemCommonDataContentLanguageID, contentItemCommonDataInfo.ContentItemCommonDataID); - - string? urlPath = (ksDocument switch - { - CmsDocumentK11 doc => wasLinkedNode ? null : doc.DocumentUrlPath, - CmsDocumentK12 doc => wasLinkedNode ? null : doc.DocumentUrlPath, - _ => null - }).NullIf(string.Empty)?.TrimStart('/'); - - if (urlPath is not null) - { - var webPageUrlPath = new WebPageUrlPathModel - { - WebPageUrlPathGUID = GuidHelper.CreateWebPageUrlPathGuid($"{urlPath}|{documentCulture}|{webSiteChannel.WebsiteChannelGUID}|{ksTree.NodeID}"), - WebPageUrlPath = urlPath, - WebPageUrlPathWebPageItemGuid = webPageItemInfo.WebPageItemGUID, - WebPageUrlPathWebsiteChannelGuid = webSiteChannelGuid, - WebPageUrlPathContentLanguageGuid = languageGuid, - WebPageUrlPathIsLatest = contentItemCommonDataInfo.ContentItemCommonDataIsLatest, - WebPageUrlPathIsDraft = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus switch - { - VersionStatus.InitialDraft => false, - VersionStatus.Draft => true, - VersionStatus.Published => false, - VersionStatus.Unpublished => false, - _ => throw new ArgumentOutOfRangeException() - } - }; - - CheckPathAlreadyExists(webPageUrlPath, languageInfo, webSiteChannel, webPageItemInfo.WebPageItemID); - - var importResult = await importer.ImportAsync(webPageUrlPath); - - LogImportResult(importResult); - } - } - } - } - - private async Task GenerateDefaultPageUrlPath(ICmsTree ksTree, WebPageItemInfo webPageItemInfo, bool wasLinkedNode) - { - var man = Service.Resolve(); - string alias = wasLinkedNode ? ksTree.NodeAlias : ksTree.NodeAliasPath; - var collisionData = await man.GeneratePageUrlPath(webPageItemInfo, alias, VersionStatus.InitialDraft, CancellationToken.None); - foreach (var data in collisionData) - { - logger.LogError("WebPageUrlPath collision occured {Path}", data.Path); - } - } - - private void CheckPathAlreadyExists(WebPageUrlPathModel webPageUrlPath, - ContentLanguageInfo languageInfo, - WebsiteChannelInfo webSiteChannel, int webPageItemId) - { - Debug.Assert(webPageUrlPath is not { WebPageUrlPathIsLatest: false, WebPageUrlPathIsDraft: true }, "webPageUrlPath is not { WebPageUrlPathIsLatest: false, WebPageUrlPathIsDraft: true }"); - - var existingPaths = WebPageUrlPathInfo.Provider.Get() - .WhereEquals(nameof(WebPageUrlPathInfo.WebPageUrlPathWebPageItemID), webPageItemId) - .ToList(); - - var ep = existingPaths.FirstOrDefault(ep => - ep.WebPageUrlPathContentLanguageID == languageInfo.ContentLanguageID && - ep.WebPageUrlPathIsDraft == webPageUrlPath.WebPageUrlPathIsDraft && - ep.WebPageUrlPathWebsiteChannelID == webSiteChannel.WebsiteChannelID && - ep.WebPageUrlPathWebPageItemID == webPageItemId - ); - - if (ep != null) - { - webPageUrlPath.WebPageUrlPathGUID = ep.WebPageUrlPathGUID; - logger.LogTrace("Existing page url path found for '{Path}', fixing GUID to '{Guid}'", webPageUrlPath.WebPageUrlPath, webPageUrlPath.WebPageUrlPathGUID); - } - } - - private void LogImportResult(IImportResult importResult) - { - switch (importResult) - { - case { Success: true, Imported: WebPageUrlPathInfo imported }: - { - logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); - break; - } - case { Success: false, Exception: { } exception }: - { - logger.LogError("Failed to import page url path: {Error}", exception.ToString()); - break; - } - case { Success: false, ModelValidationResults: { } validation }: - { - foreach (var validationResult in validation) - { - logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); - } - - break; - } - - default: - break; - } - } - - private void MigrateFormerUrls(ICmsTree ksNode, WebPageItemInfo targetPage) - { - if (modelFacade.IsAvailable()) - { - var formerUrlPaths = modelFacade.SelectWhere( - "PageFormerUrlPathSiteID = @siteId AND PageFormerUrlPathNodeID = @nodeId", - new SqlParameter("siteId", ksNode.NodeSiteID), - new SqlParameter("nodeId", ksNode.NodeID) - ); - foreach (var cmsPageFormerUrlPath in formerUrlPaths) - { - logger.LogDebug("PageFormerUrlPath migration '{PageFormerUrlPath}' ", cmsPageFormerUrlPath); - protocol.FetchedSource(cmsPageFormerUrlPath); - - switch (cmsPageFormerUrlPath) - { - case CmsPageFormerUrlPathK11: - case CmsPageFormerUrlPathK12: - { - logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); - break; - } - case CmsPageFormerUrlPathK13 pfup: - { - try - { - var languageInfo = languages.GetOrAdd( - pfup.PageFormerUrlPathCulture, - s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") - ); - - var ktPath = WebPageFormerUrlPathInfo.Provider.Get() - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) - .SingleOrDefault(); - - if (ktPath != null) - { - protocol.FetchedTarget(ktPath); - } - - var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); - webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; - webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; - - WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); - logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); - } - catch (Exception ex) - { - protocol.Append(HandbookReferences - .ErrorCreatingTargetInstance(ex) - .NeedsManualAction() - .WithIdentityPrint(pfup) - ); - logger.LogError("Failed to import page former url path: {Exception}", ex); - } - - break; - } - default: - throw new ArgumentOutOfRangeException(nameof(cmsPageFormerUrlPath)); - } - } - } - else - { - logger.LogDebug("CmsPageFormerUrlPath not supported in source instance"); - } - } - - internal static QueryExpression GetWebPageUrlPathHashQueryExpression(string urlPath) => $"CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', LOWER(N'{SqlHelper.EscapeQuotes(urlPath)}')), 2)".AsExpression(); - - #region Deffered patch - - private async Task ExecDeferredPageBuilderPatch() - { - logger.LogInformation("Executing TreePath patch"); - - foreach ((var uniqueId, string className, int webSiteChannelId) in deferredPathService.GetWidgetsToPatch()) - { - if (className == ContentItemCommonDataInfo.TYPEINFO.ObjectClassName) - { - var contentItemCommonDataInfo = await ContentItemCommonDataInfo.Provider.GetAsync(uniqueId); - - contentItemCommonDataInfo.ContentItemCommonDataPageBuilderWidgets = DeferredPatchPageBuilderWidgets( - contentItemCommonDataInfo.ContentItemCommonDataPageBuilderWidgets, webSiteChannelId, out bool anythingChangedW); - contentItemCommonDataInfo.ContentItemCommonDataPageTemplateConfiguration = DeferredPatchPageTemplateConfiguration( - contentItemCommonDataInfo.ContentItemCommonDataPageTemplateConfiguration, webSiteChannelId, out bool anythingChangedC); - - if (anythingChangedC || anythingChangedW) - { - contentItemCommonDataInfo.Update(); - } - } - else if (className == PageTemplateConfigurationInfo.TYPEINFO.ObjectClassName) - { - var pageTemplateConfigurationInfo = await PageTemplateConfigurationInfo.Provider.GetAsync(uniqueId); - pageTemplateConfigurationInfo.PageTemplateConfigurationWidgets = DeferredPatchPageBuilderWidgets( - pageTemplateConfigurationInfo.PageTemplateConfigurationWidgets, - webSiteChannelId, - out bool anythingChangedW - ); - pageTemplateConfigurationInfo.PageTemplateConfigurationTemplate = DeferredPatchPageTemplateConfiguration( - pageTemplateConfigurationInfo.PageTemplateConfigurationTemplate, - webSiteChannelId, - out bool anythingChangedC - ); - if (anythingChangedW || anythingChangedC) - { - PageTemplateConfigurationInfo.Provider.Set(pageTemplateConfigurationInfo); - } - } - } - } - - private string DeferredPatchPageTemplateConfiguration(string documentPageTemplateConfiguration, int webSiteChannelId, out bool anythingChanged) - { - if (!string.IsNullOrWhiteSpace(documentPageTemplateConfiguration)) - { - var configuration = JObject.Parse(documentPageTemplateConfiguration); - PageBuilderWidgetsPatcher.DeferredPatchProperties(configuration, TreePathConvertor.GetSiteConverter(webSiteChannelId), out anythingChanged); - return JsonConvert.SerializeObject(configuration); - } - - anythingChanged = false; - return documentPageTemplateConfiguration; - } - - private string DeferredPatchPageBuilderWidgets(string documentPageBuilderWidgets, int webSiteChannelId, out bool anythingChanged) - { - if (!string.IsNullOrWhiteSpace(documentPageBuilderWidgets)) - { - var patched = PageBuilderWidgetsPatcher.DeferredPatchConfiguration( - JsonConvert.DeserializeObject(documentPageBuilderWidgets), - TreePathConvertor.GetSiteConverter(webSiteChannelId), - out anythingChanged - ); - return JsonConvert.SerializeObject(patched); - } - - anythingChanged = false; - return documentPageBuilderWidgets; - } - - #endregion -} +using System.Collections.Concurrent; +using System.Diagnostics; + +using CMS.ContentEngine; +using CMS.ContentEngine.Internal; +using CMS.Core; +using CMS.Core.Internal; +using CMS.DataEngine; +using CMS.DataEngine.Query; +using CMS.Websites; +using CMS.Websites.Internal; +using CMS.Websites.Routing.Internal; +using Kentico.Xperience.UMT.Model; +using Kentico.Xperience.UMT.Services; + +using MediatR; + +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; + +using Migration.Tool.Common; +using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.Helpers; +using Migration.Tool.Common.MigrationProtocol; +using Migration.Tool.Common.Model; +using Migration.Tool.KXP.Models; +using Migration.Tool.Source.Contexts; +using Migration.Tool.Source.Helpers; +using Migration.Tool.Source.Mappers; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; +using Migration.Tool.Source.Services; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Source.Handlers; +// ReSharper disable once UnusedType.Global +public class MigratePagesCommandHandler( + ILogger logger, + ToolConfiguration toolConfiguration, + IProtocol protocol, + IImporter importer, + IUmtMapper mapper, + ModelFacade modelFacade, + DeferredPathService deferredPathService, + SpoiledGuidContext spoiledGuidContext, + SourceInstanceContext sourceInstanceContext, + ClassMappingProvider classMappingProvider +) + : IRequestHandler +{ + private const string ClassCmsRoot = "CMS.Root"; + + private readonly ContentItemNameProvider contentItemNameProvider = new(new ContentItemNameValidator()); + + private readonly ConcurrentDictionary languages = new(StringComparer.InvariantCultureIgnoreCase); + + public async Task Handle(MigratePagesCommand request, CancellationToken cancellationToken) + { + var classEntityConfiguration = toolConfiguration.EntityConfigurations.GetEntityConfiguration(); + + var cultureCodeToLanguageGuid = modelFacade.SelectAll() + .ToDictionary(c => c.CultureCode, c => c.CultureGUID, StringComparer.InvariantCultureIgnoreCase); + + var sites = modelFacade.SelectAll(); + foreach (var ksSite in sites) + { + var channelInfo = ChannelInfoProvider.ProviderObject.Get(ksSite.SiteGUID); + if (channelInfo == null) + { + logger.LogError("Target channel for site '{SiteName}' not exists!", ksSite.SiteName); + continue; + } + + logger.LogInformation("Migrating pages for site '{SourceSiteName}' to target channel '{TargetChannelName}' as content items", ksSite.SiteName, channelInfo.ChannelName); + + var ksTrees = modelFacade.Select( + "NodeSiteId = @siteId", + "NodeLevel, NodeOrder", + new SqlParameter("siteId", ksSite.SiteID) + ); + + foreach (var ksTreeOriginal in ksTrees) + { + logger.LogDebug("Page '{NodeAliasPath}' migration", ksTreeOriginal.NodeAliasPath); + + protocol.FetchedSource(ksTreeOriginal); + + var ksNode = ksTreeOriginal; + var nodeLinkedNode = modelFacade.SelectById(ksNode.NodeLinkedNodeID); + var migratedDocuments = modelFacade + .SelectWhere("DocumentNodeID = @nodeId", new SqlParameter("nodeId", ksNode.NodeID)) + .ToList(); + + bool wasLinkedNode = nodeLinkedNode != null; + if (wasLinkedNode) + { + if (nodeLinkedNode?.NodeSiteID != ksNode.NodeSiteID) + { + // skip & write to protocol + logger.LogWarning("Linked node with NodeGuid {NodeGuid} is linked from different site - unable to migrate", ksTreeOriginal.NodeGUID); + protocol.Warning(HandbookReferences.CmsTreeTreeIsLinkFromDifferentSite, ksNode); + continue; + } + + // materialize linked node & write to protocol + var linkedNode = modelFacade.SelectWhere("NodeSiteID = @nodeSiteID AND NodeGUID = @nodeGuid", + new SqlParameter("nodeSiteID", ksNode.NodeSiteID), + new SqlParameter("nodeGuid", nodeLinkedNode.NodeGUID) + ).SingleOrDefault(); + + Debug.Assert(ksNode != null, nameof(ksNode) + " != null"); + Debug.Assert(linkedNode != null, nameof(linkedNode) + " != null"); + + migratedDocuments.Clear(); + + var linkedNodeDocuments = modelFacade + .SelectWhere("DocumentNodeID = @nodeId", new SqlParameter("nodeId", linkedNode.NodeID)) + .ToList(); + + for (int i = 0; i < linkedNodeDocuments.Count; i++) + { + var linkedDocument = linkedNodeDocuments[i]; + var fixedDocumentGuid = GuidHelper.CreateDocumentGuid($"{linkedDocument.DocumentID}|{ksNode.NodeID}|{ksNode.NodeSiteID}"); //Guid.NewGuid(); + var patchedNodeGuid = spoiledGuidContext.EnsureNodeGuid(ksNode.NodeGUID, ksNode.NodeSiteID, ksNode.NodeID); + if (ContentItemInfo.Provider.Get(patchedNodeGuid)?.ContentItemID is { } contentItemId) + { + if (cultureCodeToLanguageGuid.TryGetValue(linkedDocument.DocumentCulture, out var languageGuid) && + ContentLanguageInfoProvider.ProviderObject.Get(languageGuid) is { } languageInfo) + { + if (ContentItemCommonDataInfo.Provider.Get() + .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentItemID), contentItemId) + .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentLanguageID), languageInfo.ContentLanguageID) + .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataIsLatest), true) + .FirstOrDefault() is { } contentItemCommonDataInfo) + { + fixedDocumentGuid = contentItemCommonDataInfo.ContentItemCommonDataGUID; + logger.LogTrace("Page '{NodeAliasPath}' is linked => ContentItemCommonDataGUID copy to DocumentGuid", ksNode.NodeAliasPath); + } + } + } + + linkedNodeDocuments[i] = linkedDocument switch + { + CmsDocumentK11 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, + CmsDocumentK12 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, + CmsDocumentK13 doc => doc with { DocumentGUID = fixedDocumentGuid, DocumentID = 0 }, + _ => linkedNodeDocuments[i] + }; + + migratedDocuments.Add(linkedNodeDocuments[i]); + ksNode = ksNode switch + { + CmsTreeK11 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, + CmsTreeK12 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, + CmsTreeK13 node => node with { NodeLinkedNodeID = null, NodeLinkedNodeSiteID = null }, + _ => ksNode + }; + + logger.LogWarning("Linked node with NodeGuid {NodeGuid} was materialized (Xperience by Kentico doesn't support links), it no longer serves as link to original document. This affect also routing, this document will have own link generated from node alias path", ksNode.NodeGUID); + } + } + + var ksNodeClass = modelFacade.SelectById(ksNode.NodeClassID) ?? throw new InvalidOperationException($"Node with missing class, node id '{ksNode.NodeID}'"); + string nodeClassClassName = ksNodeClass.ClassName; + if (classEntityConfiguration.ExcludeCodeNames.Contains(nodeClassClassName, StringComparer.InvariantCultureIgnoreCase)) + { + protocol.Warning(HandbookReferences.EntityExplicitlyExcludedByCodeName(nodeClassClassName, "PageType"), ksNode); + logger.LogWarning("Page: page of class {ClassName} was skipped => it is explicitly excluded in configuration", nodeClassClassName); + continue; + } + + if (nodeClassClassName == ClassCmsRoot) + { + logger.LogInformation("Root node skipped, V27 has no support for root nodes"); + continue; + } + + Debug.Assert(migratedDocuments.Count > 0, "migratedDocuments.Count > 0"); + + if (ksTreeOriginal is { NodeSKUID: not null }) + { + logger.LogWarning("Page '{NodeAliasPath}' has SKU bound, SKU info will be discarded", ksTreeOriginal.NodeAliasPath); + protocol.Append(HandbookReferences.NotCurrentlySupportedSkip() + .WithMessage($"Page '{ksTreeOriginal.NodeAliasPath}' has SKU bound, SKU info will be discarded") + .WithIdentityPrint(ksTreeOriginal) + .WithData(new { ksTreeOriginal.NodeSKUID }) + ); + } + + string safeNodeName = await contentItemNameProvider.Get(ksNode.NodeName); + var ksNodeParent = modelFacade.SelectById(ksNode.NodeParentID); + var nodeParentGuid = ksNodeParent?.NodeAliasPath == "/" || ksNodeParent == null + ? (Guid?)null + : spoiledGuidContext.EnsureNodeGuid(ksNodeParent); + + DataClassInfo targetClass = null!; + var classMapping = classMappingProvider.GetMapping(ksNodeClass.ClassName); + targetClass = classMapping != null + ? DataClassInfoProvider.ProviderObject.Get(classMapping.TargetClassName) + : DataClassInfoProvider.ProviderObject.Get(ksNodeClass.ClassGUID); + + var results = mapper.Map(new CmsTreeMapperSource( + ksNode, + safeNodeName, + ksSite.SiteGUID, + nodeParentGuid, + cultureCodeToLanguageGuid, + targetClass?.ClassFormDefinition, + ksNodeClass.ClassFormDefinition, + migratedDocuments, + ksSite + )); + try + { + WebPageItemInfo? webPageItemInfo = null; + var commonDataInfos = new List(); + foreach (var umtModel in results) + { + var result = await importer.ImportAsync(umtModel); + if (result is { Success: false }) + { + logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); + } + + switch (result) + { + case { Success: true, Imported: ContentItemCommonDataInfo ccid }: + { + commonDataInfos.Add(ccid); + Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } + case { Success: true, Imported: ContentItemLanguageMetadataInfo cclm }: + { + Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } + case { Success: true, Imported: WebPageItemInfo wp }: + { + webPageItemInfo = wp; + break; + } + + default: + break; + } + } + + AsserVersionStatusRule(commonDataInfos); + + if (webPageItemInfo != null && targetClass is { ClassWebPageHasUrl: true }) + { + await GenerateDefaultPageUrlPath(ksNode, webPageItemInfo, wasLinkedNode); + + foreach (var migratedDocument in migratedDocuments) + { + var languageGuid = cultureCodeToLanguageGuid[migratedDocument.DocumentCulture]; + + await MigratePageUrlPaths(ksSite.SiteGUID, + languageGuid, + commonDataInfos, + migratedDocument, + ksNode, + migratedDocument.DocumentCulture, + wasLinkedNode, webPageItemInfo); + } + + MigrateFormerUrls(ksNode, webPageItemInfo); + + var urls = WebPageUrlPathInfo.Provider.Get() + .WhereEquals(nameof(WebPageUrlPathInfo.WebPageUrlPathWebPageItemID), webPageItemInfo.WebPageItemID); + + if (urls.Count < 1) + { + logger.LogWarning("No url for page {Page}", new { webPageItemInfo.WebPageItemName, webPageItemInfo.WebPageItemTreePath, webPageItemInfo.WebPageItemGUID }); + } + } + else + { + logger.LogTrace("No webpage item produced for '{NodeAliasPath}'", ksNode.NodeAliasPath); + } + } + + catch (Exception ex) + { + protocol.Append(HandbookReferences + .ErrorCreatingTargetInstance(ex) + .NeedsManualAction() + .WithIdentityPrint(ksNode) + ); + logger.LogError("Failed to import content item: {Exception}", ex); + } + } + } + + await ExecDeferredPageBuilderPatch(); + + return new GenericCommandResult(); + } + + [Conditional("DEBUG")] + private static void AsserVersionStatusRule(List commonDataInfos) + { + foreach (var contentItemCommonDataInfos in commonDataInfos.GroupBy(x => x.ContentItemCommonDataContentLanguageID)) + { + VersionStatus? versionStatus = null; + bool onlyOneStatus = contentItemCommonDataInfos.Aggregate(true, (acc, i) => + { + try + { + if (versionStatus.HasValue) + { + return versionStatus != i.ContentItemCommonDataVersionStatus; + } + + return true; + } + finally + { + versionStatus = i.ContentItemCommonDataVersionStatus; + } + }); + Debug.Assert(onlyOneStatus); + } + } + + public enum PageRoutingModeEnum // copy of enum from KX13 dll + { + /// + /// Routing based on custom routes of standard MVC support. + /// + Custom = 0, + /// + /// Routing based on system routes driven by content tree structure. + /// + BasedOnContentTree = 1, + } + + private readonly Dictionary cmsClassCache = []; + private ICmsClass GetCmsClass(int classId) + { + if (cmsClassCache.TryGetValue(classId, out var cmsClass)) + { + return cmsClass; + } + + cmsClass = modelFacade.SelectById(classId); + cmsClassCache[classId] = cmsClass ?? throw new InvalidOperationException($"CMS Class with class id {classId} not found => invalid data"); + return cmsClass; + } + + private async Task MigratePageUrlPaths(Guid webSiteChannelGuid, Guid languageGuid, + List contentItemCommonDataInfos, ICmsDocument? ksDocument, ICmsTree ksTree, string documentCulture, bool wasLinkedNode, WebPageItemInfo webPageItemInfo) + { + var languageInfo = ContentLanguageInfoProvider.ProviderObject.Get(languageGuid); + var webSiteChannel = WebsiteChannelInfoProvider.ProviderObject.Get(webSiteChannelGuid); + + #region Migration of custom routing model + + if (modelFacade.SelectVersion() is { Major: 13 } && KenticoHelper.GetSettingsKey(modelFacade, ksTree.NodeSiteID, "CMSRoutingMode") is (int)PageRoutingModeEnum.Custom) + { + if (modelFacade.SelectById(ksTree.NodeSiteID) is not { } site) + { + logger.LogError("Unable to find source site with ID '{SiteID}', fallback url will be used for node {NodeID}", ksTree.NodeSiteID, ksTree.NodeID); + } + // for ability to resolve macros we query source instance where we can resolve marcos in url pattern for particular page + else if (!sourceInstanceContext.HasInfo) + { + logger.LogWarning("Cannot migrate url for document '{DocumentID}' / node '{NodeID}', source instance context is not available or set-up correctly - default fallback will be used.", ksDocument?.DocumentID, ksTree.NodeID); + } + else if (GetCmsClass(ksTree.NodeClassID) is { } cmsClass && string.IsNullOrWhiteSpace(cmsClass.ClassURLPattern)) + { + logger.LogWarning("Cannot migrate url for document '{DocumentID}' / node '{NodeID}', class {ClassName} has no url pattern set - cannot migrate to former url.", ksDocument?.DocumentID, ksTree.NodeID, cmsClass.ClassName); + } + else if (sourceInstanceContext.GetNodeUrls(ksTree.NodeID, site.SiteName) is not { Count: > 0 } pageModels) + { + logger.LogError("No information could be found in source instance about node {NodeID} on site {SiteName}", ksTree.NodeID, site.SiteName); + } + else if (pageModels.FirstOrDefault(pm => pm.DocumentCulture == documentCulture) is not { } pageModel) + { + logger.LogWarning("Page url information for document {DocumentID} / node {NodeID} not found for culture {Culture}", ksDocument?.DocumentID, ksTree.NodeID, documentCulture); + } + else if (string.IsNullOrWhiteSpace(pageModel.CultureUrl)) + { + logger.LogWarning("Page url information for document {DocumentID} / node {NodeID} was found for culture {Culture}, but culture url is empty - unexpected", ksDocument?.DocumentID, ksTree.NodeID, documentCulture); + } + else + { + string patchedUrl = pageModel.CultureUrl.TrimStart(['~']).TrimStart(['/']); + string urlHash = modelFacade.HashPath(patchedUrl); + var webPageFormerUrlPathInfo = new WebPageFormerUrlPathInfo + { + WebPageFormerUrlPath = patchedUrl, + WebPageFormerUrlPathHash = urlHash, + WebPageFormerUrlPathWebPageItemID = webPageItemInfo.WebPageItemID, + WebPageFormerUrlPathWebsiteChannelID = webSiteChannel.WebsiteChannelID, + WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID, + WebPageFormerUrlPathLastModified = Service.Resolve().GetDateTimeNow() + }; + WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); + logger.LogEntitySetAction(true, webPageItemInfo); + return; + } + } + + #endregion + + if (modelFacade.IsAvailable()) + { + var ksPaths = modelFacade.SelectWhere("PageUrlPathNodeId = @nodeId AND PageUrlPathCulture = @culture", + new SqlParameter("nodeId", ksTree.NodeID), + new SqlParameter("culture", documentCulture) + ).ToList(); + + if (ksPaths.Count > 0) + { + foreach (var ksPath in ksPaths) + { + logger.LogTrace("Page url path: C={Culture} S={Site} P={Path}", ksPath.PageUrlPathCulture, ksPath.PageUrlPathSiteID, ksPath.PageUrlPathUrlPath); + + foreach (var contentItemCommonDataInfo in contentItemCommonDataInfos.Where(x => x.ContentItemCommonDataContentLanguageID == languageInfo.ContentLanguageID)) + { + logger.LogTrace("Page url path common data info: CIID={ContentItemId} CLID={Language} ID={Id}", contentItemCommonDataInfo.ContentItemCommonDataContentItemID, + contentItemCommonDataInfo.ContentItemCommonDataContentLanguageID, contentItemCommonDataInfo.ContentItemCommonDataID); + + Debug.Assert(!string.IsNullOrWhiteSpace(ksPath.PageUrlPathUrlPath), "!string.IsNullOrWhiteSpace(kx13PageUrlPath.PageUrlPathUrlPath)"); + + var webPageUrlPath = new WebPageUrlPathModel + { + WebPageUrlPathGUID = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus == VersionStatus.Draft + ? Guid.NewGuid() + : ksPath.PageUrlPathGUID, + WebPageUrlPath = ksPath.PageUrlPathUrlPath.TrimStart('/'), + WebPageUrlPathHash = ksPath.PageUrlPathUrlPathHash, + WebPageUrlPathWebPageItemGuid = webPageItemInfo.WebPageItemGUID, + WebPageUrlPathWebsiteChannelGuid = webSiteChannelGuid, + WebPageUrlPathContentLanguageGuid = languageGuid, + WebPageUrlPathIsLatest = contentItemCommonDataInfo.ContentItemCommonDataIsLatest, + WebPageUrlPathIsDraft = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus switch + { + VersionStatus.InitialDraft => false, + VersionStatus.Draft => true, + VersionStatus.Published => false, + VersionStatus.Unpublished => false, + _ => throw new ArgumentOutOfRangeException() + } + }; + + CheckPathAlreadyExists(webPageUrlPath, languageInfo, webSiteChannel, webPageItemInfo.WebPageItemID); + + var importResult = await importer.ImportAsync(webPageUrlPath); + + LogImportResult(importResult); + } + } + } + } + else + { + foreach (var contentItemCommonDataInfo in contentItemCommonDataInfos.Where(x => x.ContentItemCommonDataContentLanguageID == languageInfo.ContentLanguageID)) + { + logger.LogTrace("Page url path common data info: CIID={ContentItemId} CLID={Language} ID={Id}", contentItemCommonDataInfo.ContentItemCommonDataContentItemID, + contentItemCommonDataInfo.ContentItemCommonDataContentLanguageID, contentItemCommonDataInfo.ContentItemCommonDataID); + + string? urlPath = (ksDocument switch + { + CmsDocumentK11 doc => wasLinkedNode ? null : doc.DocumentUrlPath, + CmsDocumentK12 doc => wasLinkedNode ? null : doc.DocumentUrlPath, + _ => null + }).NullIf(string.Empty)?.TrimStart('/'); + + if (urlPath is not null) + { + var webPageUrlPath = new WebPageUrlPathModel + { + WebPageUrlPathGUID = GuidHelper.CreateWebPageUrlPathGuid($"{urlPath}|{documentCulture}|{webSiteChannel.WebsiteChannelGUID}|{ksTree.NodeID}"), + WebPageUrlPath = urlPath, + WebPageUrlPathWebPageItemGuid = webPageItemInfo.WebPageItemGUID, + WebPageUrlPathWebsiteChannelGuid = webSiteChannelGuid, + WebPageUrlPathContentLanguageGuid = languageGuid, + WebPageUrlPathIsLatest = contentItemCommonDataInfo.ContentItemCommonDataIsLatest, + WebPageUrlPathIsDraft = contentItemCommonDataInfo.ContentItemCommonDataVersionStatus switch + { + VersionStatus.InitialDraft => false, + VersionStatus.Draft => true, + VersionStatus.Published => false, + VersionStatus.Unpublished => false, + _ => throw new ArgumentOutOfRangeException() + } + }; + + CheckPathAlreadyExists(webPageUrlPath, languageInfo, webSiteChannel, webPageItemInfo.WebPageItemID); + + var importResult = await importer.ImportAsync(webPageUrlPath); + + LogImportResult(importResult); + } + } + } + } + + private async Task GenerateDefaultPageUrlPath(ICmsTree ksTree, WebPageItemInfo webPageItemInfo, bool wasLinkedNode) + { + var man = Service.Resolve(); + string alias = wasLinkedNode ? ksTree.NodeAlias : ksTree.NodeAliasPath; + var collisionData = await man.GeneratePageUrlPath(webPageItemInfo, alias, VersionStatus.InitialDraft, CancellationToken.None); + foreach (var data in collisionData) + { + logger.LogError("WebPageUrlPath collision occured {Path}", data.Path); + } + } + + private void CheckPathAlreadyExists(WebPageUrlPathModel webPageUrlPath, + ContentLanguageInfo languageInfo, + WebsiteChannelInfo webSiteChannel, int webPageItemId) + { + Debug.Assert(webPageUrlPath is not { WebPageUrlPathIsLatest: false, WebPageUrlPathIsDraft: true }, "webPageUrlPath is not { WebPageUrlPathIsLatest: false, WebPageUrlPathIsDraft: true }"); + + var existingPaths = WebPageUrlPathInfo.Provider.Get() + .WhereEquals(nameof(WebPageUrlPathInfo.WebPageUrlPathWebPageItemID), webPageItemId) + .ToList(); + + var ep = existingPaths.FirstOrDefault(ep => + ep.WebPageUrlPathContentLanguageID == languageInfo.ContentLanguageID && + ep.WebPageUrlPathIsDraft == webPageUrlPath.WebPageUrlPathIsDraft && + ep.WebPageUrlPathWebsiteChannelID == webSiteChannel.WebsiteChannelID && + ep.WebPageUrlPathWebPageItemID == webPageItemId + ); + + if (ep != null) + { + webPageUrlPath.WebPageUrlPathGUID = ep.WebPageUrlPathGUID; + logger.LogTrace("Existing page url path found for '{Path}', fixing GUID to '{Guid}'", webPageUrlPath.WebPageUrlPath, webPageUrlPath.WebPageUrlPathGUID); + } + } + + private void LogImportResult(IImportResult importResult) + { + switch (importResult) + { + case { Success: true, Imported: WebPageUrlPathInfo imported }: + { + logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); + break; + } + case { Success: false, Exception: { } exception }: + { + logger.LogError("Failed to import page url path: {Error}", exception.ToString()); + break; + } + case { Success: false, ModelValidationResults: { } validation }: + { + foreach (var validationResult in validation) + { + logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); + } + + break; + } + + default: + break; + } + } + + private void MigrateFormerUrls(ICmsTree ksNode, WebPageItemInfo targetPage) + { + if (modelFacade.IsAvailable()) + { + var formerUrlPaths = modelFacade.SelectWhere( + "PageFormerUrlPathSiteID = @siteId AND PageFormerUrlPathNodeID = @nodeId", + new SqlParameter("siteId", ksNode.NodeSiteID), + new SqlParameter("nodeId", ksNode.NodeID) + ); + foreach (var cmsPageFormerUrlPath in formerUrlPaths) + { + logger.LogDebug("PageFormerUrlPath migration '{PageFormerUrlPath}' ", cmsPageFormerUrlPath); + protocol.FetchedSource(cmsPageFormerUrlPath); + + switch (cmsPageFormerUrlPath) + { + case CmsPageFormerUrlPathK11: + case CmsPageFormerUrlPathK12: + { + logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); + break; + } + case CmsPageFormerUrlPathK13 pfup: + { + try + { + var languageInfo = languages.GetOrAdd( + pfup.PageFormerUrlPathCulture, + s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") + ); + + var ktPath = WebPageFormerUrlPathInfo.Provider.Get() + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) + .SingleOrDefault(); + + if (ktPath != null) + { + protocol.FetchedTarget(ktPath); + } + + var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); + webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; + webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; + + WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); + logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); + } + catch (Exception ex) + { + protocol.Append(HandbookReferences + .ErrorCreatingTargetInstance(ex) + .NeedsManualAction() + .WithIdentityPrint(pfup) + ); + logger.LogError("Failed to import page former url path: {Exception}", ex); + } + + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(cmsPageFormerUrlPath)); + } + } + } + else + { + logger.LogDebug("CmsPageFormerUrlPath not supported in source instance"); + } + } + + internal static QueryExpression GetWebPageUrlPathHashQueryExpression(string urlPath) => $"CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', LOWER(N'{SqlHelper.EscapeQuotes(urlPath)}')), 2)".AsExpression(); + + #region Deffered patch + + private async Task ExecDeferredPageBuilderPatch() + { + logger.LogInformation("Executing TreePath patch"); + + foreach ((var uniqueId, string className, int webSiteChannelId) in deferredPathService.GetWidgetsToPatch()) + { + if (className == ContentItemCommonDataInfo.TYPEINFO.ObjectClassName) + { + var contentItemCommonDataInfo = await ContentItemCommonDataInfo.Provider.GetAsync(uniqueId); + + contentItemCommonDataInfo.ContentItemCommonDataPageBuilderWidgets = DeferredPatchPageBuilderWidgets( + contentItemCommonDataInfo.ContentItemCommonDataPageBuilderWidgets, webSiteChannelId, out bool anythingChangedW); + contentItemCommonDataInfo.ContentItemCommonDataPageTemplateConfiguration = DeferredPatchPageTemplateConfiguration( + contentItemCommonDataInfo.ContentItemCommonDataPageTemplateConfiguration, webSiteChannelId, out bool anythingChangedC); + + if (anythingChangedC || anythingChangedW) + { + contentItemCommonDataInfo.Update(); + } + } + else if (className == PageTemplateConfigurationInfo.TYPEINFO.ObjectClassName) + { + var pageTemplateConfigurationInfo = await PageTemplateConfigurationInfo.Provider.GetAsync(uniqueId); + pageTemplateConfigurationInfo.PageTemplateConfigurationWidgets = DeferredPatchPageBuilderWidgets( + pageTemplateConfigurationInfo.PageTemplateConfigurationWidgets, + webSiteChannelId, + out bool anythingChangedW + ); + pageTemplateConfigurationInfo.PageTemplateConfigurationTemplate = DeferredPatchPageTemplateConfiguration( + pageTemplateConfigurationInfo.PageTemplateConfigurationTemplate, + webSiteChannelId, + out bool anythingChangedC + ); + if (anythingChangedW || anythingChangedC) + { + PageTemplateConfigurationInfo.Provider.Set(pageTemplateConfigurationInfo); + } + } + } + } + + private string DeferredPatchPageTemplateConfiguration(string documentPageTemplateConfiguration, int webSiteChannelId, out bool anythingChanged) + { + if (!string.IsNullOrWhiteSpace(documentPageTemplateConfiguration)) + { + var configuration = JObject.Parse(documentPageTemplateConfiguration); + PageBuilderWidgetsPatcher.DeferredPatchProperties(configuration, TreePathConvertor.GetSiteConverter(webSiteChannelId), out anythingChanged); + return JsonConvert.SerializeObject(configuration); + } + + anythingChanged = false; + return documentPageTemplateConfiguration; + } + + private string DeferredPatchPageBuilderWidgets(string documentPageBuilderWidgets, int webSiteChannelId, out bool anythingChanged) + { + if (!string.IsNullOrWhiteSpace(documentPageBuilderWidgets)) + { + var patched = PageBuilderWidgetsPatcher.DeferredPatchConfiguration( + JsonConvert.DeserializeObject(documentPageBuilderWidgets), + TreePathConvertor.GetSiteConverter(webSiteChannelId), + out anythingChanged + ); + return JsonConvert.SerializeObject(patched); + } + + anythingChanged = false; + return documentPageBuilderWidgets; + } + + #endregion +} diff --git a/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs b/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs index 29b44d5a..fb7ccf75 100644 --- a/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs +++ b/KVA/Migration.Tool.Source/Helpers/PageBuilderWidgetsPatcher.cs @@ -1,79 +1,79 @@ -using Migration.Tool.Common.Helpers; -using Migration.Tool.Common.Model; -using Newtonsoft.Json.Linq; - -namespace Migration.Tool.Source.Helpers; - -public static class PageBuilderWidgetsPatcher -{ - public static EditableAreasConfiguration DeferredPatchConfiguration(EditableAreasConfiguration configuration, TreePathConvertor convertor, out bool anythingChanged) - { - anythingChanged = false; - foreach (var configurationEditableArea in configuration.EditableAreas ?? []) - { - foreach (var sectionConfiguration in configurationEditableArea.Sections ?? []) - { - foreach (var sectionConfigurationZone in sectionConfiguration.Zones ?? []) - { - foreach (var configurationZoneWidget in sectionConfigurationZone.Widgets ?? []) - { - DeferredPatchWidget(configurationZoneWidget, convertor, out bool anythingChangedTmp); - anythingChanged = anythingChanged || anythingChangedTmp; - } - } - } - } - - return configuration; - } - - private static void DeferredPatchWidget(WidgetConfiguration? configurationZoneWidget, TreePathConvertor convertor, out bool anythingChanged) - { - anythingChanged = false; - if (configurationZoneWidget == null) - { - return; - } - - var list = configurationZoneWidget.Variants ?? []; - for (int i = 0; i < list.Count; i++) - { - if (list[i] is { } variantJson) - { - var variant = JObject.FromObject(variantJson); - DeferredPatchProperties(variant, convertor, out bool anythingChangedTmp); - - list[i] = variant.ToObject(); - anythingChanged = anythingChanged || anythingChangedTmp; - } - } - } - - public static void DeferredPatchProperties(JObject propertyContainer, TreePathConvertor convertor, out bool anythingChanged) - { - anythingChanged = false; - if (propertyContainer?["properties"] is JObject { Count: 1 } properties) - { - foreach ((string key, var value) in properties) - { - switch (key) - { - case "TreePath" when value?.Value() is { } nodeAliasPath: - { - string treePath = convertor.GetConvertedOrUnchangedAssumingChannel(nodeAliasPath); - if (!TreePathConvertor.TreePathComparer.Equals(nodeAliasPath, treePath)) - { - properties["TreePath"] = JToken.FromObject(treePath); - anythingChanged = true; - } - - break; - } - - default: - break; - } - } - } - } -} +using Migration.Tool.Common.Helpers; +using Migration.Tool.Common.Model; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Source.Helpers; + +public static class PageBuilderWidgetsPatcher +{ + public static EditableAreasConfiguration DeferredPatchConfiguration(EditableAreasConfiguration configuration, TreePathConvertor convertor, out bool anythingChanged) + { + anythingChanged = false; + foreach (var configurationEditableArea in configuration.EditableAreas ?? []) + { + foreach (var sectionConfiguration in configurationEditableArea.Sections ?? []) + { + foreach (var sectionConfigurationZone in sectionConfiguration.Zones ?? []) + { + foreach (var configurationZoneWidget in sectionConfigurationZone.Widgets ?? []) + { + DeferredPatchWidget(configurationZoneWidget, convertor, out bool anythingChangedTmp); + anythingChanged = anythingChanged || anythingChangedTmp; + } + } + } + } + + return configuration; + } + + private static void DeferredPatchWidget(WidgetConfiguration? configurationZoneWidget, TreePathConvertor convertor, out bool anythingChanged) + { + anythingChanged = false; + if (configurationZoneWidget == null) + { + return; + } + + var list = configurationZoneWidget.Variants ?? []; + for (int i = 0; i < list.Count; i++) + { + if (list[i] is { } variantJson) + { + var variant = JObject.FromObject(variantJson); + DeferredPatchProperties(variant, convertor, out bool anythingChangedTmp); + + list[i] = variant.ToObject(); + anythingChanged = anythingChanged || anythingChangedTmp; + } + } + } + + public static void DeferredPatchProperties(JObject propertyContainer, TreePathConvertor convertor, out bool anythingChanged) + { + anythingChanged = false; + if (propertyContainer?["properties"] is JObject { Count: 1 } properties) + { + foreach ((string key, var value) in properties) + { + switch (key) + { + case "TreePath" when value?.Value() is { } nodeAliasPath: + { + string treePath = convertor.GetConvertedOrUnchangedAssumingChannel(nodeAliasPath); + if (!TreePathConvertor.TreePathComparer.Equals(nodeAliasPath, treePath)) + { + properties["TreePath"] = JToken.FromObject(treePath); + anythingChanged = true; + } + + break; + } + + default: + break; + } + } + } + } +} diff --git a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs index 244bfac3..ab34e833 100644 --- a/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs +++ b/KVA/Migration.Tool.Source/KsCoreDiExtensions.cs @@ -1,109 +1,109 @@ -using CMS.DataEngine; -using CMS.FormEngine; -using CMS.MediaLibrary; -using CMS.Membership; -using CMS.Modules; -using CMS.OnlineForms; -using CMS.Websites; - -using Kentico.Xperience.UMT; - -using MediatR; - -using Microsoft.Extensions.DependencyInjection; - -using Migration.Tool.Common; -using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.MigrationProtocol; -using Migration.Tool.Common.Services; -using Migration.Tool.Common.Services.BulkCopy; -using Migration.Tool.Common.Services.Ipc; -using Migration.Tool.KXP.Models; -using Migration.Tool.Source.Auxiliary; -using Migration.Tool.Source.Behaviors; -using Migration.Tool.Source.Contexts; -using Migration.Tool.Source.Helpers; -using Migration.Tool.Source.Mappers; -using Migration.Tool.Source.Model; -using Migration.Tool.Source.Providers; -using Migration.Tool.Source.Services; - -namespace Migration.Tool.Source; - -public static class KsCoreDiExtensions -{ - public static IServiceProvider ServiceProvider { get; private set; } = null!; - public static void InitServiceProvider(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider; - - public static IServiceCollection UseKsToolCore(this IServiceCollection services, bool? migrateMediaToMediaLibrary = false) - { - var printService = new PrintService(); - services.AddSingleton(printService); - HandbookReference.PrintService = printService; - LogExtensions.PrintService = printService; - - services.AddTransient(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(s => s.GetRequiredService() as SpoiledGuidContext ?? throw new InvalidOperationException()); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - if (migrateMediaToMediaLibrary ?? false) - { - services.AddScoped(); - services.AddScoped(); - } - else - { - services.AddScoped(); - services.AddScoped(); - } - services.AddScoped(); - services.AddScoped(); - - services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(KsCoreDiExtensions).Assembly)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestHandlingBehavior<,>)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandConstraintBehavior<,>)); - services.AddTransient(typeof(IPipelineBehavior<,>), typeof(XbKApiContextBehavior<,>)); - - services.AddSingleton(); - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - - services.AddScoped(); - services.AddScoped(s => s.GetRequiredService()); - services.AddScoped(); - - // umt mappers - services.AddTransient, ContentItemMapper>(); - services.AddTransient, TagMapper>(); - - // mappers - services.AddTransient, CmsAttachmentMapper>(); - services.AddTransient, CmsClassMapper>(); - services.AddTransient, CmsFormMapper>(); - services.AddTransient, CmsFormMapperEf>(); - services.AddTransient, ResourceMapper>(); - services.AddTransient, AlternativeFormMapper>(); - services.AddTransient, MemberInfoMapper>(); - services.AddTransient, PageTemplateConfigurationMapper>(); - services.AddTransient, MediaLibraryInfoMapper>(); - services.AddTransient, MediaFileInfoMapper>(); - - services.AddUniversalMigrationToolkit(); - - return services; - } -} +using CMS.DataEngine; +using CMS.FormEngine; +using CMS.MediaLibrary; +using CMS.Membership; +using CMS.Modules; +using CMS.OnlineForms; +using CMS.Websites; + +using Kentico.Xperience.UMT; + +using MediatR; + +using Microsoft.Extensions.DependencyInjection; + +using Migration.Tool.Common; +using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.MigrationProtocol; +using Migration.Tool.Common.Services; +using Migration.Tool.Common.Services.BulkCopy; +using Migration.Tool.Common.Services.Ipc; +using Migration.Tool.KXP.Models; +using Migration.Tool.Source.Auxiliary; +using Migration.Tool.Source.Behaviors; +using Migration.Tool.Source.Contexts; +using Migration.Tool.Source.Helpers; +using Migration.Tool.Source.Mappers; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; +using Migration.Tool.Source.Services; + +namespace Migration.Tool.Source; + +public static class KsCoreDiExtensions +{ + public static IServiceProvider ServiceProvider { get; private set; } = null!; + public static void InitServiceProvider(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider; + + public static IServiceCollection UseKsToolCore(this IServiceCollection services, bool? migrateMediaToMediaLibrary = false) + { + var printService = new PrintService(); + services.AddSingleton(printService); + HandbookReference.PrintService = printService; + LogExtensions.PrintService = printService; + + services.AddTransient(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(s => s.GetRequiredService() as SpoiledGuidContext ?? throw new InvalidOperationException()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + if (migrateMediaToMediaLibrary ?? false) + { + services.AddScoped(); + services.AddScoped(); + } + else + { + services.AddScoped(); + services.AddScoped(); + } + services.AddScoped(); + services.AddScoped(); + + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(KsCoreDiExtensions).Assembly)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestHandlingBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(CommandConstraintBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(XbKApiContextBehavior<,>)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + + services.AddScoped(); + services.AddScoped(s => s.GetRequiredService()); + services.AddScoped(); + + // umt mappers + services.AddTransient, ContentItemMapper>(); + services.AddTransient, TagMapper>(); + + // mappers + services.AddTransient, CmsAttachmentMapper>(); + services.AddTransient, CmsClassMapper>(); + services.AddTransient, CmsFormMapper>(); + services.AddTransient, CmsFormMapperEf>(); + services.AddTransient, ResourceMapper>(); + services.AddTransient, AlternativeFormMapper>(); + services.AddTransient, MemberInfoMapper>(); + services.AddTransient, PageTemplateConfigurationMapper>(); + services.AddTransient, MediaLibraryInfoMapper>(); + services.AddTransient, MediaFileInfoMapper>(); + + services.AddUniversalMigrationToolkit(); + + return services; + } +} diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index 399ac595..6f924e61 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -1,734 +1,734 @@ -using System.Diagnostics; -using CMS.ContentEngine; -using CMS.ContentEngine.Internal; -using CMS.Core; -using CMS.Core.Internal; -using CMS.DataEngine; -using CMS.FormEngine; -using CMS.Websites; -using CMS.Websites.Internal; -using Kentico.Xperience.UMT.Model; -using Microsoft.Extensions.Logging; -using Migration.Tool.Common; -using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.Builders; -using Migration.Tool.Common.Helpers; -using Migration.Tool.Common.Services; -using Migration.Tool.KXP.Api.Auxiliary; -using Migration.Tool.KXP.Api.Services.CmsClass; -using Migration.Tool.Source.Auxiliary; -using Migration.Tool.Source.Contexts; -using Migration.Tool.Source.Helpers; -using Migration.Tool.Source.Model; -using Migration.Tool.Source.Providers; -using Migration.Tool.Source.Services; -using Newtonsoft.Json.Linq; - -namespace Migration.Tool.Source.Mappers; - -public record CmsTreeMapperSource( - ICmsTree CmsTree, - string SafeNodeName, - Guid SiteGuid, - Guid? NodeParentGuid, - Dictionary CultureToLanguageGuid, - string? TargetFormDefinition, - string SourceFormDefinition, - List MigratedDocuments, - ICmsSite SourceSite -); - -public class ContentItemMapper( - ILogger logger, - CoupledDataService coupledDataService, - IAttachmentMigrator attachmentMigrator, - CmsRelationshipService relationshipService, - FieldMigrationService fieldMigrationService, - ModelFacade modelFacade, - ReusableSchemaService reusableSchemaService, - DeferredPathService deferredPathService, - SpoiledGuidContext spoiledGuidContext, - IAssetFacade assetFacade, - MediaLinkServiceFactory mediaLinkServiceFactory, - ToolConfiguration configuration, - ClassMappingProvider classMappingProvider, - PageBuilderPatcher pageBuilderPatcher - ) : UmtMapperBase -{ - private const string CLASS_FIELD_CONTROL_NAME = "controlname"; - - protected override IEnumerable MapInternal(CmsTreeMapperSource source) - { - (var cmsTree, string safeNodeName, var siteGuid, var nodeParentGuid, var cultureToLanguageGuid, string? targetFormDefinition, string sourceFormDefinition, var migratedDocuments, var sourceSite) = source; - - logger.LogTrace("Mapping {Value}", new { cmsTree.NodeAliasPath, cmsTree.NodeName, cmsTree.NodeGUID, cmsTree.NodeSiteID }); - - var sourceNodeClass = modelFacade.SelectById(cmsTree.NodeClassID) ?? throw new InvalidOperationException($"Fatal: node class is missing, class id '{cmsTree.NodeClassID}'"); - var mapping = classMappingProvider.GetMapping(sourceNodeClass.ClassName); - var targetClassGuid = sourceNodeClass.ClassGUID; - if (mapping != null) - { - targetClassGuid = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName)?.ClassGUID ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); - } - - bool migratedAsContentFolder = sourceNodeClass.ClassName.Equals("cms.folder", StringComparison.InvariantCultureIgnoreCase) && !configuration.UseDeprecatedFolderPageType.GetValueOrDefault(false); - - var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); - yield return new ContentItemModel - { - ContentItemGUID = contentItemGuid, - ContentItemName = safeNodeName, - ContentItemIsReusable = false, // page is not reusable - ContentItemIsSecured = cmsTree.IsSecuredNode ?? false, - ContentItemDataClassGuid = migratedAsContentFolder ? null : targetClassGuid, - ContentItemChannelGuid = siteGuid - }; - - var targetWebPage = WebPageItemInfo.Provider.Get() - .WhereEquals(nameof(WebPageItemInfo.WebPageItemGUID), contentItemGuid) - .FirstOrDefault(); - string? treePath = targetWebPage?.WebPageItemTreePath; - - var websiteChannelInfo = WebsiteChannelInfoProvider.ProviderObject.Get(siteGuid); - var treePathConvertor = TreePathConvertor.GetSiteConverter(websiteChannelInfo.WebsiteChannelID); - if (treePath == null) - { - (bool treePathIsDifferent, treePath) = treePathConvertor.ConvertAndEnsureUniqueness(cmsTree.NodeAliasPath).GetAwaiter().GetResult(); - if (treePathIsDifferent) - { - logger.LogInformation($"Original node alias path '{cmsTree.NodeAliasPath}' of '{cmsTree.NodeName}' item was converted to '{treePath}' since the value does not allow original range of allowed characters."); - } - } - - foreach (var cmsDocument in migratedDocuments) - { - if (!cultureToLanguageGuid.TryGetValue(cmsDocument.DocumentCulture, out var languageGuid)) - { - logger.LogWarning("Document '{DocumentGUID}' was skipped, unknown culture", cmsDocument.DocumentGUID); - continue; - } - - bool hasDraft = cmsDocument.DocumentPublishedVersionHistoryID is not null && - cmsDocument.DocumentPublishedVersionHistoryID != cmsDocument.DocumentCheckedOutVersionHistoryID; - - var checkoutVersion = hasDraft - ? modelFacade.SelectById(cmsDocument.DocumentCheckedOutVersionHistoryID) - : null; - - bool draftMigrated = false; - if (checkoutVersion is { PublishFrom: null } draftVersion && !migratedAsContentFolder) - { - List? migratedDraft = null; - try - { - migratedDraft = MigrateDraft(draftVersion, cmsTree, sourceFormDefinition, targetFormDefinition, contentItemGuid, languageGuid, sourceNodeClass, websiteChannelInfo, sourceSite, mapping).ToList(); - draftMigrated = true; - } - catch - { - logger.LogWarning("Failed to migrate checkout version of document with DocumentID={CmsDocumentDocumentId} VersionHistoryID={CmsDocumentDocumentCheckedOutVersionHistoryId}", - cmsDocument.DocumentID, cmsDocument.DocumentCheckedOutVersionHistoryID); - draftMigrated = false; - } - - if (migratedDraft != null) - { - foreach (var umtModel in migratedDraft) - { - yield return umtModel; - } - } - } - - var versionStatus = cmsDocument switch - { - { DocumentIsArchived: true } => VersionStatus.Unpublished, - { DocumentPublishedVersionHistoryID: null, DocumentCheckedOutVersionHistoryID: null } => VersionStatus.Published, - { DocumentPublishedVersionHistoryID: { } pubId, DocumentCheckedOutVersionHistoryID: { } chId } when pubId <= chId => VersionStatus.Published, - { DocumentPublishedVersionHistoryID: null, DocumentCheckedOutVersionHistoryID: not null } => VersionStatus.InitialDraft, - _ => draftMigrated ? VersionStatus.Published : VersionStatus.InitialDraft - }; - if (migratedAsContentFolder) - { - versionStatus = VersionStatus.Published; // folder is automatically published - } - - DateTime? scheduledPublishWhen = null; - DateTime? scheduleUnpublishWhen = null; - string? contentItemCommonDataPageBuilderWidgets = null; - string? contentItemCommonDataPageTemplateConfiguration = null; - - bool ndp = false; - if (!migratedAsContentFolder) - { - if (cmsDocument.DocumentPublishFrom is { } publishFrom) - { - var now = Service.Resolve().GetDateTimeNow(); - if (publishFrom > now) - { - versionStatus = VersionStatus.Unpublished; - } - else - { - scheduledPublishWhen = publishFrom; - } - } - - if (cmsDocument.DocumentPublishTo is { } publishTo) - { - var now = Service.Resolve().GetDateTimeNow(); - if (publishTo < now) - { - versionStatus = VersionStatus.Unpublished; - } - else - { - scheduleUnpublishWhen = publishTo; - } - } - - switch (cmsDocument) - { - case CmsDocumentK11: - { - break; - } - case CmsDocumentK12 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } - case CmsDocumentK13 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } - - default: - break; - } - - (contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets, ndp) = pageBuilderPatcher.PatchJsonDefinitions(source.CmsTree.NodeSiteID, contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets).GetAwaiter().GetResult(); - } - - var documentGuid = spoiledGuidContext.EnsureDocumentGuid( - cmsDocument.DocumentGUID ?? throw new InvalidOperationException("DocumentGUID is null"), - cmsTree.NodeSiteID, - cmsTree.NodeID, - cmsDocument.DocumentID - ); - - var commonDataModel = new ContentItemCommonDataModel - { - ContentItemCommonDataGUID = documentGuid, - ContentItemCommonDataContentItemGuid = contentItemGuid, - ContentItemCommonDataContentLanguageGuid = languageGuid, // DocumentCulture -> language entity needs to be created and its ID used here - ContentItemCommonDataVersionStatus = versionStatus, - ContentItemCommonDataIsLatest = !draftMigrated, // Flag for latest record to know what to retrieve for the UI - ContentItemCommonDataPageBuilderWidgets = contentItemCommonDataPageBuilderWidgets, - ContentItemCommonDataPageTemplateConfiguration = contentItemCommonDataPageTemplateConfiguration, - }; - - if (ndp) - { - deferredPathService.AddPatch( - commonDataModel.ContentItemCommonDataGUID ?? throw new InvalidOperationException("DocumentGUID is null"), - ContentItemCommonDataInfo.TYPEINFO.ObjectClassName, - websiteChannelInfo.WebsiteChannelID - ); - } - - if (!migratedAsContentFolder) - { - var dataModel = new ContentItemDataModel { ContentItemDataGUID = commonDataModel.ContentItemCommonDataGUID, ContentItemDataCommonDataGuid = commonDataModel.ContentItemCommonDataGUID, ContentItemContentTypeName = mapping?.TargetClassName ?? sourceNodeClass.ClassName }; - - var fi = new FormInfo(targetFormDefinition); - if (sourceNodeClass.ClassIsCoupledClass) - { - var sfi = new FormInfo(sourceFormDefinition); - string primaryKeyName = ""; - foreach (var sourceFieldInfo in sfi.GetFields(true, true)) - { - if (sourceFieldInfo.PrimaryKey) - { - primaryKeyName = sourceFieldInfo.Name; - } - } - - if (string.IsNullOrWhiteSpace(primaryKeyName)) - { - throw new Exception("Error, unable to find coupled data primary key"); - } - - var commonFields = UnpackReusableFieldSchemas(fi.GetFields()).ToArray(); - var targetColumns = commonFields - .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) - .Union(fi.GetColumnNames(false)) - .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) - .ToList(); - - var coupledDataRow = coupledDataService.GetSourceCoupledDataRow(sourceNodeClass.ClassTableName, primaryKeyName, cmsDocument.DocumentForeignKeyValue); - // TODO tomas.krch: 2024-09-05 propagate async to root - MapCoupledDataFieldValues(dataModel.CustomProperties, - columnName => coupledDataRow?[columnName], - columnName => coupledDataRow?.ContainsKey(columnName) ?? false, - cmsTree, cmsDocument.DocumentID, - targetColumns, sfi, fi, - false, sourceNodeClass, sourceSite, mapping - ).GetAwaiter().GetResult(); - - foreach (var formFieldInfo in commonFields) - { - string originalFieldName = ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, formFieldInfo.Name); - if (dataModel.CustomProperties.TryGetValue(originalFieldName, out object? value)) - { - commonDataModel.CustomProperties ??= []; - logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' populated", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); - commonDataModel.CustomProperties[formFieldInfo.Name] = value; - dataModel.CustomProperties.Remove(originalFieldName); - } - else - { - logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' missing", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); - } - } - } - - string targetClassName = mapping?.TargetClassName ?? sourceNodeClass.ClassName; - if (CmsClassMapper.GetLegacyDocumentName(fi, targetClassName) is { } legacyDocumentNameFieldName) - { - if (reusableSchemaService.IsConversionToReusableFieldSchemaRequested(targetClassName)) - { - string fieldName = ReusableSchemaService.GetUniqueFieldName(targetClassName, legacyDocumentNameFieldName); - commonDataModel.CustomProperties.Add(fieldName, cmsDocument.DocumentName); - } - else - { - dataModel.CustomProperties.Add(legacyDocumentNameFieldName, cmsDocument.DocumentName); - } - } - - yield return commonDataModel; - yield return dataModel; - } - - Guid? documentCreatedByUserGuid = null; - if (modelFacade.TrySelectGuid(cmsDocument.DocumentCreatedByUserID, out var createdByUserGuid)) - { - documentCreatedByUserGuid = createdByUserGuid; - } - - Guid? documentModifiedByUserGuid = null; - if (modelFacade.TrySelectGuid(cmsDocument.DocumentModifiedByUserID, out var modifiedByUserGuid)) - { - documentModifiedByUserGuid = modifiedByUserGuid; - } - - var languageMetadataInfo = new ContentItemLanguageMetadataModel - { - ContentItemLanguageMetadataGUID = documentGuid, - ContentItemLanguageMetadataContentItemGuid = contentItemGuid, - ContentItemLanguageMetadataDisplayName = cmsDocument.DocumentName, // For the admin UI only - ContentItemLanguageMetadataLatestVersionStatus = draftMigrated ? VersionStatus.Draft : versionStatus, // That's the latest status of th item for admin optimization - ContentItemLanguageMetadataCreatedWhen = cmsDocument.DocumentCreatedWhen, // DocumentCreatedWhen - ContentItemLanguageMetadataModifiedWhen = cmsDocument.DocumentModifiedWhen, // DocumentModifiedWhen - ContentItemLanguageMetadataCreatedByUserGuid = documentCreatedByUserGuid, - ContentItemLanguageMetadataModifiedByUserGuid = documentModifiedByUserGuid, - // logic inaccessible, not supported - // ContentItemLanguageMetadataHasImageAsset = ContentItemAssetHasImageArbiter.HasImage(contentItemDataInfo), // This is for admin UI optimization - set to true if latest version contains a field with an image asset - ContentItemLanguageMetadataHasImageAsset = false, - ContentItemLanguageMetadataContentLanguageGuid = languageGuid, // DocumentCulture -> language entity needs to be created and its ID used here - ContentItemLanguageMetadataScheduledPublishWhen = scheduledPublishWhen, - ContentItemLanguageMetadataScheduledUnpublishWhen = scheduleUnpublishWhen - }; - yield return languageMetadataInfo; - } - - // mapping of linked nodes is not supported - Debug.Assert(cmsTree.NodeLinkedNodeID == null, "cmsTree.NodeLinkedNodeId == null"); - Debug.Assert(cmsTree.NodeLinkedNodeSiteID == null, "cmsTree.NodeLinkedNodeSiteId == null"); - - yield return new WebPageItemModel - { - WebPageItemParentGuid = nodeParentGuid, // NULL => under root - WebPageItemGUID = contentItemGuid, - WebPageItemName = safeNodeName, - WebPageItemTreePath = treePath, - WebPageItemWebsiteChannelGuid = siteGuid, - WebPageItemContentItemGuid = contentItemGuid, - WebPageItemOrder = cmsTree.NodeOrder ?? 0 // 0 is nullish value - }; - } - - private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, ICmsTree cmsTree, string sourceFormClassDefinition, string targetFormDefinition, Guid contentItemGuid, - Guid contentLanguageGuid, ICmsClass sourceNodeClass, WebsiteChannelInfo websiteChannelInfo, ICmsSite sourceSite, IClassMapping mapping) - { - var adapter = new NodeXmlAdapter(checkoutVersion.NodeXML); - - ContentItemCommonDataModel? commonDataModel = null; - ContentItemDataModel? dataModel = null; - try - { - string? pageTemplateConfiguration = adapter.DocumentPageTemplateConfiguration; - string? pageBuildWidgets = adapter.DocumentPageBuilderWidgets; - (pageTemplateConfiguration, pageBuildWidgets, bool ndp) = pageBuilderPatcher.PatchJsonDefinitions(checkoutVersion.NodeSiteID, pageTemplateConfiguration, pageBuildWidgets).GetAwaiter().GetResult(); - - #region Find existing guid - - var contentItemCommonDataGuid = Guid.NewGuid(); - var contentItemInfo = ContentItemInfo.Provider.Get() - .WhereEquals(nameof(ContentItemInfo.ContentItemGUID), contentItemGuid) - .FirstOrDefault(); - if (contentItemInfo != null) - { - var contentItems = ContentItemCommonDataInfo.Provider.Get() - .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentItemID), contentItemInfo.ContentItemID) - .ToList() - ; - - var existingDraft = contentItems.FirstOrDefault(x => x.ContentItemCommonDataVersionStatus == VersionStatus.Draft); - if (existingDraft is { ContentItemCommonDataGUID: { } existingGuid }) - { - contentItemCommonDataGuid = existingGuid; - } - } - - #endregion - - commonDataModel = new ContentItemCommonDataModel - { - ContentItemCommonDataGUID = contentItemCommonDataGuid, // adapter.DocumentGUID ?? throw new InvalidOperationException($"DocumentGUID is null"), - ContentItemCommonDataContentItemGuid = contentItemGuid, - ContentItemCommonDataContentLanguageGuid = contentLanguageGuid, - ContentItemCommonDataVersionStatus = VersionStatus.Draft, - ContentItemCommonDataIsLatest = true, // Flag for latest record to know what to retrieve for the UI - ContentItemCommonDataPageBuilderWidgets = pageBuildWidgets, - ContentItemCommonDataPageTemplateConfiguration = pageTemplateConfiguration - }; - - if (ndp) - { - deferredPathService.AddPatch( - commonDataModel.ContentItemCommonDataGUID ?? throw new InvalidOperationException("DocumentGUID is null"), - ContentItemCommonDataInfo.TYPEINFO.ObjectClassName,// sourceNodeClass.ClassName, - websiteChannelInfo.WebsiteChannelID - ); - } - - dataModel = new ContentItemDataModel - { - ContentItemDataGUID = commonDataModel.ContentItemCommonDataGUID, - ContentItemDataCommonDataGuid = commonDataModel.ContentItemCommonDataGUID, - ContentItemContentTypeName = mapping?.TargetClassName ?? sourceNodeClass.ClassName - }; - - if (sourceNodeClass.ClassIsCoupledClass) - { - var fi = new FormInfo(targetFormDefinition); - var sfi = new FormInfo(sourceFormClassDefinition); - string primaryKeyName = ""; - foreach (var sourceFieldInfo in sfi.GetFields(true, true)) - { - if (sourceFieldInfo.PrimaryKey) - { - primaryKeyName = sourceFieldInfo.Name; - } - } - - if (string.IsNullOrWhiteSpace(primaryKeyName)) - { - throw new Exception("Error, unable to find coupled data primary key"); - } - - var commonFields = UnpackReusableFieldSchemas(fi.GetFields()).ToArray(); - var sourceColumns = commonFields - .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) - .Union(fi.GetColumnNames(false)) - .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) - .ToList(); - - // TODO tomas.krch: 2024-09-05 propagate async to root - MapCoupledDataFieldValues(dataModel.CustomProperties, - s => adapter.GetValue(s), - s => adapter.HasValueSet(s) - , cmsTree, adapter.DocumentID, sourceColumns, sfi, fi, true, sourceNodeClass, sourceSite, mapping).GetAwaiter().GetResult(); - - foreach (var formFieldInfo in commonFields) - { - string originalFieldName = ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, formFieldInfo.Name); - if (dataModel.CustomProperties.TryGetValue(originalFieldName, out object? value)) - { - commonDataModel.CustomProperties ??= []; - logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' populated", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); - commonDataModel.CustomProperties[formFieldInfo.Name] = value; - dataModel.CustomProperties.Remove(originalFieldName); - } - else - { - logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' missing", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); - } - } - } - - // supply document name - if (reusableSchemaService.IsConversionToReusableFieldSchemaRequested(sourceNodeClass.ClassName)) - { - string fieldName = ReusableSchemaService.GetUniqueFieldName(sourceNodeClass.ClassName, "DocumentName"); - commonDataModel.CustomProperties.Add(fieldName, adapter.DocumentName); - } - else - { - dataModel.CustomProperties.Add("DocumentName", adapter.DocumentName); - } - } - catch (Exception ex) - { - Debug.WriteLine($"Failed attempt to create draft from '{checkoutVersion}' {ex}"); - throw; - } - - if (dataModel != null && commonDataModel != null) - { - yield return commonDataModel; - yield return dataModel; - } - } - - private async Task MapCoupledDataFieldValues( - Dictionary target, - Func getSourceValue, - Func containsSourceValue, - ICmsTree cmsTree, - int? documentId, - List newColumnNames, - FormInfo oldFormInfo, - FormInfo newFormInfo, - bool migratingFromVersionHistory, - ICmsClass sourceNodeClass, - ICmsSite site, - IClassMapping mapping - ) - { - Debug.Assert(sourceNodeClass.ClassTableName != null, "sourceNodeClass.ClassTableName != null"); - - foreach (string targetColumnName in newColumnNames) - { - string targetFieldName = null!; - Func valueConvertor = sourceValue => sourceValue; - switch (mapping?.GetMapping(targetColumnName, sourceNodeClass.ClassName)) - { - case FieldMappingWithConversion fieldMappingWithConversion: - { - targetFieldName = fieldMappingWithConversion.TargetFieldName; - valueConvertor = fieldMappingWithConversion.Converter; - break; - } - case FieldMapping fieldMapping: - { - targetFieldName = fieldMapping.TargetFieldName; - valueConvertor = sourceValue => sourceValue; - break; - } - case null: - { - targetFieldName = targetColumnName; - valueConvertor = sourceValue => sourceValue; - break; - } - - default: - break; - } - - if ( - targetFieldName.Equals("ContentItemDataID", StringComparison.InvariantCultureIgnoreCase) || - targetFieldName.Equals("ContentItemDataCommonDataID", StringComparison.InvariantCultureIgnoreCase) || - targetFieldName.Equals("ContentItemDataGUID", StringComparison.InvariantCultureIgnoreCase) || - targetFieldName.Equals(CmsClassMapper.GetLegacyDocumentName(newFormInfo, sourceNodeClass.ClassName), StringComparison.InvariantCultureIgnoreCase) - ) - { - logger.LogTrace("Skipping '{FieldName}'", targetFieldName); - continue; - } - -#pragma warning disable CS0618 // Type or member is obsolete - if (oldFormInfo.GetFormField(targetFieldName)?.External is true) -#pragma warning restore CS0618 // Type or member is obsolete - { - logger.LogTrace("Skipping '{FieldName}' - is external", targetFieldName); - continue; - } - - string sourceFieldName = mapping?.GetSourceFieldName(targetColumnName, sourceNodeClass.ClassName) ?? targetColumnName; - if (!containsSourceValue(sourceFieldName)) - { - if (migratingFromVersionHistory) - { - logger.LogDebug("Value is not contained in source, field '{Field}' (possibly because version existed before field was added to class form)", targetColumnName); - } - else - { - logger.LogWarning("Value is not contained in source, field '{Field}'", targetColumnName); - } - - continue; - } - - - var field = oldFormInfo.GetFormField(sourceFieldName); - string? controlName = field.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant(); - - object? sourceValue = getSourceValue(sourceFieldName); - target[targetFieldName] = valueConvertor.Invoke(sourceValue); - var fvmc = new FieldMigrationContext(field.DataType, controlName, targetColumnName, new DocumentSourceObjectContext(cmsTree, sourceNodeClass, site, oldFormInfo, newFormInfo, documentId)); - var fmb = fieldMigrationService.GetFieldMigration(fvmc); - if (fmb is FieldMigration fieldMigration) - { - if (controlName != null) - { - if (fieldMigration.Actions?.Contains(TcaDirective.ConvertToPages) ?? false) - { - // relation to other document - var convertedRelation = relationshipService.GetNodeRelationships(cmsTree.NodeID, sourceNodeClass.ClassName, field.Guid) - .Select(r => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(r.RightNode.NodeGUID, r.RightNode.NodeSiteID, r.RightNode.NodeID) }); - - target.SetValueAsJson(targetFieldName, valueConvertor.Invoke(convertedRelation)); - } - else - { - // leave as is - target[targetFieldName] = valueConvertor.Invoke(sourceValue); - } - - if (fieldMigration.TargetFormComponent == "webpages") - { - if (sourceValue is string pageReferenceJson) - { - var parsed = JObject.Parse(pageReferenceJson); - foreach (var jToken in parsed.DescendantsAndSelf()) - { - if (jToken.Path.EndsWith("NodeGUID", StringComparison.InvariantCultureIgnoreCase)) - { - var patchedGuid = spoiledGuidContext.EnsureNodeGuid(jToken.Value(), cmsTree.NodeSiteID); - jToken.Replace(JToken.FromObject(patchedGuid)); - } - } - - target[targetFieldName] = valueConvertor.Invoke(parsed.ToString().Replace("\"NodeGuid\"", "\"WebPageGuid\"")); - } - } - } - else - { - target[targetFieldName] = valueConvertor.Invoke(sourceValue); - } - } - else if (fmb != null) - { - switch (await fmb.MigrateValue(sourceValue, fvmc)) - { - case { Success: true } result: - { - target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); - break; - } - case { Success: false }: - { - logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); - break; - } - - default: - break; - } - } - else - { - target[targetFieldName] = valueConvertor?.Invoke(sourceValue); - } - - - var newField = newFormInfo.GetFormField(targetColumnName); - if (newField == null) - { - - var commonFields = UnpackReusableFieldSchemas(newFormInfo.GetFields()).ToArray(); - newField = commonFields - .FirstOrDefault(cf => ReusableSchemaService.RemoveClassPrefix(mapping?.TargetClassName ?? sourceNodeClass.ClassName, cf.Name).Equals(targetColumnName, StringComparison.InvariantCultureIgnoreCase)); - } - string? newControlName = newField?.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant(); - if (newControlName?.Equals(FormComponents.AdminRichTextEditorComponent, StringComparison.InvariantCultureIgnoreCase) == true && target[targetColumnName] is string { } html && !string.IsNullOrWhiteSpace(html) && - !configuration.MigrateMediaToMediaLibrary) - { - var mediaLinkService = mediaLinkServiceFactory.Create(); - var htmlProcessor = new HtmlProcessor(html, mediaLinkService); - - target[targetColumnName] = await htmlProcessor.ProcessHtml(site.SiteID, async (result, original) => - { - switch (result) - { - case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: - { - var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); - if (mediaFile is null) - { - return original; - } - - return assetFacade.GetAssetUri(mediaFile); - } - case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: - { - var attachment = MediaHelper.GetAttachment(result, modelFacade); - if (attachment is null) - { - return original; - } - - await attachmentMigrator.MigrateAttachment(attachment); - - string? culture = null; - if (attachment.AttachmentDocumentID is { } attachmentDocumentId) - { - culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; - } - - return assetFacade.GetAssetUri(attachment, culture); - } - - default: - break; - } - - return original; - }); - } - } - } - - private static IEnumerable UnpackReusableFieldSchemas(IEnumerable schemaInfos) - { - using var siEnum = schemaInfos.GetEnumerator(); - - if (siEnum.MoveNext() && FormHelper.GetFormInfo(ContentItemCommonDataInfo.TYPEINFO.ObjectClassName, true) is { } cfi) - { - do - { - var fsi = siEnum.Current; - var formFieldInfos = cfi - .GetFields(true, true) - .Where(f => string.Equals(f.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY] as string, fsi.Guid.ToString(), - StringComparison.InvariantCultureIgnoreCase)); - - foreach (var formFieldInfo in formFieldInfos) - { - yield return formFieldInfo; - } - } while (siEnum.MoveNext()); - } - } - - -} +using System.Diagnostics; +using CMS.ContentEngine; +using CMS.ContentEngine.Internal; +using CMS.Core; +using CMS.Core.Internal; +using CMS.DataEngine; +using CMS.FormEngine; +using CMS.Websites; +using CMS.Websites.Internal; +using Kentico.Xperience.UMT.Model; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common; +using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.Builders; +using Migration.Tool.Common.Helpers; +using Migration.Tool.Common.Services; +using Migration.Tool.KXP.Api.Auxiliary; +using Migration.Tool.KXP.Api.Services.CmsClass; +using Migration.Tool.Source.Auxiliary; +using Migration.Tool.Source.Contexts; +using Migration.Tool.Source.Helpers; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Providers; +using Migration.Tool.Source.Services; +using Newtonsoft.Json.Linq; + +namespace Migration.Tool.Source.Mappers; + +public record CmsTreeMapperSource( + ICmsTree CmsTree, + string SafeNodeName, + Guid SiteGuid, + Guid? NodeParentGuid, + Dictionary CultureToLanguageGuid, + string? TargetFormDefinition, + string SourceFormDefinition, + List MigratedDocuments, + ICmsSite SourceSite +); + +public class ContentItemMapper( + ILogger logger, + CoupledDataService coupledDataService, + IAttachmentMigrator attachmentMigrator, + CmsRelationshipService relationshipService, + FieldMigrationService fieldMigrationService, + ModelFacade modelFacade, + ReusableSchemaService reusableSchemaService, + DeferredPathService deferredPathService, + SpoiledGuidContext spoiledGuidContext, + IAssetFacade assetFacade, + MediaLinkServiceFactory mediaLinkServiceFactory, + ToolConfiguration configuration, + ClassMappingProvider classMappingProvider, + PageBuilderPatcher pageBuilderPatcher + ) : UmtMapperBase +{ + private const string CLASS_FIELD_CONTROL_NAME = "controlname"; + + protected override IEnumerable MapInternal(CmsTreeMapperSource source) + { + (var cmsTree, string safeNodeName, var siteGuid, var nodeParentGuid, var cultureToLanguageGuid, string? targetFormDefinition, string sourceFormDefinition, var migratedDocuments, var sourceSite) = source; + + logger.LogTrace("Mapping {Value}", new { cmsTree.NodeAliasPath, cmsTree.NodeName, cmsTree.NodeGUID, cmsTree.NodeSiteID }); + + var sourceNodeClass = modelFacade.SelectById(cmsTree.NodeClassID) ?? throw new InvalidOperationException($"Fatal: node class is missing, class id '{cmsTree.NodeClassID}'"); + var mapping = classMappingProvider.GetMapping(sourceNodeClass.ClassName); + var targetClassGuid = sourceNodeClass.ClassGUID; + if (mapping != null) + { + targetClassGuid = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName)?.ClassGUID ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); + } + + bool migratedAsContentFolder = sourceNodeClass.ClassName.Equals("cms.folder", StringComparison.InvariantCultureIgnoreCase) && !configuration.UseDeprecatedFolderPageType.GetValueOrDefault(false); + + var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); + yield return new ContentItemModel + { + ContentItemGUID = contentItemGuid, + ContentItemName = safeNodeName, + ContentItemIsReusable = false, // page is not reusable + ContentItemIsSecured = cmsTree.IsSecuredNode ?? false, + ContentItemDataClassGuid = migratedAsContentFolder ? null : targetClassGuid, + ContentItemChannelGuid = siteGuid + }; + + var targetWebPage = WebPageItemInfo.Provider.Get() + .WhereEquals(nameof(WebPageItemInfo.WebPageItemGUID), contentItemGuid) + .FirstOrDefault(); + string? treePath = targetWebPage?.WebPageItemTreePath; + + var websiteChannelInfo = WebsiteChannelInfoProvider.ProviderObject.Get(siteGuid); + var treePathConvertor = TreePathConvertor.GetSiteConverter(websiteChannelInfo.WebsiteChannelID); + if (treePath == null) + { + (bool treePathIsDifferent, treePath) = treePathConvertor.ConvertAndEnsureUniqueness(cmsTree.NodeAliasPath).GetAwaiter().GetResult(); + if (treePathIsDifferent) + { + logger.LogInformation($"Original node alias path '{cmsTree.NodeAliasPath}' of '{cmsTree.NodeName}' item was converted to '{treePath}' since the value does not allow original range of allowed characters."); + } + } + + foreach (var cmsDocument in migratedDocuments) + { + if (!cultureToLanguageGuid.TryGetValue(cmsDocument.DocumentCulture, out var languageGuid)) + { + logger.LogWarning("Document '{DocumentGUID}' was skipped, unknown culture", cmsDocument.DocumentGUID); + continue; + } + + bool hasDraft = cmsDocument.DocumentPublishedVersionHistoryID is not null && + cmsDocument.DocumentPublishedVersionHistoryID != cmsDocument.DocumentCheckedOutVersionHistoryID; + + var checkoutVersion = hasDraft + ? modelFacade.SelectById(cmsDocument.DocumentCheckedOutVersionHistoryID) + : null; + + bool draftMigrated = false; + if (checkoutVersion is { PublishFrom: null } draftVersion && !migratedAsContentFolder) + { + List? migratedDraft = null; + try + { + migratedDraft = MigrateDraft(draftVersion, cmsTree, sourceFormDefinition, targetFormDefinition, contentItemGuid, languageGuid, sourceNodeClass, websiteChannelInfo, sourceSite, mapping).ToList(); + draftMigrated = true; + } + catch + { + logger.LogWarning("Failed to migrate checkout version of document with DocumentID={CmsDocumentDocumentId} VersionHistoryID={CmsDocumentDocumentCheckedOutVersionHistoryId}", + cmsDocument.DocumentID, cmsDocument.DocumentCheckedOutVersionHistoryID); + draftMigrated = false; + } + + if (migratedDraft != null) + { + foreach (var umtModel in migratedDraft) + { + yield return umtModel; + } + } + } + + var versionStatus = cmsDocument switch + { + { DocumentIsArchived: true } => VersionStatus.Unpublished, + { DocumentPublishedVersionHistoryID: null, DocumentCheckedOutVersionHistoryID: null } => VersionStatus.Published, + { DocumentPublishedVersionHistoryID: { } pubId, DocumentCheckedOutVersionHistoryID: { } chId } when pubId <= chId => VersionStatus.Published, + { DocumentPublishedVersionHistoryID: null, DocumentCheckedOutVersionHistoryID: not null } => VersionStatus.InitialDraft, + _ => draftMigrated ? VersionStatus.Published : VersionStatus.InitialDraft + }; + if (migratedAsContentFolder) + { + versionStatus = VersionStatus.Published; // folder is automatically published + } + + DateTime? scheduledPublishWhen = null; + DateTime? scheduleUnpublishWhen = null; + string? contentItemCommonDataPageBuilderWidgets = null; + string? contentItemCommonDataPageTemplateConfiguration = null; + + bool ndp = false; + if (!migratedAsContentFolder) + { + if (cmsDocument.DocumentPublishFrom is { } publishFrom) + { + var now = Service.Resolve().GetDateTimeNow(); + if (publishFrom > now) + { + versionStatus = VersionStatus.Unpublished; + } + else + { + scheduledPublishWhen = publishFrom; + } + } + + if (cmsDocument.DocumentPublishTo is { } publishTo) + { + var now = Service.Resolve().GetDateTimeNow(); + if (publishTo < now) + { + versionStatus = VersionStatus.Unpublished; + } + else + { + scheduleUnpublishWhen = publishTo; + } + } + + switch (cmsDocument) + { + case CmsDocumentK11: + { + break; + } + case CmsDocumentK12 doc: + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } + case CmsDocumentK13 doc: + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } + + default: + break; + } + + (contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets, ndp) = pageBuilderPatcher.PatchJsonDefinitions(source.CmsTree.NodeSiteID, contentItemCommonDataPageTemplateConfiguration, contentItemCommonDataPageBuilderWidgets).GetAwaiter().GetResult(); + } + + var documentGuid = spoiledGuidContext.EnsureDocumentGuid( + cmsDocument.DocumentGUID ?? throw new InvalidOperationException("DocumentGUID is null"), + cmsTree.NodeSiteID, + cmsTree.NodeID, + cmsDocument.DocumentID + ); + + var commonDataModel = new ContentItemCommonDataModel + { + ContentItemCommonDataGUID = documentGuid, + ContentItemCommonDataContentItemGuid = contentItemGuid, + ContentItemCommonDataContentLanguageGuid = languageGuid, // DocumentCulture -> language entity needs to be created and its ID used here + ContentItemCommonDataVersionStatus = versionStatus, + ContentItemCommonDataIsLatest = !draftMigrated, // Flag for latest record to know what to retrieve for the UI + ContentItemCommonDataPageBuilderWidgets = contentItemCommonDataPageBuilderWidgets, + ContentItemCommonDataPageTemplateConfiguration = contentItemCommonDataPageTemplateConfiguration, + }; + + if (ndp) + { + deferredPathService.AddPatch( + commonDataModel.ContentItemCommonDataGUID ?? throw new InvalidOperationException("DocumentGUID is null"), + ContentItemCommonDataInfo.TYPEINFO.ObjectClassName, + websiteChannelInfo.WebsiteChannelID + ); + } + + if (!migratedAsContentFolder) + { + var dataModel = new ContentItemDataModel { ContentItemDataGUID = commonDataModel.ContentItemCommonDataGUID, ContentItemDataCommonDataGuid = commonDataModel.ContentItemCommonDataGUID, ContentItemContentTypeName = mapping?.TargetClassName ?? sourceNodeClass.ClassName }; + + var fi = new FormInfo(targetFormDefinition); + if (sourceNodeClass.ClassIsCoupledClass) + { + var sfi = new FormInfo(sourceFormDefinition); + string primaryKeyName = ""; + foreach (var sourceFieldInfo in sfi.GetFields(true, true)) + { + if (sourceFieldInfo.PrimaryKey) + { + primaryKeyName = sourceFieldInfo.Name; + } + } + + if (string.IsNullOrWhiteSpace(primaryKeyName)) + { + throw new Exception("Error, unable to find coupled data primary key"); + } + + var commonFields = UnpackReusableFieldSchemas(fi.GetFields()).ToArray(); + var targetColumns = commonFields + .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) + .Union(fi.GetColumnNames(false)) + .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) + .ToList(); + + var coupledDataRow = coupledDataService.GetSourceCoupledDataRow(sourceNodeClass.ClassTableName, primaryKeyName, cmsDocument.DocumentForeignKeyValue); + // TODO tomas.krch: 2024-09-05 propagate async to root + MapCoupledDataFieldValues(dataModel.CustomProperties, + columnName => coupledDataRow?[columnName], + columnName => coupledDataRow?.ContainsKey(columnName) ?? false, + cmsTree, cmsDocument.DocumentID, + targetColumns, sfi, fi, + false, sourceNodeClass, sourceSite, mapping + ).GetAwaiter().GetResult(); + + foreach (var formFieldInfo in commonFields) + { + string originalFieldName = ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, formFieldInfo.Name); + if (dataModel.CustomProperties.TryGetValue(originalFieldName, out object? value)) + { + commonDataModel.CustomProperties ??= []; + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' populated", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + commonDataModel.CustomProperties[formFieldInfo.Name] = value; + dataModel.CustomProperties.Remove(originalFieldName); + } + else + { + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' missing", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + } + } + } + + string targetClassName = mapping?.TargetClassName ?? sourceNodeClass.ClassName; + if (CmsClassMapper.GetLegacyDocumentName(fi, targetClassName) is { } legacyDocumentNameFieldName) + { + if (reusableSchemaService.IsConversionToReusableFieldSchemaRequested(targetClassName)) + { + string fieldName = ReusableSchemaService.GetUniqueFieldName(targetClassName, legacyDocumentNameFieldName); + commonDataModel.CustomProperties.Add(fieldName, cmsDocument.DocumentName); + } + else + { + dataModel.CustomProperties.Add(legacyDocumentNameFieldName, cmsDocument.DocumentName); + } + } + + yield return commonDataModel; + yield return dataModel; + } + + Guid? documentCreatedByUserGuid = null; + if (modelFacade.TrySelectGuid(cmsDocument.DocumentCreatedByUserID, out var createdByUserGuid)) + { + documentCreatedByUserGuid = createdByUserGuid; + } + + Guid? documentModifiedByUserGuid = null; + if (modelFacade.TrySelectGuid(cmsDocument.DocumentModifiedByUserID, out var modifiedByUserGuid)) + { + documentModifiedByUserGuid = modifiedByUserGuid; + } + + var languageMetadataInfo = new ContentItemLanguageMetadataModel + { + ContentItemLanguageMetadataGUID = documentGuid, + ContentItemLanguageMetadataContentItemGuid = contentItemGuid, + ContentItemLanguageMetadataDisplayName = cmsDocument.DocumentName, // For the admin UI only + ContentItemLanguageMetadataLatestVersionStatus = draftMigrated ? VersionStatus.Draft : versionStatus, // That's the latest status of th item for admin optimization + ContentItemLanguageMetadataCreatedWhen = cmsDocument.DocumentCreatedWhen, // DocumentCreatedWhen + ContentItemLanguageMetadataModifiedWhen = cmsDocument.DocumentModifiedWhen, // DocumentModifiedWhen + ContentItemLanguageMetadataCreatedByUserGuid = documentCreatedByUserGuid, + ContentItemLanguageMetadataModifiedByUserGuid = documentModifiedByUserGuid, + // logic inaccessible, not supported + // ContentItemLanguageMetadataHasImageAsset = ContentItemAssetHasImageArbiter.HasImage(contentItemDataInfo), // This is for admin UI optimization - set to true if latest version contains a field with an image asset + ContentItemLanguageMetadataHasImageAsset = false, + ContentItemLanguageMetadataContentLanguageGuid = languageGuid, // DocumentCulture -> language entity needs to be created and its ID used here + ContentItemLanguageMetadataScheduledPublishWhen = scheduledPublishWhen, + ContentItemLanguageMetadataScheduledUnpublishWhen = scheduleUnpublishWhen + }; + yield return languageMetadataInfo; + } + + // mapping of linked nodes is not supported + Debug.Assert(cmsTree.NodeLinkedNodeID == null, "cmsTree.NodeLinkedNodeId == null"); + Debug.Assert(cmsTree.NodeLinkedNodeSiteID == null, "cmsTree.NodeLinkedNodeSiteId == null"); + + yield return new WebPageItemModel + { + WebPageItemParentGuid = nodeParentGuid, // NULL => under root + WebPageItemGUID = contentItemGuid, + WebPageItemName = safeNodeName, + WebPageItemTreePath = treePath, + WebPageItemWebsiteChannelGuid = siteGuid, + WebPageItemContentItemGuid = contentItemGuid, + WebPageItemOrder = cmsTree.NodeOrder ?? 0 // 0 is nullish value + }; + } + + private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, ICmsTree cmsTree, string sourceFormClassDefinition, string targetFormDefinition, Guid contentItemGuid, + Guid contentLanguageGuid, ICmsClass sourceNodeClass, WebsiteChannelInfo websiteChannelInfo, ICmsSite sourceSite, IClassMapping mapping) + { + var adapter = new NodeXmlAdapter(checkoutVersion.NodeXML); + + ContentItemCommonDataModel? commonDataModel = null; + ContentItemDataModel? dataModel = null; + try + { + string? pageTemplateConfiguration = adapter.DocumentPageTemplateConfiguration; + string? pageBuildWidgets = adapter.DocumentPageBuilderWidgets; + (pageTemplateConfiguration, pageBuildWidgets, bool ndp) = pageBuilderPatcher.PatchJsonDefinitions(checkoutVersion.NodeSiteID, pageTemplateConfiguration, pageBuildWidgets).GetAwaiter().GetResult(); + + #region Find existing guid + + var contentItemCommonDataGuid = Guid.NewGuid(); + var contentItemInfo = ContentItemInfo.Provider.Get() + .WhereEquals(nameof(ContentItemInfo.ContentItemGUID), contentItemGuid) + .FirstOrDefault(); + if (contentItemInfo != null) + { + var contentItems = ContentItemCommonDataInfo.Provider.Get() + .WhereEquals(nameof(ContentItemCommonDataInfo.ContentItemCommonDataContentItemID), contentItemInfo.ContentItemID) + .ToList() + ; + + var existingDraft = contentItems.FirstOrDefault(x => x.ContentItemCommonDataVersionStatus == VersionStatus.Draft); + if (existingDraft is { ContentItemCommonDataGUID: { } existingGuid }) + { + contentItemCommonDataGuid = existingGuid; + } + } + + #endregion + + commonDataModel = new ContentItemCommonDataModel + { + ContentItemCommonDataGUID = contentItemCommonDataGuid, // adapter.DocumentGUID ?? throw new InvalidOperationException($"DocumentGUID is null"), + ContentItemCommonDataContentItemGuid = contentItemGuid, + ContentItemCommonDataContentLanguageGuid = contentLanguageGuid, + ContentItemCommonDataVersionStatus = VersionStatus.Draft, + ContentItemCommonDataIsLatest = true, // Flag for latest record to know what to retrieve for the UI + ContentItemCommonDataPageBuilderWidgets = pageBuildWidgets, + ContentItemCommonDataPageTemplateConfiguration = pageTemplateConfiguration + }; + + if (ndp) + { + deferredPathService.AddPatch( + commonDataModel.ContentItemCommonDataGUID ?? throw new InvalidOperationException("DocumentGUID is null"), + ContentItemCommonDataInfo.TYPEINFO.ObjectClassName,// sourceNodeClass.ClassName, + websiteChannelInfo.WebsiteChannelID + ); + } + + dataModel = new ContentItemDataModel + { + ContentItemDataGUID = commonDataModel.ContentItemCommonDataGUID, + ContentItemDataCommonDataGuid = commonDataModel.ContentItemCommonDataGUID, + ContentItemContentTypeName = mapping?.TargetClassName ?? sourceNodeClass.ClassName + }; + + if (sourceNodeClass.ClassIsCoupledClass) + { + var fi = new FormInfo(targetFormDefinition); + var sfi = new FormInfo(sourceFormClassDefinition); + string primaryKeyName = ""; + foreach (var sourceFieldInfo in sfi.GetFields(true, true)) + { + if (sourceFieldInfo.PrimaryKey) + { + primaryKeyName = sourceFieldInfo.Name; + } + } + + if (string.IsNullOrWhiteSpace(primaryKeyName)) + { + throw new Exception("Error, unable to find coupled data primary key"); + } + + var commonFields = UnpackReusableFieldSchemas(fi.GetFields()).ToArray(); + var sourceColumns = commonFields + .Select(cf => ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, cf.Name)) + .Union(fi.GetColumnNames(false)) + .Except([CmsClassMapper.GetLegacyDocumentName(fi, sourceNodeClass.ClassName)]) + .ToList(); + + // TODO tomas.krch: 2024-09-05 propagate async to root + MapCoupledDataFieldValues(dataModel.CustomProperties, + s => adapter.GetValue(s), + s => adapter.HasValueSet(s) + , cmsTree, adapter.DocumentID, sourceColumns, sfi, fi, true, sourceNodeClass, sourceSite, mapping).GetAwaiter().GetResult(); + + foreach (var formFieldInfo in commonFields) + { + string originalFieldName = ReusableSchemaService.RemoveClassPrefix(sourceNodeClass.ClassName, formFieldInfo.Name); + if (dataModel.CustomProperties.TryGetValue(originalFieldName, out object? value)) + { + commonDataModel.CustomProperties ??= []; + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' populated", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + commonDataModel.CustomProperties[formFieldInfo.Name] = value; + dataModel.CustomProperties.Remove(originalFieldName); + } + else + { + logger.LogTrace("Reusable schema field '{FieldName}' from schema '{SchemaGuid}' missing", formFieldInfo.Name, formFieldInfo.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY]); + } + } + } + + // supply document name + if (reusableSchemaService.IsConversionToReusableFieldSchemaRequested(sourceNodeClass.ClassName)) + { + string fieldName = ReusableSchemaService.GetUniqueFieldName(sourceNodeClass.ClassName, "DocumentName"); + commonDataModel.CustomProperties.Add(fieldName, adapter.DocumentName); + } + else + { + dataModel.CustomProperties.Add("DocumentName", adapter.DocumentName); + } + } + catch (Exception ex) + { + Debug.WriteLine($"Failed attempt to create draft from '{checkoutVersion}' {ex}"); + throw; + } + + if (dataModel != null && commonDataModel != null) + { + yield return commonDataModel; + yield return dataModel; + } + } + + private async Task MapCoupledDataFieldValues( + Dictionary target, + Func getSourceValue, + Func containsSourceValue, + ICmsTree cmsTree, + int? documentId, + List newColumnNames, + FormInfo oldFormInfo, + FormInfo newFormInfo, + bool migratingFromVersionHistory, + ICmsClass sourceNodeClass, + ICmsSite site, + IClassMapping mapping + ) + { + Debug.Assert(sourceNodeClass.ClassTableName != null, "sourceNodeClass.ClassTableName != null"); + + foreach (string targetColumnName in newColumnNames) + { + string targetFieldName = null!; + Func valueConvertor = sourceValue => sourceValue; + switch (mapping?.GetMapping(targetColumnName, sourceNodeClass.ClassName)) + { + case FieldMappingWithConversion fieldMappingWithConversion: + { + targetFieldName = fieldMappingWithConversion.TargetFieldName; + valueConvertor = fieldMappingWithConversion.Converter; + break; + } + case FieldMapping fieldMapping: + { + targetFieldName = fieldMapping.TargetFieldName; + valueConvertor = sourceValue => sourceValue; + break; + } + case null: + { + targetFieldName = targetColumnName; + valueConvertor = sourceValue => sourceValue; + break; + } + + default: + break; + } + + if ( + targetFieldName.Equals("ContentItemDataID", StringComparison.InvariantCultureIgnoreCase) || + targetFieldName.Equals("ContentItemDataCommonDataID", StringComparison.InvariantCultureIgnoreCase) || + targetFieldName.Equals("ContentItemDataGUID", StringComparison.InvariantCultureIgnoreCase) || + targetFieldName.Equals(CmsClassMapper.GetLegacyDocumentName(newFormInfo, sourceNodeClass.ClassName), StringComparison.InvariantCultureIgnoreCase) + ) + { + logger.LogTrace("Skipping '{FieldName}'", targetFieldName); + continue; + } + +#pragma warning disable CS0618 // Type or member is obsolete + if (oldFormInfo.GetFormField(targetFieldName)?.External is true) +#pragma warning restore CS0618 // Type or member is obsolete + { + logger.LogTrace("Skipping '{FieldName}' - is external", targetFieldName); + continue; + } + + string sourceFieldName = mapping?.GetSourceFieldName(targetColumnName, sourceNodeClass.ClassName) ?? targetColumnName; + if (!containsSourceValue(sourceFieldName)) + { + if (migratingFromVersionHistory) + { + logger.LogDebug("Value is not contained in source, field '{Field}' (possibly because version existed before field was added to class form)", targetColumnName); + } + else + { + logger.LogWarning("Value is not contained in source, field '{Field}'", targetColumnName); + } + + continue; + } + + + var field = oldFormInfo.GetFormField(sourceFieldName); + string? controlName = field.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant(); + + object? sourceValue = getSourceValue(sourceFieldName); + target[targetFieldName] = valueConvertor.Invoke(sourceValue); + var fvmc = new FieldMigrationContext(field.DataType, controlName, targetColumnName, new DocumentSourceObjectContext(cmsTree, sourceNodeClass, site, oldFormInfo, newFormInfo, documentId)); + var fmb = fieldMigrationService.GetFieldMigration(fvmc); + if (fmb is FieldMigration fieldMigration) + { + if (controlName != null) + { + if (fieldMigration.Actions?.Contains(TcaDirective.ConvertToPages) ?? false) + { + // relation to other document + var convertedRelation = relationshipService.GetNodeRelationships(cmsTree.NodeID, sourceNodeClass.ClassName, field.Guid) + .Select(r => new WebPageRelatedItem { WebPageGuid = spoiledGuidContext.EnsureNodeGuid(r.RightNode.NodeGUID, r.RightNode.NodeSiteID, r.RightNode.NodeID) }); + + target.SetValueAsJson(targetFieldName, valueConvertor.Invoke(convertedRelation)); + } + else + { + // leave as is + target[targetFieldName] = valueConvertor.Invoke(sourceValue); + } + + if (fieldMigration.TargetFormComponent == "webpages") + { + if (sourceValue is string pageReferenceJson) + { + var parsed = JObject.Parse(pageReferenceJson); + foreach (var jToken in parsed.DescendantsAndSelf()) + { + if (jToken.Path.EndsWith("NodeGUID", StringComparison.InvariantCultureIgnoreCase)) + { + var patchedGuid = spoiledGuidContext.EnsureNodeGuid(jToken.Value(), cmsTree.NodeSiteID); + jToken.Replace(JToken.FromObject(patchedGuid)); + } + } + + target[targetFieldName] = valueConvertor.Invoke(parsed.ToString().Replace("\"NodeGuid\"", "\"WebPageGuid\"")); + } + } + } + else + { + target[targetFieldName] = valueConvertor.Invoke(sourceValue); + } + } + else if (fmb != null) + { + switch (await fmb.MigrateValue(sourceValue, fvmc)) + { + case { Success: true } result: + { + target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); + break; + } + case { Success: false }: + { + logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); + break; + } + + default: + break; + } + } + else + { + target[targetFieldName] = valueConvertor?.Invoke(sourceValue); + } + + + var newField = newFormInfo.GetFormField(targetColumnName); + if (newField == null) + { + + var commonFields = UnpackReusableFieldSchemas(newFormInfo.GetFields()).ToArray(); + newField = commonFields + .FirstOrDefault(cf => ReusableSchemaService.RemoveClassPrefix(mapping?.TargetClassName ?? sourceNodeClass.ClassName, cf.Name).Equals(targetColumnName, StringComparison.InvariantCultureIgnoreCase)); + } + string? newControlName = newField?.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant(); + if (newControlName?.Equals(FormComponents.AdminRichTextEditorComponent, StringComparison.InvariantCultureIgnoreCase) == true && target[targetColumnName] is string { } html && !string.IsNullOrWhiteSpace(html) && + !configuration.MigrateMediaToMediaLibrary) + { + var mediaLinkService = mediaLinkServiceFactory.Create(); + var htmlProcessor = new HtmlProcessor(html, mediaLinkService); + + target[targetColumnName] = await htmlProcessor.ProcessHtml(site.SiteID, async (result, original) => + { + switch (result) + { + case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: + { + var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); + if (mediaFile is null) + { + return original; + } + + return assetFacade.GetAssetUri(mediaFile); + } + case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: + { + var attachment = MediaHelper.GetAttachment(result, modelFacade); + if (attachment is null) + { + return original; + } + + await attachmentMigrator.MigrateAttachment(attachment); + + string? culture = null; + if (attachment.AttachmentDocumentID is { } attachmentDocumentId) + { + culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; + } + + return assetFacade.GetAssetUri(attachment, culture); + } + + default: + break; + } + + return original; + }); + } + } + } + + private static IEnumerable UnpackReusableFieldSchemas(IEnumerable schemaInfos) + { + using var siEnum = schemaInfos.GetEnumerator(); + + if (siEnum.MoveNext() && FormHelper.GetFormInfo(ContentItemCommonDataInfo.TYPEINFO.ObjectClassName, true) is { } cfi) + { + do + { + var fsi = siEnum.Current; + var formFieldInfos = cfi + .GetFields(true, true) + .Where(f => string.Equals(f.Properties[ReusableFieldSchemaConstants.SCHEMA_IDENTIFIER_KEY] as string, fsi.Guid.ToString(), + StringComparison.InvariantCultureIgnoreCase)); + + foreach (var formFieldInfo in formFieldInfos) + { + yield return formFieldInfo; + } + } while (siEnum.MoveNext()); + } + } + + +} diff --git a/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs b/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs index 2fe610d2..f6762821 100644 --- a/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/PageTemplateConfigurationMapper.cs @@ -1,57 +1,57 @@ -using CMS.Websites; -using Microsoft.Extensions.Logging; -using Migration.Tool.Common.Abstractions; -using Migration.Tool.Common.MigrationProtocol; -using Migration.Tool.Source.Contexts; -using Migration.Tool.Source.Model; -using Migration.Tool.Source.Services; - -namespace Migration.Tool.Source.Mappers; - -public class PageTemplateConfigurationMapper( - ILogger logger, - PrimaryKeyMappingContext pkContext, - IProtocol protocol, - PageBuilderPatcher pageBuilderPatcher -) - : EntityMapperBase(logger, pkContext, protocol) -{ - protected override PageTemplateConfigurationInfo? CreateNewInstance(ICmsPageTemplateConfiguration source, MappingHelper mappingHelper, AddFailure addFailure) - => source switch - { - CmsPageTemplateConfigurationK11 => null, - CmsPageTemplateConfigurationK12 => PageTemplateConfigurationInfo.New(), - CmsPageTemplateConfigurationK13 => PageTemplateConfigurationInfo.New(), - _ => null - }; - - protected override PageTemplateConfigurationInfo MapInternal(ICmsPageTemplateConfiguration s, PageTemplateConfigurationInfo target, - bool newInstance, MappingHelper mappingHelper, AddFailure addFailure) - { - if (s is ICmsPageTemplateConfigurationK12K13 source) - { - target.PageTemplateConfigurationDescription = source.PageTemplateConfigurationDescription; - target.PageTemplateConfigurationName = source.PageTemplateConfigurationName; - target.PageTemplateConfigurationLastModified = source.PageTemplateConfigurationLastModified; - target.PageTemplateConfigurationIcon = "xp-custom-element"; - - if (newInstance) - { - target.PageTemplateConfigurationGUID = source.PageTemplateConfigurationGUID; - } - - // bool needsDeferredPatch = false; - string? configurationTemplate = source.PageTemplateConfigurationTemplate; - string? configurationWidgets = source.PageTemplateConfigurationWidgets; - - (configurationTemplate, configurationWidgets, bool _) = pageBuilderPatcher.PatchJsonDefinitions(source.PageTemplateConfigurationSiteID, configurationTemplate, configurationWidgets).GetAwaiter().GetResult(); - - target.PageTemplateConfigurationTemplate = configurationTemplate; - target.PageTemplateConfigurationWidgets = configurationWidgets; - - return target; - } - - return null; - } -} +using CMS.Websites; +using Microsoft.Extensions.Logging; +using Migration.Tool.Common.Abstractions; +using Migration.Tool.Common.MigrationProtocol; +using Migration.Tool.Source.Contexts; +using Migration.Tool.Source.Model; +using Migration.Tool.Source.Services; + +namespace Migration.Tool.Source.Mappers; + +public class PageTemplateConfigurationMapper( + ILogger logger, + PrimaryKeyMappingContext pkContext, + IProtocol protocol, + PageBuilderPatcher pageBuilderPatcher +) + : EntityMapperBase(logger, pkContext, protocol) +{ + protected override PageTemplateConfigurationInfo? CreateNewInstance(ICmsPageTemplateConfiguration source, MappingHelper mappingHelper, AddFailure addFailure) + => source switch + { + CmsPageTemplateConfigurationK11 => null, + CmsPageTemplateConfigurationK12 => PageTemplateConfigurationInfo.New(), + CmsPageTemplateConfigurationK13 => PageTemplateConfigurationInfo.New(), + _ => null + }; + + protected override PageTemplateConfigurationInfo MapInternal(ICmsPageTemplateConfiguration s, PageTemplateConfigurationInfo target, + bool newInstance, MappingHelper mappingHelper, AddFailure addFailure) + { + if (s is ICmsPageTemplateConfigurationK12K13 source) + { + target.PageTemplateConfigurationDescription = source.PageTemplateConfigurationDescription; + target.PageTemplateConfigurationName = source.PageTemplateConfigurationName; + target.PageTemplateConfigurationLastModified = source.PageTemplateConfigurationLastModified; + target.PageTemplateConfigurationIcon = "xp-custom-element"; + + if (newInstance) + { + target.PageTemplateConfigurationGUID = source.PageTemplateConfigurationGUID; + } + + // bool needsDeferredPatch = false; + string? configurationTemplate = source.PageTemplateConfigurationTemplate; + string? configurationWidgets = source.PageTemplateConfigurationWidgets; + + (configurationTemplate, configurationWidgets, bool _) = pageBuilderPatcher.PatchJsonDefinitions(source.PageTemplateConfigurationSiteID, configurationTemplate, configurationWidgets).GetAwaiter().GetResult(); + + target.PageTemplateConfigurationTemplate = configurationTemplate; + target.PageTemplateConfigurationWidgets = configurationWidgets; + + return target; + } + + return null; + } +} diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index 61c751d3..1d306f3c 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -1,24 +1,24 @@ -using Microsoft.Extensions.DependencyInjection; -using Migration.Tool.Extensions.CommunityMigrations; -using Migration.Tool.Extensions.DefaultMigrations; -using Migration.Tool.KXP.Api.Services.CmsClass; - -namespace Migration.Tool.Extensions; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection UseCustomizations(this IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // services.AddClassMergeExample(); - // services.AddSimpleRemodelingSample(); - // services.AddReusableSchemaIntegrationSample(); - return services; - } -} +using Microsoft.Extensions.DependencyInjection; +using Migration.Tool.Extensions.CommunityMigrations; +using Migration.Tool.Extensions.DefaultMigrations; +using Migration.Tool.KXP.Api.Services.CmsClass; + +namespace Migration.Tool.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection UseCustomizations(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // services.AddClassMergeExample(); + // services.AddSimpleRemodelingSample(); + // services.AddReusableSchemaIntegrationSample(); + return services; + } +} diff --git a/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs b/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs index c282763b..b514df79 100644 --- a/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs +++ b/Migration.Tool.KXP.Api/DependencyInjectionExtensions.cs @@ -1,32 +1,32 @@ -using CMS.Base; -using CMS.Core; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -using Migration.Tool.KXP.Api.Services.CmsClass; - -namespace Migration.Tool.KXP.Api; - -public static class DependencyInjectionExtensions -{ - public static IServiceCollection UseKxpApi(this IServiceCollection services, IConfiguration configuration, string? applicationPhysicalPath = null) - { - Service.Use(configuration); - if (applicationPhysicalPath != null && Directory.Exists(applicationPhysicalPath)) - { - SystemContext.WebApplicationPhysicalPath = applicationPhysicalPath; - } - - services.AddTransient(); - services.AddTransient(); - - services.AddSingleton(); - services.AddSingleton(s => (s.GetService() as FieldMigrationService)!); - - services.AddSingleton(); - services.AddSingleton(); - - return services; - } -} +using CMS.Base; +using CMS.Core; + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +using Migration.Tool.KXP.Api.Services.CmsClass; + +namespace Migration.Tool.KXP.Api; + +public static class DependencyInjectionExtensions +{ + public static IServiceCollection UseKxpApi(this IServiceCollection services, IConfiguration configuration, string? applicationPhysicalPath = null) + { + Service.Use(configuration); + if (applicationPhysicalPath != null && Directory.Exists(applicationPhysicalPath)) + { + SystemContext.WebApplicationPhysicalPath = applicationPhysicalPath; + } + + services.AddTransient(); + services.AddTransient(); + + services.AddSingleton(); + services.AddSingleton(s => (s.GetService() as FieldMigrationService)!); + + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +}