From e4c340a055c0586ba6a3a59e4dce004450bb28bc Mon Sep 17 00:00:00 2001 From: akfakmot Date: Sat, 4 Oct 2025 09:53:54 +0200 Subject: [PATCH 1/4] Fix manual mappings Fix: Fix manual mappings artifacts when executing multiple of --custom-modules/--custom-tables/--pages --- .../Providers/ClassMappingProvider.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs b/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs index 4710f123..346a2b8d 100644 --- a/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs +++ b/KVA/Migration.Tool.Source/Providers/ClassMappingProvider.cs @@ -70,12 +70,18 @@ private Dictionary MappingsByClassName return MappingsByClassName.GetValueOrDefault(className); } + private Dictionary? manualMappings = null; public Dictionary ExecuteMappings() { + if (manualMappings is not null) + { + return manualMappings; + } + EnsureSettings(); ExecReusableSchemaBuilders(); - var manualMappings = new Dictionary(); + manualMappings = new Dictionary(); var metadataFields = GetLegacyMetadataFields(modelFacade.SelectVersion(), IncludedMetadata.Extended).ToArray(); foreach (var classMapping in MappingsByClassName.Values) From 3e855eb5e0d9053173700e62a160a60275392e78 Mon Sep 17 00:00:00 2001 From: akfakmot Date: Tue, 7 Oct 2025 07:38:23 +0200 Subject: [PATCH 2/4] Trim user email whitespace before migration Add: Trim user email whitespace before migration --- Migration.Tool.Core.K11/Mappers/UserInfoMapper.cs | 2 +- Migration.Tool.Core.KX12/Mappers/UserInfoMapper.cs | 2 +- Migration.Tool.Core.KX13/Mappers/UserInfoMapper.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Migration.Tool.Core.K11/Mappers/UserInfoMapper.cs b/Migration.Tool.Core.K11/Mappers/UserInfoMapper.cs index 4ae19083..c556510f 100644 --- a/Migration.Tool.Core.K11/Mappers/UserInfoMapper.cs +++ b/Migration.Tool.Core.K11/Mappers/UserInfoMapper.cs @@ -36,7 +36,7 @@ protected override UserInfo MapInternal(CmsUser source, UserInfo target, bool ne target.UserName = source.UserName; target.FirstName = source.FirstName; target.LastName = source.LastName; - target.Email = source.Email; + target.Email = source.Email?.Trim(); // target.UserPassword = source.UserPassword; target.SetValue("UserPassword", source.UserPassword); target.UserEnabled = source.UserEnabled; diff --git a/Migration.Tool.Core.KX12/Mappers/UserInfoMapper.cs b/Migration.Tool.Core.KX12/Mappers/UserInfoMapper.cs index 0047794d..8b8b4b59 100644 --- a/Migration.Tool.Core.KX12/Mappers/UserInfoMapper.cs +++ b/Migration.Tool.Core.KX12/Mappers/UserInfoMapper.cs @@ -35,7 +35,7 @@ protected override UserInfo MapInternal(KX12M.CmsUser source, UserInfo target, b target.UserName = source.UserName; target.FirstName = source.FirstName; target.LastName = source.LastName; - target.Email = source.Email; + target.Email = source.Email?.Trim(); // target.UserPassword = source.UserPassword; target.SetValue("UserPassword", source.UserPassword); target.UserEnabled = source.UserEnabled; diff --git a/Migration.Tool.Core.KX13/Mappers/UserInfoMapper.cs b/Migration.Tool.Core.KX13/Mappers/UserInfoMapper.cs index e35bf37a..fae17dbd 100644 --- a/Migration.Tool.Core.KX13/Mappers/UserInfoMapper.cs +++ b/Migration.Tool.Core.KX13/Mappers/UserInfoMapper.cs @@ -35,7 +35,7 @@ protected override UserInfo MapInternal(KX13M.CmsUser source, UserInfo target, b target.UserName = source.UserName; target.FirstName = source.FirstName; target.LastName = source.LastName; - target.Email = source.Email; + target.Email = source.Email?.Trim(); // target.UserPassword = source.UserPassword; target.SetValue("UserPassword", source.UserPassword); target.UserEnabled = source.UserEnabled; From 7c25f3a8f4b5b01022d89756eec14490d3b2ddf8 Mon Sep 17 00:00:00 2001 From: akfakmot Date: Wed, 8 Oct 2025 09:34:52 +0200 Subject: [PATCH 3/4] Check XbyK media folder restrictions Add: Check XbyK media folder restrictions --- .../Services/MediaFileMigrator.cs | 6 +- Migration.Tool.CLI/README.md | 1 + Migration.Tool.CLI/appsettings.json | 1 + Migration.Tool.Common/ConfigurationNames.cs | 1 + Migration.Tool.Common/ToolConfiguration.cs | 3 + Migration.Tool.KXP.Api/KxpMediaFileFacade.cs | 68 ++++++++++++++++++- 6 files changed, 74 insertions(+), 6 deletions(-) diff --git a/KVA/Migration.Tool.Source/Services/MediaFileMigrator.cs b/KVA/Migration.Tool.Source/Services/MediaFileMigrator.cs index c8837314..e29377e0 100644 --- a/KVA/Migration.Tool.Source/Services/MediaFileMigrator.cs +++ b/KVA/Migration.Tool.Source/Services/MediaFileMigrator.cs @@ -186,11 +186,7 @@ private void RequireMigratedMediaFiles(List<(IMediaLibrary sourceLibrary, ICmsSi try { - if (newInstance) - { - mediaFileFacade.EnsureMediaFilePathExistsInLibrary(mf, targetMediaLibrary.LibraryID); - } - + mediaFileFacade.EnsureMediaFilePathExistsInLibrary(mf, targetMediaLibrary.LibraryID); mediaFileFacade.SetMediaFile(mf, newInstance); protocol.Success(ksMediaFile, mf, mapped); diff --git a/Migration.Tool.CLI/README.md b/Migration.Tool.CLI/README.md index bf0556f6..1ac90c9b 100644 --- a/Migration.Tool.CLI/README.md +++ b/Migration.Tool.CLI/README.md @@ -426,6 +426,7 @@ Add the options under the `Settings` section in the configuration file. | MigrateOnlyMediaFileInfo | If set to `true`, only the database representations of media files are migrated, without the files in the media folder in the project's file system. For example, enable this option if your media library files are mapped to a shared directory or Cloud storage.

If `false`, media files are migrated based on the `KxCmsDirPath` location. | | MigrateMediaToMediaLibrary | Determines whether media library files and attachments from the source instance are migrated to the target instance as media libraries or as [content item assets](https://docs.kentico.com/x/content_item_assets_xp) in the content hub. The default value is `false` – media files and attachments are migrated as content item assets.

See [Convert attachments and media library files to media libraries instead of content item assets](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) | | LegacyFlatAssetTree | Use legacy behavior of versions up to 2.3.0. Content folders for asset content items will be created in a flat structure (all under root folder) | +| LegacyPermissiveMediaLibrarySubfolders | XbyK has restrictions on media library subfolder name. It must contain only alphanumeric characters, underscores ('_'), hyphens ('-'), and cannot contain file names reserved by the operating system (e.g., ‘CON’, ‘PRN’, ‘AUX’ for Windows). If set to `true`, name checks will be bypassed. Note that this may impair some XbyK functionality like migration of media libraries to content hub. User may prefer using this option e.g. for repeated migration after initially migrating with previous versions of Migration Tool, and then manually changing the names in XbyK | | | AssetRootFolders | Dictionary defining the root folder for Asset content items per site: Key is site name (CMS_Site.SiteName). Value is in format _/FolderDisplayName1/FolderDisplayName2/..._ | | TargetWorkspaceName | The code name of the [workspace](https://docs.kentico.com/x/workspaces_xp) to which content items, content folders and other related entities are migrated. This workspace is used if another workspace is not specified explicitly, for example by the content item director API in [migration customizations](../Migration.Tool.Extensions/README.md). This configuration is not necessary if the target project only contains a single workspace. | | MemberIncludeUserSystemFields | Determines which system fields from the _CMS_User_ and _CMS_UserSettings_ tables are migrated to _CMS_Member_ in Xperience by Kentico. Fields that do not exist in _CMS_Member_ are automatically created.

The sample `appsettings.json` file included with the tool by default includes all user fields that can be migrated from Kentico Xperience 13. Exclude specific fields from the migration by removing them from this configuration option. | diff --git a/Migration.Tool.CLI/appsettings.json b/Migration.Tool.CLI/appsettings.json index 72260614..a1f8604c 100644 --- a/Migration.Tool.CLI/appsettings.json +++ b/Migration.Tool.CLI/appsettings.json @@ -28,6 +28,7 @@ "MigrateOnlyMediaFileInfo": false, "MigrateMediaToMediaLibrary": false, "LegacyFlatAssetTree": false, + "LegacyPermissiveMediaLibrarySubfolders": false, "AssetRootFolders": {}, "UseDeprecatedFolderPageType": false, "ConvertClassesToContentHub": "", diff --git a/Migration.Tool.Common/ConfigurationNames.cs b/Migration.Tool.Common/ConfigurationNames.cs index 23dda216..406557e8 100644 --- a/Migration.Tool.Common/ConfigurationNames.cs +++ b/Migration.Tool.Common/ConfigurationNames.cs @@ -24,6 +24,7 @@ public class ConfigurationNames public const string MigrateMediaToMediaLibrary = "MigrateMediaToMediaLibrary"; public const string LegacyFlatAssetTree = "LegacyFlatAssetTree"; + public const string LegacyPermissiveMediaLibrarySubfolders = "LegacyPermissiveMediaLibrarySubfolders"; public const string AssetRootFolders = "AssetRootFolders"; public const string UseDeprecatedFolderPageType = "UseDeprecatedFolderPageType"; diff --git a/Migration.Tool.Common/ToolConfiguration.cs b/Migration.Tool.Common/ToolConfiguration.cs index 3ff2980c..c6103114 100644 --- a/Migration.Tool.Common/ToolConfiguration.cs +++ b/Migration.Tool.Common/ToolConfiguration.cs @@ -49,6 +49,9 @@ public class ToolConfiguration [ConfigurationKeyName(ConfigurationNames.LegacyFlatAssetTree)] public bool? LegacyFlatAssetTree { get; set; } + [ConfigurationKeyName(ConfigurationNames.LegacyPermissiveMediaLibrarySubfolders)] + public bool? LegacyPermissiveMediaLibrarySubfolders { get; set; } + [ConfigurationKeyName(ConfigurationNames.AssetRootFolders)] public Dictionary? AssetRootFolders { get; set; } diff --git a/Migration.Tool.KXP.Api/KxpMediaFileFacade.cs b/Migration.Tool.KXP.Api/KxpMediaFileFacade.cs index 6437dfec..71b258ba 100644 --- a/Migration.Tool.KXP.Api/KxpMediaFileFacade.cs +++ b/Migration.Tool.KXP.Api/KxpMediaFileFacade.cs @@ -1,17 +1,22 @@ using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; using CMS.DataEngine; using CMS.MediaLibrary; using Microsoft.Extensions.Logging; +using Migration.Tool.Common; namespace Migration.Tool.KXP.Api; public class KxpMediaFileFacade { private readonly ILogger logger; + private readonly ToolConfiguration toolConfiguration; - public KxpMediaFileFacade(ILogger logger, KxpApiInitializer kxpApiInitializer) + public KxpMediaFileFacade(ILogger logger, KxpApiInitializer kxpApiInitializer, ToolConfiguration toolConfiguration) { this.logger = logger; + this.toolConfiguration = toolConfiguration; kxpApiInitializer.EnsureApiIsInitialized(); } @@ -42,11 +47,72 @@ public void SetMediaFile(MediaFileInfo mfi, bool newInstance) public MediaLibraryInfo GetMediaLibraryInfo(Guid mediaLibraryGuid) => MediaLibraryInfoProvider.ProviderObject.Get(mediaLibraryGuid); #pragma warning restore CS0618 // Type or member is obsolete + public static bool IsDirectoryNameAllowed(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + if (!name.All(x => char.IsLetterOrDigit(x) || new char[] { '-', '_', System.IO.Path.DirectorySeparatorChar, System.IO.Path.AltDirectorySeparatorChar }.Contains(x))) + { + return false; + } + + // Reserved device names (Windows only) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string[] reserved = { + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + }; + + string upper = name.ToUpperInvariant(); + if (reserved.Contains(upper) || reserved.Any(r => upper.StartsWith(r + ".", StringComparison.Ordinal))) + { + return false; + } + } + + return true; + } + + private readonly HashSet fixedFolderPaths = []; + #pragma warning disable CS0618 // Type or member is obsolete public void EnsureMediaFilePathExistsInLibrary(MediaFileInfo mfi, int libraryId) #pragma warning restore CS0618 // Type or member is obsolete { string? librarySubDir = System.IO.Path.GetDirectoryName(mfi.FilePath); + if (toolConfiguration.LegacyFlatAssetTree != true) + { + if (!string.IsNullOrEmpty(librarySubDir) && !IsDirectoryNameAllowed(librarySubDir)) + { + // Try an automatic correction - replacing stretches of whitespaces with underscore. + // If that succeeds, inform user by a warning. Otherwise raise error and defer fixing the issue to user. + string fixedLibrarySubdir = Regex.Replace(librarySubDir.Trim(), @"\s+", "-"); + + string bypassMessage = $"If you want to bypass this, use {nameof(toolConfiguration.LegacyPermissiveMediaLibrarySubfolders)} appsettings option. Please refer to README for potential sideeffects."; + + if (IsDirectoryNameAllowed(fixedLibrarySubdir)) + { + librarySubDir = fixedLibrarySubdir; + mfi.FilePath = $"{librarySubDir}/{System.IO.Path.GetFileName(mfi.FilePath)}"; // Don't use Path.Combine, because it doesn't work correctly with subsequent internal CMS logic + + if (!fixedFolderPaths.Contains(librarySubDir)) + { + fixedFolderPaths.Add(librarySubDir); + logger.LogWarning($"Media library subfolder '{{MediaLibrarySubfolder}}' was transformed to XbyK allowed format '{{FixedMediaLibrarySubfolder}}'. {bypassMessage}", librarySubDir, fixedLibrarySubdir); + } + } + else + { + throw new InvalidOperationException($"Media library subfolder name {librarySubDir} is not in XbyK allowed format. It must contain only alphanumeric characters, underscores ('_'), hyphens ('-'), and cannot contain file names reserved by the operating system. {bypassMessage}"); + } + } + } + #pragma warning disable CS0618 // Type or member is obsolete MediaLibraryInfoProvider.CreateMediaLibraryFolder(libraryId, $"{librarySubDir}"); #pragma warning restore CS0618 // Type or member is obsolete From 47d3659dd0af305a177cd6ccbdb6af2735347bb8 Mon Sep 17 00:00:00 2001 From: Matthew Sandstrom Date: Thu, 9 Oct 2025 01:42:23 -0400 Subject: [PATCH 4/4] Add links to new upgrade materials from training guides (#525) Approved by TWs. --- Migration.Tool.CLI/README.md | 4 ++++ Migration.Tool.Extensions/README.md | 4 +++- docs/Supported-Data.md | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Migration.Tool.CLI/README.md b/Migration.Tool.CLI/README.md index bf0556f6..ce8957c1 100644 --- a/Migration.Tool.CLI/README.md +++ b/Migration.Tool.CLI/README.md @@ -280,12 +280,16 @@ Custom table migration does NOT include: Media library files are migrated as [content item assets](https://docs.kentico.com/x/content_item_assets_xp) to Content hub into a content folder `/`. All assets are created in the default language of the respective site. Migrated assets are created as content items of a _Legacy media file_ content type (code name `Legacy.Mediafile`) created by the tool. +If you want to use Xperience by Kentico's [automatic image optimization](https://docs.kentico.com/documentation/developers-and-admins/development/content-types#configure-asset-content-types) feature for the _Legacy media file_ type, follow along with our [guide about customizing asset migration](https://docs.kentico.com/x/optimize_images_during_upgrade_guides). + If required, you can [configure the tool](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) to instead migrate media libraries as media libraries on the target instance. #### Attachments Attachment files are migrated as [content item assets](https://docs.kentico.com/x/content_item_assets_xp) to Content hub into a content folder `/__Attachments`. Assets are created in the specified language if the language is available (e.g., attachments of pages). Migrated assets are created as content items of a _Legacy attachment_ content type (code name `Legacy.Attachment`) created by the tool. +If you want to use Xperience by Kentico's [automatic image optimization](https://docs.kentico.com/documentation/developers-and-admins/development/content-types#configure-asset-content-types) feature for the _Legacy attachment_ type, follow along with our [guide about customizing asset migration](https://docs.kentico.com/x/optimize_images_during_upgrade_guides). + If required, you can [configure the tool](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) to instead migrate attachments as media libraries on the target instance. #### Forms diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index 4419dcd1..c9ba2768 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -335,6 +335,8 @@ You can customize class mappings to adjust the content model between the source **Note**: Your mappings now replace the default migration functionality for all data classes (page types, custom tables or custom module classes) that you use as a source. Any class where you set at least one source field is affected. If you map only some fields from a source class, the remaining fields are not migrated at all. +If you need class mappings to alter several data classes in Kentico Xperience 13, consider [using AI tools to help you generate mappings quickly](https://docs.kentico.com/x/speed_up_remodeling_with_ai_guides). + ### Remodel page types as reusable field schemas guide For an end-to-end example of how to extract common fields from two page types from Kentico Xperience 13 and move them to a [reusable field schema](https://docs.kentico.com/x/D4_OD) shared by both web page content types in Xperience by Kentico follow this [migration guide](https://docs.kentico.com/x/remodel_page_types_as_reusable_field_schemas_guides) in the documentation. @@ -356,7 +358,7 @@ This feature allows you to link child pages as referenced content items of a pag This feature is available by means of content item director. -You can apply a simple general rule to link child pages e.g. in `Children` field or you can apply more elaborate rules. You can see samples of both approaches in [SampleChildLinkDirector.cs](./CommunityMigrations/SampleChildLinkDirector.cs) +You can apply a simple general rule to link child pages e.g. in `Children` field or you can apply more elaborate rules. You can see samples of both approaches in [SampleChildLinkDirector.cs](./CommunityMigrations/SampleChildLinkDirector.cs) or follow along with our [guide to transfer page hierarchy to the Content hub](https://docs.kentico.com/x/transfer_page_hierarchy_to_content_hub_guides). After implementing the content item director, you need to [register the director](#register-migrations) in the system. diff --git a/docs/Supported-Data.md b/docs/Supported-Data.md index eec21902..6fc89f45 100644 --- a/docs/Supported-Data.md +++ b/docs/Supported-Data.md @@ -36,6 +36,7 @@ Currently, the Kentico Migration Tool supports the following types of data: - Page permissions are currently not supported by the Kentico Migration Tool and are not migrated. If you need page ACLs to be migrated, please [open an issue and request the feature](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool/issues/new?assignees=&labels=&projects=&template=feature_request.md). - Migration of Page Builder content is only available for Kentico Xperience 13. - If you are [migrating a Kentico 12 MVC application](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool/blob/master/docs/Usage-Guide.md#kentico-12-mvc) you can upgrade it to Kentico Xperience 13 using the Kentico Installation Manager (KIM) and then upgrade to Xperience by Kentico with Page Builder content. + - You can find examples of how to update your page retrieval code to work with Xperience by Kentico's _content items_ [in our guides](https://docs.kentico.com/x/upgrade_content_retrieval_code_guides). - **Page attachments** - Page attachments are not supported in Xperience by Kentico. Attachments are migrated into media libraries. See [`Migration.Tool.CLI/README.md - Attachments`](../Migration.Tool.CLI/README.md#Attachments) for detailed information about the conversion process. - **Preset page templates** (_Custom page templates_ in Kentico Xperience 13)