diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 310477fa4c..af8cdea3a3 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -292,7 +292,7 @@ stages: Linux: vmImage: 'ubuntu-latest' testDb: SqlServer - connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);' + connectionString: 'Server=localhost,1433;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=true' pool: vmImage: $(vmImage) variables: diff --git a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj index fd5e7d4b83..5a2dfa69ef 100644 --- a/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj +++ b/src/Umbraco.Cms.Api.Common/Umbraco.Cms.Api.Common.csproj @@ -10,6 +10,8 @@ + + diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index af566ab67a..3e6e57983f 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs b/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs new file mode 100644 index 0000000000..2af7570183 --- /dev/null +++ b/src/Umbraco.Core/Models/DeliveryApi/RichTextModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models.DeliveryApi; + +public class RichTextModel +{ + public required string Markup { get; set; } +} diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index f863c96151..d0ab92223b 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -204,6 +204,13 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); var entityType = GetEntityType(propertyType); + + if (entityType == "content") + { + // TODO Why do MNTP config saves "content" and not "document"? + entityType = Constants.UdiEntityType.Document; + } + GuidUdi[] entityTypeUdis = udis.Where(udi => udi.EntityType == entityType).OfType().ToArray(); return entityType switch { diff --git a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs index 065a8e049f..09d726fd6a 100644 --- a/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/ExecutedMigrationPlan.cs @@ -24,6 +24,7 @@ public class ExecutedMigrationPlan FinalState = finalState ?? throw new ArgumentNullException(nameof(finalState)); Successful = successful; CompletedTransitions = completedTransitions; + ExecutedMigrationContexts = Array.Empty(); } public ExecutedMigrationPlan() @@ -59,4 +60,7 @@ public class ExecutedMigrationPlan /// A collection of all the succeeded transition. /// public required IReadOnlyList CompletedTransitions { get; init; } + + [Obsolete("This will be removed in the V13, and replaced with UmbracoPlanExecutedNotification")] + internal IReadOnlyList ExecutedMigrationContexts { get; init; } = Array.Empty(); } diff --git a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs index 0001b3381d..5e0766755a 100644 --- a/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/IMigrationContext.cs @@ -37,4 +37,11 @@ public interface IMigrationContext /// Gets or sets a value indicating whether an expression is being built. /// bool BuildingExpression { get; set; } + + /// + /// Adds a post-migration. + /// + [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] + void AddPostMigration() + where TMigration : MigrationBase; } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs index 4b1900c27c..c07adbec90 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationContext.cs @@ -8,6 +8,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations; /// internal class MigrationContext : IMigrationContext { + private readonly List _postMigrations = new(); + /// /// Initializes a new instance of the class. /// @@ -18,6 +20,10 @@ internal class MigrationContext : IMigrationContext Logger = logger ?? throw new ArgumentNullException(nameof(logger)); } + // this is only internally exposed + [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase")] + internal IReadOnlyList PostMigrations => _postMigrations; + /// public ILogger Logger { get; } @@ -34,4 +40,13 @@ internal class MigrationContext : IMigrationContext /// public bool BuildingExpression { get; set; } + + + /// + [Obsolete("This will be removed in the V13, and replaced with a RebuildCache flag on the MigrationBase, and a UmbracoPlanExecutedNotification.")] + public void AddPostMigration() + where TMigration : MigrationBase => + + // just adding - will be de-duplicated when executing + _postMigrations.Add(typeof(TMigration)); } diff --git a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs index c271088626..faea87bd68 100644 --- a/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs +++ b/src/Umbraco.Infrastructure/Migrations/MigrationPlanExecutor.cs @@ -99,6 +99,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor ExecutedMigrationPlan result = RunMigrationPlan(plan, fromState); + HandlePostMigrations(result); + // If any completed migration requires us to rebuild cache we'll do that. if (_rebuildCache) { @@ -108,6 +110,33 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor return result; } + [Obsolete] + private void HandlePostMigrations(ExecutedMigrationPlan result) + { + // prepare and de-duplicate post-migrations, only keeping the 1st occurence + var executedTypes = new HashSet(); + + foreach (var executedMigrationContext in result.ExecutedMigrationContexts) + { + if (executedMigrationContext is MigrationContext migrationContext) + { + foreach (Type migrationContextPostMigration in migrationContext.PostMigrations) + { + if (executedTypes.Contains(migrationContextPostMigration)) + { + continue; + } + + _logger.LogInformation($"PostMigration: {migrationContextPostMigration.FullName}."); + MigrationBase postMigration = _migrationBuilder.Build(migrationContextPostMigration, executedMigrationContext); + postMigration.Run(); + + executedTypes.Add(migrationContextPostMigration); + } + } + } + } + private ExecutedMigrationPlan RunMigrationPlan(MigrationPlan plan, string fromState) { _logger.LogInformation("Starting '{MigrationName}'...", plan.Name); @@ -122,6 +151,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor List completedTransitions = new(); + var executedMigrationContexts = new List(); while (transition is not null) { _logger.LogInformation("Execute {MigrationType}", transition.MigrationType.Name); @@ -130,11 +160,11 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor { if (transition.MigrationType.IsAssignableTo(typeof(UnscopedMigrationBase))) { - RunUnscopedMigration(transition.MigrationType, plan); + executedMigrationContexts.Add(RunUnscopedMigration(transition.MigrationType, plan)); } else { - RunScopedMigration(transition.MigrationType, plan); + executedMigrationContexts.Add(RunScopedMigration(transition.MigrationType, plan)); } } catch (Exception exception) @@ -149,6 +179,7 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor FinalState = transition.SourceState, CompletedTransitions = completedTransitions, Plan = plan, + ExecutedMigrationContexts = executedMigrationContexts }; } @@ -197,18 +228,21 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor FinalState = finalState, CompletedTransitions = completedTransitions, Plan = plan, + ExecutedMigrationContexts = executedMigrationContexts }; } - private void RunUnscopedMigration(Type migrationType, MigrationPlan plan) + private MigrationContext RunUnscopedMigration(Type migrationType, MigrationPlan plan) { using IUmbracoDatabase database = _databaseFactory.CreateDatabase(); var context = new MigrationContext(plan, database, _loggerFactory.CreateLogger()); RunMigration(migrationType, context); + + return context; } - private void RunScopedMigration(Type migrationType, MigrationPlan plan) + private MigrationContext RunScopedMigration(Type migrationType, MigrationPlan plan) { // We want to suppress scope (service, etc...) notifications during a migration plan // execution. This is because if a package that doesn't have their migration plan @@ -225,6 +259,8 @@ public class MigrationPlanExecutor : IMigrationPlanExecutor RunMigration(migrationType, context); scope.Complete(); + + return context; } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs index ce105bf6b8..a3ec96ad7e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs @@ -22,6 +22,11 @@ public class ResetCache : MigrationBase private void DeleteAllFilesInFolder(string path) { + if (Directory.Exists(path) == false) + { + return; + } + var directoryInfo = new DirectoryInfo(path); foreach (FileInfo file in directoryInfo.GetFiles()) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs index 68d15aa99f..9d8010ab5f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteMacroRenderingValueConverter.cs @@ -82,13 +82,16 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) => _deliveryApiSettings.RichTextOutputAsJson ? typeof(IRichTextElement) - : typeof(string); + : typeof(RichTextModel); public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { if (_deliveryApiSettings.RichTextOutputAsJson is false) { - return Convert(inter, preview) ?? string.Empty; + return new RichTextModel + { + Markup = Convert(inter, preview, false) ?? string.Empty + }; } var sourceString = inter?.ToString(); @@ -126,7 +129,7 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel } } - private string? Convert(object? source, bool preview) + private string? Convert(object? source, bool preview, bool handleMacros = true) { if (source == null) { @@ -141,7 +144,10 @@ public class RteMacroRenderingValueConverter : SimpleTinyMceValueConverter, IDel sourceString = _imageSourceParser.EnsureImageSources(sourceString); // ensure string is parsed for macros and macros are executed correctly - sourceString = RenderRteMacros(sourceString, preview); + if (handleMacros) + { + sourceString = RenderRteMacros(sourceString, preview); + } // find and remove the rel attributes used in the Umbraco UI from img tags var doc = new HtmlDocument(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index 80e175bf81..276df9a223 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -93,7 +93,9 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest } [Test] - public void MultiNodeTreePickerValueConverter_RendersContentProperties() + [TestCase(Constants.UdiEntityType.Document)] + [TestCase("content")] + public void MultiNodeTreePickerValueConverter_RendersContentProperties(string entityType) { var content = new Mock(); @@ -112,7 +114,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest .Setup(pcc => pcc.GetById(key)) .Returns(content.Object); - var publishedDataType = MultiNodePickerPublishedDataType(false, Constants.UdiEntityType.Document); + var publishedDataType = MultiNodePickerPublishedDataType(false, entityType); var publishedPropertyType = new Mock(); publishedPropertyType.SetupGet(p => p.DataType).Returns(publishedDataType); diff --git a/version.json b/version.json index 1af6ab23c5..e22da7303c 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.0.0-rc", + "version": "12.0.0-rc1", "assemblyVersion": { "precision": "build" },