diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
index 4627b48d3797..de573d23d183 100644
--- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
+++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
@@ -118,8 +118,8 @@ private IEnumerable FindLocalLinkIds(string text)
}
}
- // todo remove at some point?
- private IEnumerable FindLegacyLocalLinkIds(string text)
+ [Obsolete("This is a temporary method to support legacy formats until we are sure all data has been migration. Scheduled for removal in v17")]
+ public IEnumerable FindLegacyLocalLinkIds(string text)
{
// Parse internal links
MatchCollection tags = LocalLinkPattern.Matches(text);
@@ -148,7 +148,8 @@ private IEnumerable FindLegacyLocalLinkIds(string text)
}
}
- private class LocalLinkTag
+ [Obsolete("This is a temporary method to support legacy formats until we are sure all data has been migration. Scheduled for removal in v17")]
+ public class LocalLinkTag
{
public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref)
{
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 6c4becfc6d25..745e8fb5af80 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -103,5 +103,6 @@ protected virtual void DefinePlan()
To("{6C04B137-0097-4938-8C6A-276DF1A0ECA8}");
To("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}");
To("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}");
+ To("{42E44F9E-7262-4269-922D-7310CB48E724}");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertLocalLinks.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertLocalLinks.cs
new file mode 100644
index 000000000000..10118acf7420
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertLocalLinks.cs
@@ -0,0 +1,188 @@
+using Microsoft.Extensions.Logging;
+using NPoco;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.Editors;
+using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Web;
+using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
+using Umbraco.Cms.Infrastructure.Persistence;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0;
+
+public class ConvertLocalLinks : MigrationBase
+{
+ private readonly IUmbracoContextFactory _umbracoContextFactory;
+ private readonly IContentTypeService _contentTypeService;
+ private readonly ILogger _logger;
+ private readonly IDataTypeService _dataTypeService;
+ private readonly ILanguageService _languageService;
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly LocalLinkProcessor _localLinkProcessor;
+ private readonly IMediaTypeService _mediaTypeService;
+
+ public ConvertLocalLinks(
+ IMigrationContext context,
+ IUmbracoContextFactory umbracoContextFactory,
+ IContentTypeService contentTypeService,
+ ILogger logger,
+ IDataTypeService dataTypeService,
+ ILanguageService languageService,
+ IJsonSerializer jsonSerializer,
+ LocalLinkProcessor localLinkProcessor,
+ IMediaTypeService mediaTypeService)
+ : base(context)
+ {
+ _umbracoContextFactory = umbracoContextFactory;
+ _contentTypeService = contentTypeService;
+ _logger = logger;
+ _dataTypeService = dataTypeService;
+ _languageService = languageService;
+ _jsonSerializer = jsonSerializer;
+ _localLinkProcessor = localLinkProcessor;
+ _mediaTypeService = mediaTypeService;
+ }
+
+ protected override void Migrate()
+ {
+ IEnumerable propertyEditorAliases = _localLinkProcessor.GetSupportedPropertyEditorAliases();
+
+ using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
+ var languagesById = _languageService.GetAllAsync().GetAwaiter().GetResult()
+ .ToDictionary(language => language.Id);
+
+ IEnumerable allContentTypes = _contentTypeService.GetAll();
+ IEnumerable contentPropertyTypes = allContentTypes
+ .SelectMany(ct => ct.PropertyTypes);
+
+ IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray();
+ IEnumerable mediaPropertyTypes = allMediaTypes
+ .SelectMany(ct => ct.PropertyTypes);
+
+ var relevantPropertyEditors =
+ contentPropertyTypes.Concat(mediaPropertyTypes).DistinctBy(pt => pt.Id)
+ .Where(pt => propertyEditorAliases.Contains(pt.PropertyEditorAlias))
+ .GroupBy(pt => pt.PropertyEditorAlias)
+ .ToDictionary(group => group.Key, group => group.ToArray());
+
+
+ foreach (var propertyEditorAlias in propertyEditorAliases)
+ {
+ if (relevantPropertyEditors.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false)
+ {
+ continue;
+ }
+
+ _logger.LogInformation(
+ "Migration starting for all properties of type: {propertyEditorAlias}",
+ propertyEditorAlias);
+ if (ProcessPropertyTypes(propertyTypes, languagesById))
+ {
+ _logger.LogInformation(
+ "Migration succeeded for all properties of type: {propertyEditorAlias}",
+ propertyEditorAlias);
+ }
+ else
+ {
+ _logger.LogError(
+ "Migration failed for one or more properties of type: {propertyEditorAlias}",
+ propertyEditorAlias);
+ }
+ }
+ }
+
+ private bool ProcessPropertyTypes(IPropertyType[] propertyTypes, IDictionary languagesById)
+ {
+ foreach (IPropertyType propertyType in propertyTypes)
+ {
+ IDataType dataType = _dataTypeService.GetAsync(propertyType.DataTypeKey).GetAwaiter().GetResult()
+ ?? throw new InvalidOperationException("The data type could not be fetched.");
+
+ IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor()
+ ?? throw new InvalidOperationException(
+ "The data type value editor could not be fetched.");
+
+ Sql sql = Sql()
+ .Select()
+ .From()
+ .InnerJoin()
+ .On((propertyData, contentVersion) =>
+ propertyData.VersionId == contentVersion.Id)
+ .LeftJoin()
+ .On((contentVersion, documentVersion) =>
+ contentVersion.Id == documentVersion.Id)
+ .Where(
+ (propertyData, contentVersion, documentVersion) =>
+ (contentVersion.Current == true || documentVersion.Published == true)
+ && propertyData.PropertyTypeId == propertyType.Id);
+
+ List propertyDataDtos = Database.Fetch(sql);
+ if (propertyDataDtos.Any() is false)
+ {
+ continue;
+ }
+
+ foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
+ {
+ if (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor))
+ {
+ Database.Update(propertyDataDto);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private bool ProcessPropertyDataDto(PropertyDataDto propertyDataDto, IPropertyType propertyType,
+ IDictionary languagesById, IDataValueEditor valueEditor)
+ {
+ // NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies
+ var culture = propertyType.VariesByCulture()
+ && propertyDataDto.LanguageId.HasValue
+ && languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language)
+ ? language.IsoCode
+ : null;
+
+ if (culture is null && propertyType.VariesByCulture())
+ {
+ // if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario,
+ // and we can't really handle it in any other way than logging; in all likelihood this is an old property version,
+ // and it won't cause any runtime issues
+ _logger.LogWarning(
+ " - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
+ propertyDataDto.Id,
+ propertyDataDto.LanguageId,
+ propertyType.Name,
+ propertyType.Id,
+ propertyType.Alias);
+ return false;
+ }
+
+ var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null;
+ var property = new Property(propertyType);
+ property.SetValue(propertyDataDto.Value, culture, segment);
+ var toEditorValue = valueEditor.ToEditor(property, culture, segment);
+
+ _localLinkProcessor.ProcessToEditorValue(toEditorValue);
+
+ var editorValue = _jsonSerializer.Serialize(toEditorValue);
+ var dbValue = valueEditor.FromEditor(new ContentPropertyData(editorValue, null), null);
+ if (dbValue is not string stringValue || stringValue.DetectIsJson() is false)
+ {
+ _logger.LogError(
+ " - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
+ propertyDataDto.Id,
+ propertyType.Name,
+ propertyType.Id,
+ propertyType.Alias);
+ return false;
+ }
+
+ propertyDataDto.TextValue = stringValue;
+ return true;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ConvertLocalLinkComposer.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ConvertLocalLinkComposer.cs
new file mode 100644
index 000000000000..a34def894cd7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ConvertLocalLinkComposer.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Core.DependencyInjection;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
+
+[Obsolete("Will be removed in V18")]
+public class ConvertLocalLinkComposer : IComposer
+{
+ public void Compose(IUmbracoBuilder builder)
+ {
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ITypedLocalLinkProcessor.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ITypedLocalLinkProcessor.cs
new file mode 100644
index 000000000000..6d5b360afc99
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/LocalLinks/ITypedLocalLinkProcessor.cs
@@ -0,0 +1,11 @@
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
+
+[Obsolete("Will be removed in V18")]
+public interface ITypedLocalLinkProcessor
+{
+ public Type PropertyEditorValueType { get; }
+
+ public IEnumerable PropertyEditorAliases { get; }
+
+ public Func