diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ConfigurationDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ConfigurationDocumentController.cs index a900477296..9c8d83d8e2 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ConfigurationDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ConfigurationDocumentController.cs @@ -1,43 +1,26 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Document; -using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] public class ConfigurationDocumentController : DocumentControllerBase { - private readonly GlobalSettings _globalSettings; - private readonly ContentSettings _contentSettings; - private readonly SegmentSettings _segmentSettings; + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; public ConfigurationDocumentController( - IOptionsSnapshot globalSettings, - IOptionsSnapshot contentSettings, - IOptionsSnapshot segmentSettings) - { - _contentSettings = contentSettings.Value; - _globalSettings = globalSettings.Value; - _segmentSettings = segmentSettings.Value; - } + IConfigurationPresentationFactory configurationPresentationFactory) => + _configurationPresentationFactory = configurationPresentationFactory; [HttpGet("configuration")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(DocumentConfigurationResponseModel), StatusCodes.Status200OK)] public Task Configuration() { - var responseModel = new DocumentConfigurationResponseModel - { - DisableDeleteWhenReferenced = _contentSettings.DisableDeleteWhenReferenced, - DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced, - SanitizeTinyMce = _globalSettings.SanitizeTinyMce, - AllowEditInvariantFromNonDefault = _contentSettings.AllowEditInvariantFromNonDefault, - AllowNonExistingSegmentsCreation = _segmentSettings.AllowCreation, - }; - + DocumentConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateDocumentConfigurationResponseModel(); return Task.FromResult(Ok(responseModel)); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/ConfigurationMediaController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/ConfigurationMediaController.cs index a13de61f0f..0c7ef3f889 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/ConfigurationMediaController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/ConfigurationMediaController.cs @@ -1,35 +1,26 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.Media; -using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Api.Management.Controllers.Media; [ApiVersion("1.0")] public class ConfigurationMediaController : MediaControllerBase { - private readonly GlobalSettings _globalSettings; - private readonly ContentSettings _contentSettings; + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; - public ConfigurationMediaController(IOptionsSnapshot globalSettings, IOptionsSnapshot contentSettings) - { - _contentSettings = contentSettings.Value; - _globalSettings = globalSettings.Value; - } + + public ConfigurationMediaController(IConfigurationPresentationFactory configurationPresentationFactory) + => _configurationPresentationFactory = configurationPresentationFactory; [HttpGet("configuration")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(MediaConfigurationResponseModel), StatusCodes.Status200OK)] public Task Configuration() { - var responseModel = new MediaConfigurationResponseModel - { - DisableDeleteWhenReferenced = _contentSettings.DisableDeleteWhenReferenced, - DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced, - SanitizeTinyMce = _globalSettings.SanitizeTinyMce, - }; + MediaConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMediaConfigurationResponseModel(); return Task.FromResult(Ok(responseModel)); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Member/ConfigurationMemberController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Member/ConfigurationMemberController.cs new file mode 100644 index 0000000000..2c3e7e0ef3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Member/ConfigurationMemberController.cs @@ -0,0 +1,26 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Member; + +namespace Umbraco.Cms.Api.Management.Controllers.Member; + +[ApiVersion("1.0")] +public class ConfigurationMemberController : MemberControllerBase +{ + private readonly IConfigurationPresentationFactory _configurationPresentationFactory; + + public ConfigurationMemberController( + IConfigurationPresentationFactory configurationPresentationFactory) => + _configurationPresentationFactory = configurationPresentationFactory; + + [HttpGet("configuration")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(MemberConfigurationResponseModel), StatusCodes.Status200OK)] + public Task Configuration() + { + MemberConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMemberConfigurationResponseModel(); + return Task.FromResult(Ok(responseModel)); + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..5ff48901f0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Api.Management.DependencyInjection; + +public static class ConfigurationBuilderExtensions +{ + internal static IUmbracoBuilder AddConfigurationFactories(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + + return builder; + } +} diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs index b1ca97a325..8ef9ad3982 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilderExtensions.cs @@ -32,6 +32,7 @@ public static partial class UmbracoBuilderExtensions .AddSearchManagement() .AddTrees() .AddAuditLogs() + .AddConfigurationFactories() .AddDocuments() .AddDocumentTypes() .AddMedia() diff --git a/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs new file mode 100644 index 0000000000..a250fd5647 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/ConfigurationPresentationFactory.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Media; +using Umbraco.Cms.Api.Management.ViewModels.Member; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Factories; + +public class ConfigurationPresentationFactory : IConfigurationPresentationFactory +{ + private readonly IReservedFieldNamesService _reservedFieldNamesService; + private readonly GlobalSettings _globalSettings; + private readonly ContentSettings _contentSettings; + private readonly SegmentSettings _segmentSettings; + + public ConfigurationPresentationFactory( + IReservedFieldNamesService reservedFieldNamesService, + IOptions globalSettings, + IOptions contentSettings, + IOptions segmentSettings) + { + _reservedFieldNamesService = reservedFieldNamesService; + _globalSettings = globalSettings.Value; + _contentSettings = contentSettings.Value; + _segmentSettings = segmentSettings.Value; + } + + public DocumentConfigurationResponseModel CreateDocumentConfigurationResponseModel() => + new() + { + DisableDeleteWhenReferenced = _contentSettings.DisableDeleteWhenReferenced, + DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced, + SanitizeTinyMce = _globalSettings.SanitizeTinyMce, + AllowEditInvariantFromNonDefault = _contentSettings.AllowEditInvariantFromNonDefault, + AllowNonExistingSegmentsCreation = _segmentSettings.AllowCreation, + ReservedFieldNames = _reservedFieldNamesService.GetDocumentReservedFieldNames(), + }; + + public MemberConfigurationResponseModel CreateMemberConfigurationResponseModel() => + new() + { + ReservedFieldNames = _reservedFieldNamesService.GetMemberReservedFieldNames(), + }; + + public MediaConfigurationResponseModel CreateMediaConfigurationResponseModel() => + new() + { + DisableDeleteWhenReferenced = _contentSettings.DisableDeleteWhenReferenced, + DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced, + SanitizeTinyMce = _globalSettings.SanitizeTinyMce, + ReservedFieldNames = _reservedFieldNamesService.GetMediaReservedFieldNames(), + }; +} diff --git a/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs new file mode 100644 index 0000000000..920a7fd7b8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Factories/IConfigurationPresentationFactory.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Api.Management.ViewModels.Document; +using Umbraco.Cms.Api.Management.ViewModels.Media; +using Umbraco.Cms.Api.Management.ViewModels.Member; + +namespace Umbraco.Cms.Api.Management.Factories; + +public interface IConfigurationPresentationFactory +{ + DocumentConfigurationResponseModel CreateDocumentConfigurationResponseModel(); + + MemberConfigurationResponseModel CreateMemberConfigurationResponseModel(); + + MediaConfigurationResponseModel CreateMediaConfigurationResponseModel(); +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index a44838a340..33b4e2207f 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -17429,6 +17429,56 @@ ] } }, + "/umbraco/management/api/v1/member/configuration": { + "get": { + "tags": [ + "Member" + ], + "operationId": "GetMemberConfiguration", + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberConfigurationResponseModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberConfigurationResponseModel" + } + ] + } + }, + "text/plain": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/MemberConfigurationResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/member/validate": { "post": { "tags": [ @@ -34442,6 +34492,7 @@ "allowNonExistingSegmentsCreation", "disableDeleteWhenReferenced", "disableUnpublishWhenReferenced", + "reservedFieldNames", "sanitizeTinyMce" ], "type": "object", @@ -34460,6 +34511,13 @@ }, "allowNonExistingSegmentsCreation": { "type": "boolean" + }, + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -36156,6 +36214,7 @@ "required": [ "disableDeleteWhenReferenced", "disableUnpublishWhenReferenced", + "reservedFieldNames", "sanitizeTinyMce" ], "type": "object", @@ -36168,6 +36227,13 @@ }, "sanitizeTinyMce": { "type": "boolean" + }, + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -36506,6 +36572,22 @@ ], "additionalProperties": false }, + "MemberConfigurationResponseModel": { + "required": [ + "reservedFieldNames" + ], + "type": "object", + "properties": { + "reservedFieldNames": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "MemberGroupItemResponseModel": { "type": "object", "allOf": [ diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs index 626a294eb3..029f24d48e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs @@ -11,4 +11,6 @@ public class DocumentConfigurationResponseModel public required bool AllowEditInvariantFromNonDefault { get; set; } public required bool AllowNonExistingSegmentsCreation { get; set; } + + public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs index 53864a0884..cad09a0bdd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaConfigurationResponseModel.cs @@ -7,4 +7,6 @@ public class MediaConfigurationResponseModel public required bool DisableUnpublishWhenReferenced { get; set; } public required bool SanitizeTinyMce { get; set; } + + public required ISet ReservedFieldNames { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs new file mode 100644 index 0000000000..5faeed7fd8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberConfigurationResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +public class MemberConfigurationResponseModel +{ + public required ISet ReservedFieldNames { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentPropertySettings.cs b/src/Umbraco.Core/Models/ContentPropertySettings.cs new file mode 100644 index 0000000000..46b130bc9e --- /dev/null +++ b/src/Umbraco.Core/Models/ContentPropertySettings.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Models; + +public class ContentPropertySettings +{ + private readonly HashSet _reservedFieldNames = new(); + + /// + /// Gets a set of standard names for fields that cannot be used for custom properties. + /// + public ISet ReservedFieldNames => _reservedFieldNames; + + public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name); +} diff --git a/src/Umbraco.Core/Models/MediaPropertySettings.cs b/src/Umbraco.Core/Models/MediaPropertySettings.cs new file mode 100644 index 0000000000..db3eafa218 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaPropertySettings.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Models; + +public class MediaPropertySettings +{ + private readonly HashSet _reservedFieldNames = new(); + + /// + /// Gets a set of standard names for fields that cannot be used for custom properties. + /// + public ISet ReservedFieldNames => _reservedFieldNames; + + public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name); +} diff --git a/src/Umbraco.Core/Models/MemberPropertySettings.cs b/src/Umbraco.Core/Models/MemberPropertySettings.cs new file mode 100644 index 0000000000..b7e8b754c8 --- /dev/null +++ b/src/Umbraco.Core/Models/MemberPropertySettings.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Core.Models; + +public class MemberPropertySettings +{ + private readonly HashSet _reservedFieldNames = new(); + + /// + /// Gets a set of standard names for fields that cannot be used for custom properties. + /// + public ISet ReservedFieldNames => _reservedFieldNames; + + public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name); +} diff --git a/src/Umbraco.Core/Services/IReservedFieldNamesService.cs b/src/Umbraco.Core/Services/IReservedFieldNamesService.cs new file mode 100644 index 0000000000..84af0e1f3b --- /dev/null +++ b/src/Umbraco.Core/Services/IReservedFieldNamesService.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Core.Services; + +public interface IReservedFieldNamesService +{ + ISet GetDocumentReservedFieldNames(); + + ISet GetMediaReservedFieldNames(); + + ISet GetMemberReservedFieldNames(); +} diff --git a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs index 194459962b..0c1042c3ff 100644 --- a/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -35,6 +35,7 @@ public static class UmbracoBuilderExtensions builder.Services.TryAddTransient(factory => new PublishedSnapshotServiceOptions()); builder.SetPublishedSnapshotService(); builder.Services.TryAddSingleton(); + builder.Services.TryAddTransient(); // replace this service since we want to improve the content/media // mapping lookups if we are using nucache. diff --git a/src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs b/src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs new file mode 100644 index 0000000000..5243a6a8d6 --- /dev/null +++ b/src/Umbraco.PublishedCache.NuCache/ReservedFieldNamesService.cs @@ -0,0 +1,54 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.PublishedCache; + +internal class ReservedFieldNamesService : IReservedFieldNamesService +{ + private readonly ContentPropertySettings _contentPropertySettings; + private readonly MemberPropertySettings _memberPropertySettings; + private readonly MediaPropertySettings _mediaPropertySettings; + + public ReservedFieldNamesService( + IOptions contentPropertySettings, + IOptions memberPropertySettings, + IOptions mediaPropertySettings) + { + _contentPropertySettings = contentPropertySettings.Value; + _memberPropertySettings = memberPropertySettings.Value; + _mediaPropertySettings = mediaPropertySettings.Value; + } + + public ISet GetDocumentReservedFieldNames() + { + var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet(); + var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet(); + reservedProperties.UnionWith(reservedMethods); + reservedProperties.UnionWith(_contentPropertySettings.ReservedFieldNames); + + return reservedProperties; + } + + public ISet GetMediaReservedFieldNames() + { + var reservedProperties = typeof(IPublishedContent).GetPublicProperties().Select(x => x.Name).ToHashSet(); + var reservedMethods = typeof(IPublishedContent).GetPublicMethods().Select(x => x.Name).ToHashSet(); + reservedProperties.UnionWith(reservedMethods); + reservedProperties.UnionWith(_mediaPropertySettings.ReservedFieldNames); + + return reservedProperties; + } + + public ISet GetMemberReservedFieldNames() + { + var reservedProperties = typeof(PublishedMember).GetPublicProperties().Select(x => x.Name).ToHashSet(); + var reservedMethods = typeof(PublishedMember).GetPublicMethods().Select(x => x.Name).ToHashSet(); + reservedProperties.UnionWith(reservedMethods); + reservedProperties.UnionWith(_memberPropertySettings.ReservedFieldNames); + + return reservedProperties; + } +}