From c9feaa150ee915d084b81e06c320448c6d0f0eb1 Mon Sep 17 00:00:00 2001 From: Chris Fitz-Avon Date: Thu, 14 Dec 2023 12:49:46 +0000 Subject: [PATCH 01/19] Remove content section access policy from GetAllLanguages endpoint. #15435 (#15450) --- src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 4e5ed094ac..3e54bb705a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -43,7 +43,6 @@ public class LanguageController : UmbracoAuthorizedJsonController /// /// [HttpGet] - [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public IEnumerable? GetAllLanguages() { IEnumerable allLanguages = _localizationService.GetAllLanguages(); From fa9b0e4718638e3c432c7f04d47cbdef2b364ea6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 18 Dec 2023 08:23:34 +0100 Subject: [PATCH 02/19] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2fd485a967..e22162ce3c 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": "10.8.2", + "version": "10.8.3", "assemblyVersion": { "precision": "build" }, From d5156aced2de0385d7db1768e442c2e1b23240e5 Mon Sep 17 00:00:00 2001 From: Chris Fitz-Avon Date: Thu, 14 Dec 2023 12:49:46 +0000 Subject: [PATCH 03/19] Remove content section access policy from GetAllLanguages endpoint. #15435 (#15450) (cherry picked from commit cedfdcc9b20cbb9ce588d5b2bd49eb3b7553f14d) --- src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 285e1127eb..f29672d53a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -52,7 +52,6 @@ public class LanguageController : UmbracoAuthorizedJsonController /// /// [HttpGet] - [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public IEnumerable? GetAllLanguages() { IEnumerable allLanguages = _localizationService.GetAllLanguages(); From f9ea546c66563b47142c1b4b0c06711749bb9716 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 18 Dec 2023 08:26:38 +0100 Subject: [PATCH 04/19] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 7a67ed81fd..1d168fd91b 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.3.5", + "version": "12.3.6", "assemblyVersion": { "precision": "build" }, From 38f7df2b0c456dfc73481107f1d30c568432b4b1 Mon Sep 17 00:00:00 2001 From: Chris Fitz-Avon Date: Thu, 14 Dec 2023 12:49:46 +0000 Subject: [PATCH 05/19] Remove content section access policy from GetAllLanguages endpoint. #15435 (#15450) (cherry picked from commit cedfdcc9b20cbb9ce588d5b2bd49eb3b7553f14d) --- src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs index 4e5ed094ac..3e54bb705a 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/LanguageController.cs @@ -43,7 +43,6 @@ public class LanguageController : UmbracoAuthorizedJsonController /// /// [HttpGet] - [Authorize(Policy = AuthorizationPolicies.SectionAccessContent)] public IEnumerable? GetAllLanguages() { IEnumerable allLanguages = _localizationService.GetAllLanguages(); From 78850087f8558dbaf5e6dcc4d115aa2976f37fbf Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Tue, 19 Dec 2023 14:18:10 +0100 Subject: [PATCH 06/19] Added a temporary DataCollector for v14 editorAlias migrations (#15463) * Added a temporary DataCollector for v14 editorAlias migrations * PR feedback Do not run the collector on subscribers or non running instances Reduced logger noise * Extra v14 sanity check --------- Co-authored-by: Sven Geusens --- .../DataTypeSplitDataCollector.cs | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs diff --git a/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs b/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs new file mode 100644 index 0000000000..3a133bc253 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/PreMigration/DataTypeSplitDataCollector.cs @@ -0,0 +1,169 @@ +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; + +namespace Umbraco.Cms.Infrastructure.Migrations.PreMigration; + +// we use a composer here so its easier to clean this up when we no longer need it. +// same for additional classes in the same file, nice and self contained +// should only be used to migrate from v13 to v14 +// ⚠️ FIXME: PLEASE DELETE THIS IN V14! ⚠️ +public class DataTypeSplitDataCollectorComposer : IComposer +{ + public void Compose(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + } +} + +public class DataTypeSplitDataCollector : INotificationHandler +{ + private readonly DataEditorCollection _dataEditors; + private readonly IManifestParser _manifestParser; + private readonly IDataTypeService _dataTypeService; + private readonly IKeyValueService _keyValueService; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly IRuntimeState _runtimeState; + private readonly IServerRoleAccessor _serverRoleAccessor; + private readonly IUmbracoVersion _umbracoVersion; + + public DataTypeSplitDataCollector( + DataEditorCollection dataEditors, + IManifestParser manifestParser, + IDataTypeService dataTypeService, + IKeyValueService keyValueService, + IJsonSerializer jsonSerializer, + ILogger logger, + ICoreScopeProvider coreScopeProvider, + IRuntimeState runtimeState, + IServerRoleAccessor serverRoleAccessor, + IUmbracoVersion umbracoVersion) + { + _dataEditors = dataEditors; + _manifestParser = manifestParser; + _dataTypeService = dataTypeService; + _keyValueService = keyValueService; + _jsonSerializer = jsonSerializer; + _logger = logger; + _coreScopeProvider = coreScopeProvider; + _runtimeState = runtimeState; + _serverRoleAccessor = serverRoleAccessor; + _umbracoVersion = umbracoVersion; + } + + public void Handle(UmbracoApplicationStartedNotification notification) + { + // should only be used to collect data in v13 + if (_umbracoVersion.Version.Major is not 13) + { + return; + } + + // only run this if the application is actually running and not in an install/upgrade state + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + // do not run on load balanced subscribers + if (_serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber) + { + return; + } + + var manifestEditorsData = _manifestParser.CombinedManifest.PropertyEditors + .Select(pe => new EditorAliasSplitData(pe.Alias){EditorUiAlias = pe.Alias, EditorAlias = EditorAliasFromValueEditorValueType(pe.GetValueEditor().ValueType)}) + .ToDictionary(data => data.OriginalEditorAlias); + + _logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough manifest files",manifestEditorsData.Count); + + var fromCodeEditorsData = _dataEditors + .Where(de => + de.GetType().Assembly.GetName().FullName + .StartsWith("umbraco.core", StringComparison.InvariantCultureIgnoreCase) is false + && de.GetType().Assembly.GetName().FullName + .StartsWith("umbraco.infrastructure", StringComparison.InvariantCultureIgnoreCase) is false) + .Select(de => new EditorAliasSplitData(de.Alias) { EditorAlias = de.Alias }) + .ToDictionary(data => data.OriginalEditorAlias); + + _logger.LogDebug("Found {count} custom PropertyEditor(s) configured trough code",fromCodeEditorsData.Count); + + var combinedEditorsData = new Dictionary(manifestEditorsData); + foreach (KeyValuePair pair in fromCodeEditorsData) + { + combinedEditorsData.Add(pair.Key,pair.Value); + } + + if (combinedEditorsData.Any() == false) + { + _logger.LogDebug("No custom PropertyEditors found, skipping collection datatype migration data."); + return; + } + + using ICoreScope coreScope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + + IEnumerable dataTypes = _dataTypeService.GetAll(); + + DataTypeEditorAliasMigrationData[] migrationData = dataTypes + .Where(dt => combinedEditorsData.ContainsKey(dt.EditorAlias)) + .Select(dt => new DataTypeEditorAliasMigrationData + { + DataTypeId = dt.Id, + EditorAlias = combinedEditorsData[dt.EditorAlias].EditorAlias, + EditorUiAlias = combinedEditorsData[dt.EditorAlias].EditorUiAlias + }).ToArray(); + + _logger.LogDebug("Collected migration data for {count} DataType(s) that use custom PropertyEditors",migrationData.Length); + + _keyValueService.SetValue("migrateDataEditorSplitCollectionData",_jsonSerializer.Serialize(migrationData)); + } + + private class EditorAliasSplitData + { + public EditorAliasSplitData(string originalEditorAlias) + { + OriginalEditorAlias = originalEditorAlias; + } + + public string OriginalEditorAlias { get; init; } + public string? EditorUiAlias { get; init; } + public string? EditorAlias { get; init; } + } + + private class DataTypeEditorAliasMigrationData + { + public int DataTypeId { get; set; } + public string? EditorUiAlias { get; init; } + public string? EditorAlias { get; init; } + } + + private string EditorAliasFromValueEditorValueType(string valueType) + { + switch (valueType) + { + case ValueTypes.Date: return "Umbraco.Plain.DateTime"; + case ValueTypes.DateTime: return "Umbraco.Plain.DateTime"; + case ValueTypes.Decimal: return "Umbraco.Plain.Decimal"; + case ValueTypes.Integer: return "Umbraco.Plain.Integer"; + case ValueTypes.Bigint: return "Umbraco.Plain.Integer"; + case ValueTypes.Json: return "Umbraco.Plain.Json"; + case ValueTypes.Time: return "Umbraco.Plain.Time"; + case ValueTypes.String: return "Umbraco.Plain.String"; + case ValueTypes.Xml: return "Umbraco.Plain.String"; + default:return string.Empty; + } + } +} From e04a41be45602385fb99f511aec4f5d30a055ad9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 19 Dec 2023 14:18:48 +0100 Subject: [PATCH 07/19] Add policies for content template handling (#15482) --- .../Controllers/ContentController.cs | 5 ++++- .../UmbracoBuilder.BackOfficeAuth.cs | 7 +++++++ .../Filters/ContentSaveValidationAttribute.cs | 14 ++++++++++---- .../Authorization/AuthorizationPolicies.cs | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index f41a5e4c3a..8beec57812 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -766,6 +766,7 @@ public class ContentController : ContentControllerBase /// The content id to copy /// The name of the blueprint /// + [Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)] [HttpPost] public ActionResult CreateBlueprintFromContent( [FromQuery] int contentId, @@ -821,8 +822,9 @@ public class ContentController : ContentControllerBase /// /// Saves content /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] - [ContentSaveValidation] + [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -2012,6 +2014,7 @@ public class ContentController : ContentControllerBase return Ok(); } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [HttpDelete] [HttpPost] public IActionResult DeleteBlueprint(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 97722d8305..7ec5ff4730 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -179,6 +179,13 @@ public static partial class UmbracoBuilderExtensions policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId")); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index f2e61b694a..33017ada88 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute { - public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) => + public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) + : base(typeof(ContentSaveValidationFilter)) + { Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder - + Arguments = new object[] { skipUserAccessValidation }; + } private sealed class ContentSaveValidationFilter : IAsyncActionFilter { @@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly IPropertyValidationService _propertyValidationService; + private readonly bool _skipUserAccessValidation; public ContentSaveValidationFilter( @@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizationService localizationService) + ILocalizationService localizationService, + bool skipUserAccessValidation) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute _authorizationService = authorizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizationService = localizationService; + _skipUserAccessValidation = skipUserAccessValidation; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute return; } - if (!await ValidateUserAccessAsync(model, context)) + if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false) { return; } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 50e399d4f0..a2e7b77d9a 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -22,6 +22,7 @@ public static class AuthorizationPolicies public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId); public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById); From e34b2c09e0942b35fb0ab22c2ab708a61424ef09 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 19 Dec 2023 14:18:48 +0100 Subject: [PATCH 08/19] Add policies for content template handling (#15482) --- .../Controllers/ContentController.cs | 5 ++++- .../UmbracoBuilder.BackOfficeAuth.cs | 7 +++++++ .../Filters/ContentSaveValidationAttribute.cs | 14 ++++++++++---- .../Authorization/AuthorizationPolicies.cs | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index d06062498a..6ba81172c8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -826,6 +826,7 @@ public class ContentController : ContentControllerBase /// The content id to copy /// The name of the blueprint /// + [Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)] [HttpPost] public ActionResult CreateBlueprintFromContent( [FromQuery] int contentId, @@ -881,8 +882,9 @@ public class ContentController : ContentControllerBase /// /// Saves content /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] - [ContentSaveValidation] + [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -2077,6 +2079,7 @@ public class ContentController : ContentControllerBase return Ok(); } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [HttpDelete] [HttpPost] public IActionResult DeleteBlueprint(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index bfd460bda4..0945d3459b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -179,6 +179,13 @@ public static partial class UmbracoBuilderExtensions policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId")); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 100d089451..f7be9d129a 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute { - public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) => + public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) + : base(typeof(ContentSaveValidationFilter)) + { Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder - + Arguments = new object[] { skipUserAccessValidation }; + } private sealed class ContentSaveValidationFilter : IAsyncActionFilter { @@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly IPropertyValidationService _propertyValidationService; + private readonly bool _skipUserAccessValidation; public ContentSaveValidationFilter( @@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizationService localizationService) + ILocalizationService localizationService, + bool skipUserAccessValidation) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute _authorizationService = authorizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizationService = localizationService; + _skipUserAccessValidation = skipUserAccessValidation; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute return; } - if (!await ValidateUserAccessAsync(model, context)) + if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false) { return; } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index cfad020a92..429cc77faf 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -22,6 +22,7 @@ public static class AuthorizationPolicies public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId); public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById); From 11cef81c77ccbade8c967091653d8dcab90c4f85 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 19 Dec 2023 14:21:00 +0100 Subject: [PATCH 09/19] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 26ecc33103..a04a865bbf 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.0.1", + "version": "13.0.2", "assemblyVersion": { "precision": "build" }, From e8a81e30b1680deca682a179705a0795959ea377 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 19 Dec 2023 14:18:48 +0100 Subject: [PATCH 10/19] Add policies for content template handling (#15482) --- .../Controllers/ContentController.cs | 5 ++++- .../UmbracoBuilder.BackOfficeAuth.cs | 7 +++++++ .../Filters/ContentSaveValidationAttribute.cs | 14 ++++++++++---- .../Authorization/AuthorizationPolicies.cs | 1 + 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 83063f156f..1126dceba1 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -826,6 +826,7 @@ public class ContentController : ContentControllerBase /// The content id to copy /// The name of the blueprint /// + [Authorize(Policy = AuthorizationPolicies.ContentPermissionCreateBlueprintFromId)] [HttpPost] public ActionResult CreateBlueprintFromContent( [FromQuery] int contentId, @@ -881,8 +882,9 @@ public class ContentController : ContentControllerBase /// /// Saves content /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] - [ContentSaveValidation] + [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch public async Task?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -2077,6 +2079,7 @@ public class ContentController : ContentControllerBase return Ok(); } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [HttpDelete] [HttpPost] public IActionResult DeleteBlueprint(int id) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 97722d8305..7ec5ff4730 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -179,6 +179,13 @@ public static partial class UmbracoBuilderExtensions policy.Requirements.Add(new ContentPermissionsQueryStringRequirement(ActionDelete.ActionLetter)); }); + options.AddPolicy(AuthorizationPolicies.ContentPermissionCreateBlueprintFromId, policy => + { + policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); + policy.Requirements.Add( + new ContentPermissionsQueryStringRequirement(ActionCreateBlueprintFromContent.ActionLetter, "contentId")); + }); + options.AddPolicy(AuthorizationPolicies.BackOfficeAccess, policy => { policy.AuthenticationSchemes.Add(backOfficeAuthenticationScheme); diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index 100d089451..f7be9d129a 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -20,9 +20,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute { - public ContentSaveValidationAttribute() : base(typeof(ContentSaveValidationFilter)) => + public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) + : base(typeof(ContentSaveValidationFilter)) + { Order = -3000; // More important than ModelStateInvalidFilter.FilterOrder - + Arguments = new object[] { skipUserAccessValidation }; + } private sealed class ContentSaveValidationFilter : IAsyncActionFilter { @@ -32,6 +35,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute private readonly ILocalizationService _localizationService; private readonly ILoggerFactory _loggerFactory; private readonly IPropertyValidationService _propertyValidationService; + private readonly bool _skipUserAccessValidation; public ContentSaveValidationFilter( @@ -40,7 +44,8 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute IPropertyValidationService propertyValidationService, IAuthorizationService authorizationService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizationService localizationService) + ILocalizationService localizationService, + bool skipUserAccessValidation) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); @@ -49,6 +54,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute _authorizationService = authorizationService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizationService = localizationService; + _skipUserAccessValidation = skipUserAccessValidation; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) @@ -88,7 +94,7 @@ internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute return; } - if (!await ValidateUserAccessAsync(model, context)) + if (_skipUserAccessValidation is false && await ValidateUserAccessAsync(model, context) is false) { return; } diff --git a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs index 50e399d4f0..a2e7b77d9a 100644 --- a/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs +++ b/src/Umbraco.Web.Common/Authorization/AuthorizationPolicies.cs @@ -22,6 +22,7 @@ public static class AuthorizationPolicies public const string ContentPermissionProtectById = nameof(ContentPermissionProtectById); public const string ContentPermissionBrowseById = nameof(ContentPermissionBrowseById); public const string ContentPermissionDeleteById = nameof(ContentPermissionDeleteById); + public const string ContentPermissionCreateBlueprintFromId = nameof(ContentPermissionCreateBlueprintFromId); public const string MediaPermissionByResource = nameof(MediaPermissionByResource); public const string MediaPermissionPathById = nameof(MediaPermissionPathById); From 0b811d6648ae357944de6a03a2269de315af9a75 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 20 Dec 2023 13:36:01 +0100 Subject: [PATCH 11/19] Reenabled package validation and fixed breaking change in test --- Directory.Build.props | 2 +- .../UmbracoExamine/IndexInitializer.cs | 59 ++++++++++++++++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 56ab2370c3..1d8f755b34 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,7 +29,7 @@ false - false + true 13.0.0 true true diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs index 21a06c3643..0ca0b2f579 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs @@ -58,21 +58,64 @@ public class IndexInitializer _contentTypeService = contentTypeService; } + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] public IndexInitializer( IShortStringHelper shortStringHelper, PropertyEditorCollection propertyEditors, MediaUrlGeneratorCollection mediaUrlGenerators, IScopeProvider scopeProvider, ILoggerFactory loggerFactory, - IOptions contentSettings, IContentTypeService contentTypeService) + IOptions contentSettings, + ILocalizationService localizationService) : this( - shortStringHelper, - propertyEditors, - mediaUrlGenerators, - scopeProvider, - loggerFactory, - contentSettings, - StaticServiceProvider.Instance.GetRequiredService(), contentTypeService) + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + localizationService, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] + public IndexInitializer( + IShortStringHelper shortStringHelper, + PropertyEditorCollection propertyEditors, + MediaUrlGeneratorCollection mediaUrlGenerators, + IScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IOptions contentSettings, + IContentTypeService contentTypeService) + : this( + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + StaticServiceProvider.Instance.GetRequiredService(), contentTypeService) + { + } + + [Obsolete("Use ctor that is not obsolete. This will be removed in Umbraco 15.")] + public IndexInitializer( + IShortStringHelper shortStringHelper, + PropertyEditorCollection propertyEditors, + MediaUrlGeneratorCollection mediaUrlGenerators, + IScopeProvider scopeProvider, + ILoggerFactory loggerFactory, + IOptions contentSettings) + : this( + shortStringHelper, + propertyEditors, + mediaUrlGenerators, + scopeProvider, + loggerFactory, + contentSettings, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } From 812b414d965cb4b8de32c1a61c594f903fdcf76e Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Thu, 21 Dec 2023 09:43:37 +0100 Subject: [PATCH 12/19] Batched more update calls to avoid Sql paramater count error (#15487) Co-authored-by: Sven Geusens --- .../Implement/ContentTypeRepositoryBase.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 59aa92bb82..e9d6348e45 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1223,8 +1223,11 @@ AND umbracoNode.id <> @id", /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true /// when it should be false /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values - /// stored for each - /// property, culture and current/published version. + /// stored for each property, culture and current/published version. + /// + /// Some of the sql statements in this function have a tendency to take a lot of parameters (nodeIds) + /// as the WhereIn Npoco method translates all the nodeIds being passed in as parameters when using the SqlClient provider. + /// this results in to many parameters (>2100) error => We need to batch the calls /// private void RenormalizeDocumentEditedFlags( IReadOnlyCollection propertyTypeIds, @@ -1380,16 +1383,19 @@ AND umbracoNode.id <> @id", // Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false foreach (IGrouping editValue in toUpdate.GroupBy(x => x.Edited)) { - Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) - .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + // update in batches to account for maximum parameter count + foreach (IEnumerable batchedValues in editValue.InGroupsOf(Constants.Sql.MaxParameterCount)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, batchedValues.Select(x => x.Id))); + } } // Now bulk update the umbracoDocument table - // we need to do this in batches as the WhereIn Npoco method translates to all the nodeIds being passed in as parameters when using the SqlClient provider - // this results in to many parameters (>2100) being passed to the client when there are a lot of documents being normalized foreach (IGrouping> groupByValue in editedDocument.GroupBy(x => x.Value)) { - foreach (IEnumerable> batch in groupByValue.InGroupsOf(2000)) + // update in batches to account for maximum parameter count + foreach (IEnumerable> batch in groupByValue.InGroupsOf(Constants.Sql.MaxParameterCount)) { Database.Execute(Sql().Update(u => u.Set(x => x.Edited, groupByValue.Key)) .WhereIn(x => x.NodeId, batch.Select(x => x.Key))); From f7054797673614a567feba3183e30ab99192f09c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 21 Dec 2023 14:55:43 +0100 Subject: [PATCH 13/19] Ensure artifact dependency keeps correct mode, ordering and checksum values (#15500) --- src/Umbraco.Core/Deploy/ArtifactDependency.cs | 4 +- .../Deploy/ArtifactDependencyCollection.cs | 36 +++++++--- .../Umbraco.Core/Deploy/ArtifactBaseTests.cs | 67 ++++++++++++++++++- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs index 80a77740d0..aeb2cfe4a6 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -34,7 +34,7 @@ public class ArtifactDependency /// /// true if the dependency is included when building a dependency tree and gets deployed in the correct order; otherwise, false. /// - public bool Ordering { get; } + public bool Ordering { get; internal set; } /// /// Gets the dependency mode. @@ -42,7 +42,7 @@ public class ArtifactDependency /// /// The dependency mode. /// - public ArtifactDependencyMode Mode { get; } + public ArtifactDependencyMode Mode { get; internal set; } /// /// Gets or sets the checksum. diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs index 7f2b05eaad..6446af1ad5 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -21,25 +21,39 @@ public class ArtifactDependencyCollection : ICollection /// public void Add(ArtifactDependency item) { - if (item.Mode == ArtifactDependencyMode.Exist && - _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - existingItem.Mode == ArtifactDependencyMode.Match) + if (_dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem)) { - // Don't downgrade dependency mode from Match to Exist - return; - } + // Update existing item + if (existingItem.Mode is ArtifactDependencyMode.Exist) + { + // Allow updating dependency mode from Exist to Match + existingItem.Mode = item.Mode; + } - _dependencies[item.Udi] = item; + if (existingItem.Ordering is false) + { + // Allow updating non-ordering to ordering + existingItem.Ordering = item.Ordering; + } + + if (string.IsNullOrEmpty(item.Checksum) is false) + { + // Allow updating checksum if set + existingItem.Checksum = item.Checksum; + } + } + else + { + // Add new item + _dependencies[item.Udi] = item; + } } /// public void Clear() => _dependencies.Clear(); /// - public bool Contains(ArtifactDependency item) - => _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - // Check whether it has the same or higher dependency mode - (existingItem.Mode == item.Mode || existingItem.Mode == ArtifactDependencyMode.Match); + public bool Contains(ArtifactDependency item) => _dependencies.ContainsKey(item.Udi); /// public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs index f029c42749..82aa79a27f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -49,6 +47,71 @@ public class ArtifactBaseTests string.Join(",", artifact.Dependencies.Select(x => x.Udi.ToString()))); } + [Test] + public void Dependencies_Correctly_Updates_Mode() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep Match + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(false, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Ordering() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep ordering (regardless of mode) + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Checksum() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep checksum + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, "123"), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, string.Empty), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual("123", dependency.Checksum); + } + private class TestArtifact : ArtifactBase { public TestArtifact(GuidUdi udi, IEnumerable dependencies = null) From 1e55ac97cd93199f461124328d36382ad27ab604 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 21 Dec 2023 14:55:43 +0100 Subject: [PATCH 14/19] Ensure artifact dependency keeps correct mode, ordering and checksum values (#15500) (cherry picked from commit f7054797673614a567feba3183e30ab99192f09c) --- src/Umbraco.Core/Deploy/ArtifactDependency.cs | 4 +- .../Deploy/ArtifactDependencyCollection.cs | 36 +++++++--- .../Umbraco.Core/Deploy/ArtifactBaseTests.cs | 67 ++++++++++++++++++- 3 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs index 80a77740d0..aeb2cfe4a6 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -34,7 +34,7 @@ public class ArtifactDependency /// /// true if the dependency is included when building a dependency tree and gets deployed in the correct order; otherwise, false. /// - public bool Ordering { get; } + public bool Ordering { get; internal set; } /// /// Gets the dependency mode. @@ -42,7 +42,7 @@ public class ArtifactDependency /// /// The dependency mode. /// - public ArtifactDependencyMode Mode { get; } + public ArtifactDependencyMode Mode { get; internal set; } /// /// Gets or sets the checksum. diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs index 7f2b05eaad..6446af1ad5 100644 --- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -21,25 +21,39 @@ public class ArtifactDependencyCollection : ICollection /// public void Add(ArtifactDependency item) { - if (item.Mode == ArtifactDependencyMode.Exist && - _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - existingItem.Mode == ArtifactDependencyMode.Match) + if (_dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem)) { - // Don't downgrade dependency mode from Match to Exist - return; - } + // Update existing item + if (existingItem.Mode is ArtifactDependencyMode.Exist) + { + // Allow updating dependency mode from Exist to Match + existingItem.Mode = item.Mode; + } - _dependencies[item.Udi] = item; + if (existingItem.Ordering is false) + { + // Allow updating non-ordering to ordering + existingItem.Ordering = item.Ordering; + } + + if (string.IsNullOrEmpty(item.Checksum) is false) + { + // Allow updating checksum if set + existingItem.Checksum = item.Checksum; + } + } + else + { + // Add new item + _dependencies[item.Udi] = item; + } } /// public void Clear() => _dependencies.Clear(); /// - public bool Contains(ArtifactDependency item) - => _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) && - // Check whether it has the same or higher dependency mode - (existingItem.Mode == item.Mode || existingItem.Mode == ArtifactDependencyMode.Match); + public bool Contains(ArtifactDependency item) => _dependencies.ContainsKey(item.Udi); /// public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs index f029c42749..82aa79a27f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Newtonsoft.Json; using NUnit.Framework; using Umbraco.Cms.Core; @@ -49,6 +47,71 @@ public class ArtifactBaseTests string.Join(",", artifact.Dependencies.Select(x => x.Udi.ToString()))); } + [Test] + public void Dependencies_Correctly_Updates_Mode() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep Match + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(false, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Ordering() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep ordering (regardless of mode) + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Exist), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Match), + new ArtifactDependency(udi, false, ArtifactDependencyMode.Exist), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual(null, dependency.Checksum); + } + + [Test] + public void Dependencies_Correctly_Updates_Checksum() + { + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, Guid.NewGuid()); + + var dependencies = new ArtifactDependencyCollection + { + // Keep checksum + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, "123"), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match, string.Empty), + new ArtifactDependency(udi, true, ArtifactDependencyMode.Match), + }; + + Assert.AreEqual(1, dependencies.Count); + var dependency = dependencies.First(); + Assert.AreEqual(udi, dependency.Udi); + Assert.AreEqual(true, dependency.Ordering); + Assert.AreEqual(ArtifactDependencyMode.Match, dependency.Mode); + Assert.AreEqual("123", dependency.Checksum); + } + private class TestArtifact : ArtifactBase { public TestArtifact(GuidUdi udi, IEnumerable dependencies = null) From a0f3c158df0dc8f7a69ee91a16695ffe1dc0205c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 21 Dec 2023 14:56:26 +0100 Subject: [PATCH 15/19] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index a04a865bbf..5c34c948c0 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.0.2", + "version": "13.0.3", "assemblyVersion": { "precision": "build" }, From b648126d196aeae4a3a2601fc4a0849142c9f2ce Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 22 Dec 2023 12:59:13 +0100 Subject: [PATCH 16/19] Use wildcard as default valid --- templates/UmbracoPackage/.template.config/template.json | 2 +- templates/UmbracoProject/.template.config/template.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/UmbracoPackage/.template.config/template.json b/templates/UmbracoPackage/.template.config/template.json index d5aeaf7a82..e2607d0ee7 100644 --- a/templates/UmbracoPackage/.template.config/template.json +++ b/templates/UmbracoPackage/.template.config/template.json @@ -41,7 +41,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "10.0.0-rc1", + "defaultValue": "*", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "Namespace": { diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index 6476fb1568..7e99eed512 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -51,7 +51,7 @@ "description": "The version of Umbraco.Cms to add as PackageReference.", "type": "parameter", "datatype": "string", - "defaultValue": "10.0.0-rc1", + "defaultValue": "*", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, "UseHttpsRedirect": { From 64f2447c0e0af3e25eb8e9ca815e6838464a743e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 22 Dec 2023 13:15:13 +0100 Subject: [PATCH 17/19] Added script to update default version --- templates/Umbraco.Templates.csproj | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 6a0d70646f..4543023507 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -43,4 +43,25 @@ UmbracoProject\wwwroot + + + + + + + + <_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" /> + <_TemplateJsonFiles> + $(IntermediateOutputPath)%(RelativeDir)%(Filename)%(Extension) + + + + + + <_PackageFiles Remove="@(_TemplateJsonFiles)" /> + <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> + %(RelativeDir) + + + From a2ad95d965af9533fa21c7ec55fe52705a9a20f3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 22 Dec 2023 13:49:57 +0100 Subject: [PATCH 18/19] Change content --- templates/Umbraco.Templates.csproj | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 4543023507..a19e4ed6f2 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -48,7 +48,10 @@ - + + + + <_TemplateJsonFiles Include="**\.template.config\template.json" Exclude="bin\**;obj\**" /> <_TemplateJsonFiles> @@ -58,10 +61,10 @@ - <_PackageFiles Remove="@(_TemplateJsonFiles)" /> <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> - %(RelativeDir) + %(_TemplateJsonFiles.RelativeDir) + From b0c5101b2b1f294370ddd261dda10561140dd20f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 22 Dec 2023 14:10:00 +0100 Subject: [PATCH 19/19] fix build --- templates/Umbraco.Templates.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index afc47684f7..bf4e16c133 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -50,7 +50,6 @@ - @@ -64,13 +63,9 @@ - <_PackageFiles Remove="@(_TemplateJsonFiles)" /> - <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> - %(RelativeDir) <_PackageFiles Include="%(_TemplateJsonFiles.DestinationFile)"> %(_TemplateJsonFiles.RelativeDir) -