Fix mistakes in 15.0.0 migrations (#17814)

* Fix ConvertLocalLinks migration and add a new migration in case the old one has already run

* RebuildCache

* Clear cache means clear ALL caches

* Fix Block Markup recursion

* Fix Unittest mock constructor
This commit is contained in:
Sven Geusens
2024-12-18 15:24:43 +01:00
committed by GitHub
parent e8c4fb96de
commit 0a56aaaf54
10 changed files with 474 additions and 16 deletions

View File

@@ -47,6 +47,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
private readonly IKeyValueService _keyValueService;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly AppCaches _appCaches;
private readonly DistributedCache _distributedCache;
private readonly IScopeAccessor _scopeAccessor;
private readonly ICoreScopeProvider _scopeProvider;
@@ -62,7 +63,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
IDatabaseCacheRebuilder databaseCacheRebuilder,
DistributedCache distributedCache,
IKeyValueService keyValueService,
IServiceScopeFactory serviceScopeFactory)
IServiceScopeFactory serviceScopeFactory,
AppCaches appCaches)
{
_scopeProvider = scopeProvider;
_scopeAccessor = scopeAccessor;
@@ -72,10 +74,36 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
_databaseCacheRebuilder = databaseCacheRebuilder;
_keyValueService = keyValueService;
_serviceScopeFactory = serviceScopeFactory;
_appCaches = appCaches;
_distributedCache = distributedCache;
_logger = _loggerFactory.CreateLogger<MigrationPlanExecutor>();
}
[Obsolete("Use the non obsoleted constructor instead. Scheduled for removal in v17")]
public MigrationPlanExecutor(
ICoreScopeProvider scopeProvider,
IScopeAccessor scopeAccessor,
ILoggerFactory loggerFactory,
IMigrationBuilder migrationBuilder,
IUmbracoDatabaseFactory databaseFactory,
IDatabaseCacheRebuilder databaseCacheRebuilder,
DistributedCache distributedCache,
IKeyValueService keyValueService,
IServiceScopeFactory serviceScopeFactory)
: this(
scopeProvider,
scopeAccessor,
loggerFactory,
migrationBuilder,
databaseFactory,
databaseCacheRebuilder,
distributedCache,
keyValueService,
serviceScopeFactory,
StaticServiceProvider.Instance.GetRequiredService<AppCaches>())
{
}
public string Execute(MigrationPlan plan, string fromState) => ExecutePlan(plan, fromState).FinalState;
/// <summary>
@@ -303,6 +331,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor
private void RebuildCache()
{
_appCaches.RuntimeCache.Clear();
_appCaches.IsolatedCaches.ClearAllCaches();
_databaseCacheRebuilder.Rebuild();
_distributedCache.RefreshAllPublishedSnapshot();
}

View File

@@ -105,5 +105,6 @@ public class UmbracoPlan : MigrationPlan
To<V_15_0_0.ConvertRichTextEditorProperties>("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}");
To<V_15_0_0.ConvertLocalLinks>("{42E44F9E-7262-4269-922D-7310CB48E724}");
To<V_15_1_0.RebuildCacheMigration>("{7B51B4DE-5574-4484-993E-05D12D9ED703}");
To<V_15_1_0.FixConvertLocalLinks>("{F3D3EF46-1B1F-47DB-B437-7D573EEDEB98}");
}
}

View File

@@ -1,7 +1,9 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Scoping;
@@ -26,6 +28,34 @@ public class ConvertLocalLinks : MigrationBase
private readonly LocalLinkProcessor _localLinkProcessor;
private readonly IMediaTypeService _mediaTypeService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly LocalLinkMigrationTracker _linkMigrationTracker;
[Obsolete("Use non obsoleted contructor instead")]
public ConvertLocalLinks(
IMigrationContext context,
IUmbracoContextFactory umbracoContextFactory,
IContentTypeService contentTypeService,
ILogger<ConvertLocalLinks> logger,
IDataTypeService dataTypeService,
ILanguageService languageService,
IJsonSerializer jsonSerializer,
LocalLinkProcessor localLinkProcessor,
IMediaTypeService mediaTypeService,
ICoreScopeProvider coreScopeProvider,
LocalLinkMigrationTracker linkMigrationTracker)
: base(context)
{
_umbracoContextFactory = umbracoContextFactory;
_contentTypeService = contentTypeService;
_logger = logger;
_dataTypeService = dataTypeService;
_languageService = languageService;
_jsonSerializer = jsonSerializer;
_localLinkProcessor = localLinkProcessor;
_mediaTypeService = mediaTypeService;
_coreScopeProvider = coreScopeProvider;
_linkMigrationTracker = linkMigrationTracker;
}
public ConvertLocalLinks(
IMigrationContext context,
@@ -38,17 +68,19 @@ public class ConvertLocalLinks : MigrationBase
LocalLinkProcessor localLinkProcessor,
IMediaTypeService mediaTypeService,
ICoreScopeProvider coreScopeProvider)
: base(context)
: this(
context,
umbracoContextFactory,
contentTypeService,
logger,
dataTypeService,
languageService,
jsonSerializer,
localLinkProcessor,
mediaTypeService,
coreScopeProvider,
StaticServiceProvider.Instance.GetRequiredService<LocalLinkMigrationTracker>())
{
_umbracoContextFactory = umbracoContextFactory;
_contentTypeService = contentTypeService;
_logger = logger;
_dataTypeService = dataTypeService;
_languageService = languageService;
_jsonSerializer = jsonSerializer;
_localLinkProcessor = localLinkProcessor;
_mediaTypeService = mediaTypeService;
_coreScopeProvider = coreScopeProvider;
}
protected override void Migrate()
@@ -97,6 +129,9 @@ public class ConvertLocalLinks : MigrationBase
propertyEditorAlias);
}
}
_linkMigrationTracker.MarkFixedMigrationRan();
RebuildCache = true;
}
private bool ProcessPropertyTypes(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> languagesById)

View File

@@ -13,5 +13,7 @@ public class ConvertLocalLinkComposer : IComposer
builder.Services.AddSingleton<ITypedLocalLinkProcessor, LocalLinkBlockGridProcessor>();
builder.Services.AddSingleton<ITypedLocalLinkProcessor, LocalLinkRteProcessor>();
builder.Services.AddSingleton<LocalLinkProcessor>();
builder.Services.AddSingleton<LocalLinkProcessorForFaultyLinks>();
builder.Services.AddSingleton<LocalLinkMigrationTracker>();
}
}

View File

@@ -0,0 +1,8 @@
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
public class LocalLinkMigrationTracker
{
public bool HasFixedMigrationRun { get; private set; }
public void MarkFixedMigrationRan() => HasFixedMigrationRun = true;
}

View File

@@ -43,8 +43,8 @@ public class LocalLinkProcessor
string newTagHref;
if (tag.Udi is not null)
{
newTagHref = $" type=\"{tag.Udi.EntityType}\" "
+ tag.TagHref.Replace(tag.Udi.ToString(), tag.Udi.Guid.ToString());
newTagHref = tag.TagHref.Replace(tag.Udi.ToString(), tag.Udi.Guid.ToString())
+ $"\" type=\"{tag.Udi.EntityType}";
}
else if (tag.IntId is not null)
{
@@ -55,8 +55,8 @@ public class LocalLinkProcessor
continue;
}
newTagHref = $" type=\"{conversionResult.Value.EntityType}\" "
+ tag.TagHref.Replace(tag.IntId.Value.ToString(), conversionResult.Value.Key.ToString());
newTagHref = tag.TagHref.Replace(tag.IntId.Value.ToString(), conversionResult.Value.Key.ToString())
+ $"\" type=\"{conversionResult.Value.EntityType}";
}
else
{

View File

@@ -0,0 +1,73 @@
using System.Text.RegularExpressions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0.LocalLinks;
[Obsolete("Will be removed in V18")]
public class LocalLinkProcessorForFaultyLinks
{
private readonly IIdKeyMap _idKeyMap;
private readonly IEnumerable<ITypedLocalLinkProcessor> _localLinkProcessors;
private const string LocalLinkLocation = "__LOCALLINKLOCATION__";
private const string TypeAttributeLocation = "__TYPEATTRIBUTELOCATION__";
internal static readonly Regex FaultyHrefPattern = new(
@"<a (?<faultyHref>href=['""] ?(?<typeAttribute> type=*?['""][^'""]*?['""] )?(?<localLink>\/{localLink:[a-fA-F0-9-]+}['""])).*?>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
public LocalLinkProcessorForFaultyLinks(
IIdKeyMap idKeyMap,
IEnumerable<ITypedLocalLinkProcessor> localLinkProcessors)
{
_idKeyMap = idKeyMap;
_localLinkProcessors = localLinkProcessors;
}
public IEnumerable<string> GetSupportedPropertyEditorAliases() =>
_localLinkProcessors.SelectMany(p => p.PropertyEditorAliases);
public bool ProcessToEditorValue(object? editorValue)
{
ITypedLocalLinkProcessor? processor =
_localLinkProcessors.FirstOrDefault(p => p.PropertyEditorValueType == editorValue?.GetType());
return processor is not null && processor.Process.Invoke(editorValue, ProcessToEditorValue, ProcessStringValue);
}
public string ProcessStringValue(string input)
{
MatchCollection faultyTags = FaultyHrefPattern.Matches(input);
foreach (Match fullTag in faultyTags)
{
var newValue =
fullTag.Value.Replace(fullTag.Groups["typeAttribute"].Value, LocalLinkLocation)
.Replace(fullTag.Groups["localLink"].Value, TypeAttributeLocation)
.Replace(LocalLinkLocation, fullTag.Groups["localLink"].Value)
.Replace(TypeAttributeLocation, fullTag.Groups["typeAttribute"].Value);
input = input.Replace(fullTag.Value, newValue);
}
return input;
}
private (Guid Key, string EntityType)? CreateIntBasedKeyType(int id)
{
// very old data, best effort replacement
Attempt<Guid> documentAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document);
if (documentAttempt.Success)
{
return (Key: documentAttempt.Result, EntityType: UmbracoObjectTypes.Document.ToString());
}
Attempt<Guid> mediaAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media);
if (mediaAttempt.Success)
{
return (Key: mediaAttempt.Result, EntityType: UmbracoObjectTypes.Media.ToString());
}
return null;
}
}

View File

@@ -1,3 +1,4 @@
using System.Text.RegularExpressions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.Blocks;
@@ -28,6 +29,16 @@ public class LocalLinkRteProcessor : ITypedLocalLinkProcessor
bool hasChanged = false;
var newMarkup = processStringValue.Invoke(richTextValue.Markup);
// fix recursive hickup in ConvertRichTextEditorProperties
newMarkup = RteBlockHelper.BlockRegex().Replace(
newMarkup,
match => UdiParser.TryParse(match.Groups["udi"].Value, out GuidUdi? guidUdi)
? match.Value
.Replace(match.Groups["attribute"].Value, "data-content-key")
.Replace(match.Groups["udi"].Value, guidUdi.Guid.ToString("D"))
: string.Empty);
if (newMarkup.Equals(richTextValue.Markup) == false)
{
hasChanged = true;
@@ -53,3 +64,10 @@ public class LocalLinkRteProcessor : ITypedLocalLinkProcessor
return hasChanged;
}
}
[Obsolete("Will be removed in V18")]
public static partial class RteBlockHelper
{
[GeneratedRegex("<umb-rte-block.*(?<attribute>data-content-udi)=\"(?<udi>.[^\"]*)\".*<\\/umb-rte-block")]
public static partial Regex BlockRegex();
}

View File

@@ -0,0 +1,287 @@
using System.Collections.Concurrent;
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.Scoping;
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_1_0;
public class FixConvertLocalLinks : MigrationBase
{
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly IContentTypeService _contentTypeService;
private readonly ILogger<FixConvertLocalLinks> _logger;
private readonly IDataTypeService _dataTypeService;
private readonly ILanguageService _languageService;
private readonly IJsonSerializer _jsonSerializer;
private readonly LocalLinkProcessorForFaultyLinks _localLinkProcessor;
private readonly IMediaTypeService _mediaTypeService;
private readonly ICoreScopeProvider _coreScopeProvider;
private readonly LocalLinkMigrationTracker _localLinkMigrationTracker;
public FixConvertLocalLinks(
IMigrationContext context,
IUmbracoContextFactory umbracoContextFactory,
IContentTypeService contentTypeService,
ILogger<FixConvertLocalLinks> logger,
IDataTypeService dataTypeService,
ILanguageService languageService,
IJsonSerializer jsonSerializer,
LocalLinkProcessorForFaultyLinks localLinkProcessor,
IMediaTypeService mediaTypeService,
ICoreScopeProvider coreScopeProvider,
LocalLinkMigrationTracker localLinkMigrationTracker)
: base(context)
{
_umbracoContextFactory = umbracoContextFactory;
_contentTypeService = contentTypeService;
_logger = logger;
_dataTypeService = dataTypeService;
_languageService = languageService;
_jsonSerializer = jsonSerializer;
_localLinkProcessor = localLinkProcessor;
_mediaTypeService = mediaTypeService;
_coreScopeProvider = coreScopeProvider;
_localLinkMigrationTracker = localLinkMigrationTracker;
}
protected override void Migrate()
{
// the original migration was fixed, we only run this if
// this migration hits
// and the fixed original migration has not run, so it must have run in the past
if (_localLinkMigrationTracker.HasFixedMigrationRun)
{
return;
}
IEnumerable<string> propertyEditorAliases = _localLinkProcessor.GetSupportedPropertyEditorAliases();
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
var languagesById = _languageService.GetAllAsync().GetAwaiter().GetResult()
.ToDictionary(language => language.Id);
IEnumerable<IContentType> allContentTypes = _contentTypeService.GetAll();
IEnumerable<IPropertyType> contentPropertyTypes = allContentTypes
.SelectMany(ct => ct.PropertyTypes);
IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray();
IEnumerable<IPropertyType> 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);
}
}
RebuildCache = true;
}
private bool ProcessPropertyTypes(IPropertyType[] propertyTypes, IDictionary<int, ILanguage> 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<ISqlContext> sql = Sql()
.Select<PropertyDataDto>()
.From<PropertyDataDto>()
.InnerJoin<ContentVersionDto>()
.On<PropertyDataDto, ContentVersionDto>((propertyData, contentVersion) =>
propertyData.VersionId == contentVersion.Id)
.LeftJoin<DocumentVersionDto>()
.On<ContentVersionDto, DocumentVersionDto>((contentVersion, documentVersion) =>
contentVersion.Id == documentVersion.Id)
.Where<PropertyDataDto, ContentVersionDto, DocumentVersionDto>(
(propertyData, contentVersion, documentVersion) =>
(contentVersion.Current == true || documentVersion.Published == true)
&& propertyData.PropertyTypeId == propertyType.Id);
List<PropertyDataDto> propertyDataDtos = Database.Fetch<PropertyDataDto>(sql);
if (propertyDataDtos.Count < 1)
{
continue;
}
var updateBatch = propertyDataDtos.Select(propertyDataDto =>
UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList();
var updatesToSkip = new ConcurrentBag<UpdateBatch<PropertyDataDto>>();
var progress = 0;
void HandleUpdateBatch(UpdateBatch<PropertyDataDto> update)
{
using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
progress++;
if (progress % 100 == 0)
{
_logger.LogInformation(" - finíshed {progress} of {total} properties", progress,
updateBatch.Count);
}
PropertyDataDto propertyDataDto = update.Poco;
if (ProcessPropertyDataDto(propertyDataDto, propertyType, languagesById, valueEditor) == false)
{
updatesToSkip.Add(update);
}
}
if (DatabaseType == DatabaseType.SQLite)
{
// SQLite locks up if we run the migration in parallel, so... let's not.
foreach (UpdateBatch<PropertyDataDto> update in updateBatch)
{
HandleUpdateBatch(update);
}
}
else
{
Parallel.ForEachAsync(updateBatch, async (update, token) =>
{
//Foreach here, but we need to suppress the flow before each task, but not the actuall await of the task
Task task;
using (ExecutionContext.SuppressFlow())
{
task = Task.Run(
() =>
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
scope.Complete();
HandleUpdateBatch(update);
},
token);
}
await task;
}).GetAwaiter().GetResult();
}
updateBatch.RemoveAll(updatesToSkip.Contains);
if (updateBatch.Any() is false)
{
_logger.LogDebug(" - no properties to convert, continuing");
continue;
}
_logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count);
var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
if (result != updateBatch.Count)
{
throw new InvalidOperationException(
$"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries.");
}
_logger.LogDebug(
"Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.",
propertyType.Name,
propertyType.Id,
propertyType.Alias,
propertyType.PropertyEditorAlias,
result);
}
return true;
}
private bool ProcessPropertyDataDto(PropertyDataDto propertyDataDto, IPropertyType propertyType,
IDictionary<int, ILanguage> 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);
if (_localLinkProcessor.ProcessToEditorValue(toEditorValue) == false)
{
_logger.LogDebug(
" - skipping as no processor modified the data for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})",
propertyDataDto.Id,
propertyType.Name,
propertyType.Id,
propertyType.Alias);
return false;
}
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.LogWarning(
" - 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;
}
}

View File

@@ -71,13 +71,17 @@ public class MigrationPlanTests
Mock.Of<IServerMessenger>(),
new CacheRefresherCollection(() => Enumerable.Empty<ICacheRefresher>()));
var isolatedCaches = new IsolatedCaches(type => NoAppCache.Instance);
var appCaches = new AppCaches(Mock.Of<IAppPolicyCache>(), Mock.Of<IRequestCache>(), isolatedCaches);
var executor = new MigrationPlanExecutor(
scopeProvider,
scopeProvider,
loggerFactory,
migrationBuilder,
databaseFactory,
Mock.Of<IDatabaseCacheRebuilder>(), distributedCache, Mock.Of<IKeyValueService>(), Mock.Of<IServiceScopeFactory>());
Mock.Of<IDatabaseCacheRebuilder>(), distributedCache, Mock.Of<IKeyValueService>(), Mock.Of<IServiceScopeFactory>(), appCaches);
var plan = new MigrationPlan("default")
.From(string.Empty)