V14: Add reserved fields to config endpoints (#15919)

* Add reserved fields to documents

* Add member configuration endpoint

* Add reserved field to media configuration

* Refactor to use service instead on hardcoded methods

* Clean up, aligning

* Update OpenApi

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
This commit is contained in:
Nikolaj Geisle
2024-03-22 12:11:38 +01:00
committed by GitHub
parent 0ed65671bb
commit dd68a6da7f
17 changed files with 317 additions and 37 deletions

View File

@@ -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> globalSettings,
IOptionsSnapshot<ContentSettings> contentSettings,
IOptionsSnapshot<SegmentSettings> 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<IActionResult> 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<IActionResult>(Ok(responseModel));
}
}

View File

@@ -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> globalSettings, IOptionsSnapshot<ContentSettings> 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<IActionResult> Configuration()
{
var responseModel = new MediaConfigurationResponseModel
{
DisableDeleteWhenReferenced = _contentSettings.DisableDeleteWhenReferenced,
DisableUnpublishWhenReferenced = _contentSettings.DisableUnpublishWhenReferenced,
SanitizeTinyMce = _globalSettings.SanitizeTinyMce,
};
MediaConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMediaConfigurationResponseModel();
return Task.FromResult<IActionResult>(Ok(responseModel));
}
}

View File

@@ -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<IActionResult> Configuration()
{
MemberConfigurationResponseModel responseModel = _configurationPresentationFactory.CreateMemberConfigurationResponseModel();
return Task.FromResult<IActionResult>(Ok(responseModel));
}
}

View File

@@ -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<IConfigurationPresentationFactory, ConfigurationPresentationFactory>();
return builder;
}
}

View File

@@ -32,6 +32,7 @@ public static partial class UmbracoBuilderExtensions
.AddSearchManagement()
.AddTrees()
.AddAuditLogs()
.AddConfigurationFactories()
.AddDocuments()
.AddDocumentTypes()
.AddMedia()

View File

@@ -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> globalSettings,
IOptions<ContentSettings> contentSettings,
IOptions<SegmentSettings> 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(),
};
}

View File

@@ -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();
}

View File

@@ -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": [

View File

@@ -11,4 +11,6 @@ public class DocumentConfigurationResponseModel
public required bool AllowEditInvariantFromNonDefault { get; set; }
public required bool AllowNonExistingSegmentsCreation { get; set; }
public required ISet<string> ReservedFieldNames { get; set; }
}

View File

@@ -7,4 +7,6 @@ public class MediaConfigurationResponseModel
public required bool DisableUnpublishWhenReferenced { get; set; }
public required bool SanitizeTinyMce { get; set; }
public required ISet<string> ReservedFieldNames { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.Member;
public class MemberConfigurationResponseModel
{
public required ISet<string> ReservedFieldNames { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core.Models;
public class ContentPropertySettings
{
private readonly HashSet<string> _reservedFieldNames = new();
/// <summary>
/// Gets a set of standard names for fields that cannot be used for custom properties.
/// </summary>
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
}

View File

@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core.Models;
public class MediaPropertySettings
{
private readonly HashSet<string> _reservedFieldNames = new();
/// <summary>
/// Gets a set of standard names for fields that cannot be used for custom properties.
/// </summary>
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
}

View File

@@ -0,0 +1,13 @@
namespace Umbraco.Cms.Core.Models;
public class MemberPropertySettings
{
private readonly HashSet<string> _reservedFieldNames = new();
/// <summary>
/// Gets a set of standard names for fields that cannot be used for custom properties.
/// </summary>
public ISet<string> ReservedFieldNames => _reservedFieldNames;
public bool AddReservedFieldName(string name) => _reservedFieldNames.Add(name);
}

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Core.Services;
public interface IReservedFieldNamesService
{
ISet<string> GetDocumentReservedFieldNames();
ISet<string> GetMediaReservedFieldNames();
ISet<string> GetMemberReservedFieldNames();
}

View File

@@ -35,6 +35,7 @@ public static class UmbracoBuilderExtensions
builder.Services.TryAddTransient(factory => new PublishedSnapshotServiceOptions());
builder.SetPublishedSnapshotService<PublishedSnapshotService>();
builder.Services.TryAddSingleton<IPublishedSnapshotStatus, PublishedSnapshotStatus>();
builder.Services.TryAddTransient<IReservedFieldNamesService, ReservedFieldNamesService>();
// replace this service since we want to improve the content/media
// mapping lookups if we are using nucache.

View File

@@ -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> contentPropertySettings,
IOptions<MemberPropertySettings> memberPropertySettings,
IOptions<MediaPropertySettings> mediaPropertySettings)
{
_contentPropertySettings = contentPropertySettings.Value;
_memberPropertySettings = memberPropertySettings.Value;
_mediaPropertySettings = mediaPropertySettings.Value;
}
public ISet<string> 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<string> 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<string> 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;
}
}