Skip to content

Commit

Permalink
Merge pull request #254 from Kentico/feat/url_replacement_in_RTE
Browse files Browse the repository at this point in the history
Media file & attachment url replacement in Rich Text Editor values + Direct media path support
  • Loading branch information
tkrch authored Oct 3, 2024
2 parents 4e84264 + eb9e7c1 commit f905fce
Show file tree
Hide file tree
Showing 17 changed files with 887 additions and 246 deletions.
80 changes: 80 additions & 0 deletions KVA/Migration.Toolkit.Source/Helpers/MediaHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Microsoft.Data.SqlClient;
using Migration.Toolkit.Common.Helpers;
using Migration.Toolkit.Source.Model;

namespace Migration.Toolkit.Source.Helpers;

public static class MediaHelper
{
public static IMediaFile? GetMediaFile(MatchMediaLinkResult matchResult, ModelFacade modelFacade)
{
switch (matchResult)
{
case { Success: true, LinkKind: MediaLinkKind.Guid, MediaGuid: var mediaGuid, MediaKind: MediaKind.MediaFile, LinkSiteId: var linkSiteId }:
{
var mediaFile = modelFacade.SelectWhere<IMediaFile>("FileGUID = @mediaFileGuid AND FileSiteID = @fileSiteID",
new SqlParameter("mediaFileGuid", mediaGuid),
new SqlParameter("fileSiteID", linkSiteId)
)
.FirstOrDefault();

return mediaFile;
}
case { Success: true, LinkKind: MediaLinkKind.DirectMediaPath, LibraryDir: var libraryDir, Path: var path, LinkSiteId: var linkSiteId }:
{
if (path == null)
{
throw new InvalidOperationException($"Cannot determine media file for link match {matchResult}");
}

var mediaLibraries = modelFacade.SelectWhere<IMediaLibrary>(
"LibraryUseDirectPathForContent = 1 AND LibraryFolder = @libraryFolder AND LibrarySiteID = @librarySiteID",
new SqlParameter("libraryFolder", libraryDir),
new SqlParameter("librarySiteID", linkSiteId)
).ToList();
switch (mediaLibraries)
{
case [var mediaLibrary]:
{
string filePath = path.Replace($"/{mediaLibrary.LibraryFolder}/", "", StringComparison.InvariantCultureIgnoreCase);
return modelFacade.SelectWhere<IMediaFile>("FileLibraryID = @fileLibraryID AND FilePath = @filePath AND FileSiteID = @fileSiteID",
new SqlParameter("fileLibraryID", mediaLibrary.LibraryID),
new SqlParameter("filePath", filePath),
new SqlParameter("fileSiteID", linkSiteId))
.ToList() switch
{
[var mediaFile] => mediaFile,
{ Count: > 1 } => throw new InvalidOperationException($"Multiple media file were found for path {path}, site {linkSiteId} and library {libraryDir}"),
{ Count: 0 } =>
// this may happen and is valid scenaria
null,
_ => null
};
}
case { Count: > 1 }:
{
break;
}
case { Count: 0 }:
default:
{
break;
}
}

return null;
}
default:
{
return null;
}
}
}

public static ICmsAttachment? GetAttachment(MatchMediaLinkResult matchResult, ModelFacade modelFacade) =>
modelFacade.SelectWhere<ICmsAttachment>("AttachmentSiteID = @attachmentSiteID AND AttachmentGUID = @attachmentGUID",
new SqlParameter("attachmentSiteID", matchResult.LinkSiteId),
new SqlParameter("attachmentGUID", matchResult.MediaGuid)
)
.FirstOrDefault();
}
1 change: 1 addition & 0 deletions KVA/Migration.Toolkit.Source/KsCoreDiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static IServiceCollection UseKsToolkitCore(this IServiceCollection servic
services.AddSingleton<EntityIdentityFacade>();
services.AddSingleton<IdentityLocator>();
services.AddSingleton<IAssetFacade, AssetFacade>();
services.AddSingleton<MediaLinkServiceFactory>();

services.AddTransient<BulkDataCopyService>();
services.AddTransient<CmsRelationshipService>();
Expand Down
62 changes: 60 additions & 2 deletions KVA/Migration.Toolkit.Source/Mappers/ContentItemMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
using Migration.Toolkit.KXP.Api.Services.CmsClass;
using Migration.Toolkit.Source.Auxiliary;
using Migration.Toolkit.Source.Contexts;
using Migration.Toolkit.Source.Helpers;
using Migration.Toolkit.Source.Model;
using Migration.Toolkit.Source.Services;
using Migration.Toolkit.Source.Services.Model;
Expand Down Expand Up @@ -56,7 +57,8 @@ public class ContentItemMapper(
SpoiledGuidContext spoiledGuidContext,
EntityIdentityFacade entityIdentityFacade,
IAssetFacade assetFacade,
ToolkitConfiguration configuration
ToolkitConfiguration configuration,
MediaLinkServiceFactory mediaLinkServiceFactory
) : UmtMapperBase<CmsTreeMapperSource>
{
private const string CLASS_FIELD_CONTROL_NAME = "controlname";
Expand Down Expand Up @@ -627,6 +629,62 @@ ICmsSite site
{
target[columnName] = value;
}


var newField = newFormInfo.GetFormField(columnName);
if (newField == null)
{
var commonFields = UnpackReusableFieldSchemas(newFormInfo.GetFields<FormSchemaInfo>()).ToArray();
newField = commonFields
.FirstOrDefault(cf => ReusableSchemaService.RemoveClassPrefix(nodeClass.ClassName, cf.Name).Equals(columnName, StringComparison.InvariantCultureIgnoreCase));
}
string? newControlName = newField?.Settings[CLASS_FIELD_CONTROL_NAME]?.ToString()?.ToLowerInvariant();
if (newControlName?.Equals(FormComponents.AdminRichTextEditorComponent, StringComparison.InvariantCultureIgnoreCase) == true && target[columnName] is string { } html && !string.IsNullOrWhiteSpace(html) &&
!configuration.MigrateMediaToMediaLibrary)
{
var mediaLinkService = mediaLinkServiceFactory.Create();
var htmlProcessor = new HtmlProcessor(html, mediaLinkService);

target[columnName] = 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<ICmsDocument>(attachmentDocumentId)?.DocumentCulture;
}
return assetFacade.GetAssetUri(attachment, culture);
}
default:
break;
}
return original;
});
}
}
}

Expand All @@ -635,7 +693,7 @@ private async Task ConvertToAsset(Dictionary<string, object?> target, ICmsTree c
List<object> mfis = [];
bool hasMigratedAsset = false;
if (value is string link &&
MediaHelper.MatchMediaLink(link) is (true, var mediaLinkKind, var mediaKind, var path, var mediaGuid) result)
mediaLinkServiceFactory.Create().MatchMediaLink(link, site.SiteID) is (true, var mediaLinkKind, var mediaKind, var path, var mediaGuid, var linkSiteId, var libraryDir) result)
{
if (mediaLinkKind == MediaLinkKind.Path)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ private void WalkProperties(int siteId, JObject properties, List<EditingFormCont
var nv = new List<object>();
foreach (var asi in items)
{
var attachment = modelFacade.SelectWhere<ICmsAttachment>("AttachmentSiteID = @siteId AND AttachmentGUID = @attachmentGUID", new SqlParameter("attachmentSiteID", siteId),
var attachment = modelFacade.SelectWhere<ICmsAttachment>("AttachmentSiteID = @attachmentSiteID AND AttachmentGUID = @attachmentGUID", new SqlParameter("attachmentSiteID", siteId),
new SqlParameter("attachmentGUID", asi.FileGuid))
.FirstOrDefault();
if (attachment != null)
Expand Down
112 changes: 81 additions & 31 deletions KVA/Migration.Toolkit.Source/Services/AssetFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ public interface IAssetFacade
/// <param name="attachment"></param>
/// <param name="contentLanguageName"></param>
/// <returns></returns>
(Guid ownerContentItemGuid, Guid assetGuid) GetRef(ICmsAttachment attachment, string? contentLanguageName = null);
(Guid ownerContentItemGuid, Guid assetGuid) GetRef(ICmsAttachment attachment, string? contentLanguageName);

Task PreparePrerequisites();

string GetAssetUri(IMediaFile mediaFile, string? contentLanguageName = null);
string GetAssetUri(ICmsAttachment attachment, string? contentLanguageName);
}

public class AssetFacade(
Expand Down Expand Up @@ -81,6 +84,10 @@ public async Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile
Debug.Assert(mediaLibrary.LibrarySiteID == site.SiteID, "mediaLibrary.LibrarySiteID == site.SiteID");

string? mediaLibraryAbsolutePath = GetMediaLibraryAbsolutePath(toolkitConfiguration, site, mediaLibrary, modelFacade);
if (toolkitConfiguration.MigrateOnlyMediaFileInfo.GetValueOrDefault(false))
{
throw new InvalidOperationException($"Configuration 'Settings.MigrateOnlyMediaFileInfo' is set to to 'true', for migration of media files to content items this setting is required to be 'false'");
}
if (string.IsNullOrWhiteSpace(mediaLibraryAbsolutePath))
{
throw new InvalidOperationException($"Invalid media file path generated for {mediaFile} and {mediaLibrary} on {site}");
Expand Down Expand Up @@ -185,7 +192,7 @@ public async Task<ContentItemSimplifiedModel> FromAttachment(ICmsAttachment atta
ContentItemGUID = translatedAttachmentGuid,
ContentItemContentFolderGUID = (await EnsureFolderStructure(mediaFolder, folder))?.ContentFolderGUID ?? folder.ContentFolderGUID,
IsSecured = null,
ContentTypeName = LegacyMediaFileContentType.ClassName,
ContentTypeName = LegacyAttachmentContentType.ClassName,
Name = $"{attachment.AttachmentGUID}_{translatedAttachmentGuid}",
IsReusable = true,
LanguageData = languageData,
Expand Down Expand Up @@ -294,7 +301,7 @@ public async Task<ContentItemSimplifiedModel> FromAttachment(ICmsAttachment atta
}

/// <inheritdoc />
public (Guid ownerContentItemGuid, Guid assetGuid) GetRef(ICmsAttachment attachment, string? contentLanguageName = null)
public (Guid ownerContentItemGuid, Guid assetGuid) GetRef(ICmsAttachment attachment, string? contentLanguageName)
{
var (_, translatedAttachmentGuid) = entityIdentityFacade.Translate(attachment);
return (translatedAttachmentGuid, GuidHelper.CreateAssetGuid(translatedAttachmentGuid, contentLanguageName ?? DefaultContentLanguage));
Expand All @@ -310,6 +317,19 @@ public async Task PreparePrerequisites()
}
}

public string GetAssetUri(IMediaFile mediaFile, string? contentLanguageName = null)
{
string contentLanguageSafe = contentLanguageName ?? DefaultContentLanguage;
var (ownerContentItemGuid, _) = GetRef(mediaFile, contentLanguageName);
return $"/getContentAsset/{ownerContentItemGuid}/{LegacyMediaFileAssetField.Guid}/{mediaFile.FileName}?language={contentLanguageSafe}";
}

public string GetAssetUri(ICmsAttachment attachment, string? contentLanguageName)
{
var (ownerContentItemGuid, _) = GetRef(attachment, contentLanguageName ?? DefaultContentLanguage);
return $"/getContentAsset/{ownerContentItemGuid}/{LegacyAttachmentAssetField.Guid}/{attachment.AttachmentName}?language={contentLanguageName ?? DefaultContentLanguage}";
}

private void AssertSuccess(IImportResult importResult, IUmtModel model)
{
switch (importResult)
Expand Down Expand Up @@ -339,6 +359,17 @@ private void AssertSuccess(IImportResult importResult, IUmtModel model)
}
}

internal static readonly FormField LegacyMediaFileAssetField = new()
{
Column = "Asset",
ColumnType = "contentitemasset",
AllowEmpty = true,
Visible = true,
Enabled = true,
Guid = new Guid("DFC3D011-8F63-43F6-9ED8-4B444333A1D0"),
Properties = new FormFieldProperties { FieldCaption = "Asset", },
Settings = new FormFieldSettings { CustomProperties = new Dictionary<string, object?> { { "AllowedExtensions", "_INHERITED_" } }, ControlName = "Kentico.Administration.ContentItemAssetUploader" }
};
internal static readonly DataClassModel LegacyMediaFileContentType = new()
{
ClassName = "Legacy.MediaFile",
Expand All @@ -352,20 +383,21 @@ private void AssertSuccess(IImportResult importResult, IUmtModel model)
ClassWebPageHasUrl = false,
Fields =
[
new()
{
Column = "Asset",
ColumnType = "contentitemasset",
AllowEmpty = true,
Visible = true,
Enabled = true,
Guid = new Guid("DFC3D011-8F63-43F6-9ED8-4B444333A1D0"),
Properties = new FormFieldProperties { FieldCaption = "Asset", },
Settings = new FormFieldSettings { CustomProperties = new Dictionary<string, object?> { { "AllowedExtensions", "_INHERITED_" } }, ControlName = "Kentico.Administration.ContentItemAssetUploader" }
}
LegacyMediaFileAssetField
]
};

internal static readonly FormField LegacyAttachmentAssetField = new()
{
Column = "Asset",
ColumnType = "contentitemasset",
AllowEmpty = true,
Visible = true,
Enabled = true,
Guid = new Guid("50C2BC4C-A8FF-46BA-95C2-0E74752D147F"),
Properties = new FormFieldProperties { FieldCaption = "Asset", },
Settings = new FormFieldSettings { CustomProperties = new Dictionary<string, object?> { { "AllowedExtensions", "_INHERITED_" } }, ControlName = "Kentico.Administration.ContentItemAssetUploader" }
};
internal static readonly DataClassModel LegacyAttachmentContentType = new()
{
ClassName = "Legacy.Attachment",
Expand All @@ -379,17 +411,7 @@ private void AssertSuccess(IImportResult importResult, IUmtModel model)
ClassWebPageHasUrl = false,
Fields =
[
new()
{
Column = "Asset",
ColumnType = "contentitemasset",
AllowEmpty = true,
Visible = true,
Enabled = true,
Guid = new Guid("50C2BC4C-A8FF-46BA-95C2-0E74752D147F"),
Properties = new FormFieldProperties { FieldCaption = "Asset", },
Settings = new FormFieldSettings { CustomProperties = new Dictionary<string, object?> { { "AllowedExtensions", "_INHERITED_" } }, ControlName = "Kentico.Administration.ContentItemAssetUploader" }
}
LegacyAttachmentAssetField
]
};

Expand Down Expand Up @@ -417,34 +439,62 @@ internal static ContentFolderModel GetAssetFolder(ICmsSite site)
private const string DirMedia = "media";
public static string? GetMediaLibraryAbsolutePath(ToolkitConfiguration toolkitConfiguration, ICmsSite ksSite, IMediaLibrary ksMediaLibrary, ModelFacade modelFacade)
{
string? cmsMediaLibrariesFolder = KenticoHelper.GetSettingsKey(modelFacade, ksSite.SiteID, "CMSMediaLibrariesFolder");
bool cmsUseMediaLibrariesSiteFolder = !"false".Equals(KenticoHelper.GetSettingsKey(modelFacade, ksSite.SiteID, "CMSUseMediaLibrariesSiteFolder"), StringComparison.InvariantCultureIgnoreCase);

string? sourceMediaLibraryPath = null;
if (!toolkitConfiguration.MigrateOnlyMediaFileInfo.GetValueOrDefault(true) &&
if (!toolkitConfiguration.MigrateOnlyMediaFileInfo.GetValueOrDefault(false) &&
!string.IsNullOrWhiteSpace(toolkitConfiguration.KxCmsDirPath))
{
string? cmsMediaLibrariesFolder = KenticoHelper.GetSettingsKey(modelFacade, ksSite.SiteID, "CMSMediaLibrariesFolder");
var pathParts = new List<string>();
if (cmsMediaLibrariesFolder != null)
{
if (Path.IsPathRooted(cmsMediaLibrariesFolder))
{
sourceMediaLibraryPath = Path.Combine(cmsMediaLibrariesFolder, ksSite.SiteName, ksMediaLibrary.LibraryFolder);
pathParts.Add(cmsMediaLibrariesFolder);
if (cmsUseMediaLibrariesSiteFolder)
{
pathParts.Add(ksSite.SiteName);
}
pathParts.Add(ksMediaLibrary.LibraryFolder);
}
else
{
if (cmsMediaLibrariesFolder.StartsWith("~/"))
{
string cleared = $"{cmsMediaLibrariesFolder[2..]}".Replace("/", "\\");
sourceMediaLibraryPath = Path.Combine(toolkitConfiguration.KxCmsDirPath, cleared, ksSite.SiteName, ksMediaLibrary.LibraryFolder);
pathParts.Add(toolkitConfiguration.KxCmsDirPath);
pathParts.Add(cleared);
if (cmsUseMediaLibrariesSiteFolder)
{
pathParts.Add(ksSite.SiteName);
}
pathParts.Add(ksMediaLibrary.LibraryFolder);
}
else
{
sourceMediaLibraryPath = Path.Combine(toolkitConfiguration.KxCmsDirPath, cmsMediaLibrariesFolder, ksSite.SiteName, ksMediaLibrary.LibraryFolder);
pathParts.Add(toolkitConfiguration.KxCmsDirPath);
pathParts.Add(cmsMediaLibrariesFolder);
if (cmsUseMediaLibrariesSiteFolder)
{
pathParts.Add(ksSite.SiteName);
}
pathParts.Add(ksMediaLibrary.LibraryFolder);
}
}
}
else
{
sourceMediaLibraryPath = Path.Combine(toolkitConfiguration.KxCmsDirPath, ksSite.SiteName, DirMedia, ksMediaLibrary.LibraryFolder);
pathParts.Add(toolkitConfiguration.KxCmsDirPath);
if (cmsUseMediaLibrariesSiteFolder)
{
pathParts.Add(ksSite.SiteName);
}
pathParts.Add(DirMedia);
pathParts.Add(ksMediaLibrary.LibraryFolder);
}

sourceMediaLibraryPath = Path.Combine(pathParts.ToArray());
}

return sourceMediaLibraryPath;
Expand Down
Loading

0 comments on commit f905fce

Please sign in to comment.