Add endpoint for retrieving allowed media types for file extensions (#16189)

* Add endpoint for retrieving allowed media types for file extensions

* Moved paging into service

---------

Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
Kenn Jacobsen
2024-04-30 14:26:12 +02:00
committed by GitHub
parent 3eef9b9579
commit 39e51a4467
6 changed files with 284 additions and 4 deletions

View File

@@ -0,0 +1,37 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.MediaType.Item;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services.ContentTypeEditing;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Item;
[ApiVersion("1.0")]
public class AllowedMediaTypeItemController : MediaTypeItemControllerBase
{
private readonly IMediaTypeEditingService _mediaTypeEditingService;
private readonly IUmbracoMapper _mapper;
public AllowedMediaTypeItemController(IMediaTypeEditingService mediaTypeEditingService, IUmbracoMapper mapper)
{
_mediaTypeEditingService = mediaTypeEditingService;
_mapper = mapper;
}
[HttpGet("allowed")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedModel<MediaTypeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> Item(CancellationToken cancellationToken, string fileExtension, int skip = 0, int take = 100)
{
PagedModel<IMediaType> mediaTypes = await _mediaTypeEditingService.GetMediaTypesForFileExtensionAsync(fileExtension, skip, take);
var result = new PagedModel<MediaTypeItemResponseModel>
{
Items = _mapper.MapEnumerable<IMediaType, MediaTypeItemResponseModel>(mediaTypes.Items),
Total = mediaTypes.Total
};
return Ok(result);
}
}

View File

@@ -12794,6 +12794,68 @@
]
}
},
"/umbraco/management/api/v1/item/media-type/allowed": {
"get": {
"tags": [
"Media Type"
],
"operationId": "GetItemMediaTypeAllowed",
"parameters": [
{
"name": "fileExtension",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedModelMediaTypeItemResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user do not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/item/media-type/search": {
"get": {
"tags": [

View File

@@ -14,4 +14,6 @@ public interface IMediaTypeEditingService
Guid? key,
IEnumerable<Guid> currentCompositeKeys,
IEnumerable<string> currentPropertyAliases);
Task<PagedModel<IMediaType>> GetMediaTypesForFileExtensionAsync(string fileExtension, int skip, int take);
}

View File

@@ -1,22 +1,32 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentTypeEditing;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services.ContentTypeEditing;
internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IMediaType, IMediaTypeService, MediaTypePropertyTypeModel, MediaTypePropertyContainerModel>, IMediaTypeEditingService
{
private readonly IMediaTypeService _mediaTypeService;
private readonly IDataTypeService _dataTypeService;
private readonly IImageUrlGenerator _imageUrlGenerator;
public MediaTypeEditingService(
IContentTypeService contentTypeService,
IMediaTypeService mediaTypeService,
IDataTypeService dataTypeService,
IEntityService entityService,
IShortStringHelper shortStringHelper)
IShortStringHelper shortStringHelper,
IImageUrlGenerator imageUrlGenerator)
: base(contentTypeService, mediaTypeService, dataTypeService, entityService, shortStringHelper)
=> _mediaTypeService = mediaTypeService;
{
_mediaTypeService = mediaTypeService;
_dataTypeService = dataTypeService;
_imageUrlGenerator = imageUrlGenerator;
}
public async Task<Attempt<IMediaType?, ContentTypeOperationStatus>> CreateAsync(MediaTypeCreateModel model, Guid userKey)
{
@@ -48,6 +58,50 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
IEnumerable<string> currentPropertyAliases) =>
await FindAvailableCompositionsAsync(key, currentCompositeKeys, currentPropertyAliases);
public async Task<PagedModel<IMediaType>> GetMediaTypesForFileExtensionAsync(string fileExtension, int skip, int take)
{
fileExtension = fileExtension.TrimStart(Constants.CharArrays.Period);
IMediaType[] candidateMediaTypes = _mediaTypeService.GetAll().Where(mt => mt.CompositionPropertyTypes.Any(pt => pt.Alias == Constants.Conventions.Media.File)).ToArray();
var allowedMediaTypes = new List<IMediaType>();
// is this an image format supported by the image cropper?
if (_imageUrlGenerator.IsSupportedImageFormat(fileExtension))
{
// yes - add all media types with an image cropper "file" property
allowedMediaTypes.AddRange(candidateMediaTypes
.Where(mt => mt.CompositionPropertyTypes.Any(propertyType => propertyType is
{
Alias: Constants.Conventions.Media.File,
PropertyEditorAlias: Constants.PropertyEditors.Aliases.ImageCropper
})));
}
// find media types that have an explicit allow-list of file extensions
// - NOTE: an empty allow-list should be interpreted as "all file extensions are allowed"
IDictionary<IMediaType, IEnumerable<string>> allowedFileExtensionsByMediaType = await FetchAllowedFileExtensionsByMediaTypeAsync(candidateMediaTypes);
// add all media types where the file extension is explicitly allowed
allowedMediaTypes.AddRange(allowedFileExtensionsByMediaType
.Where(kvp => kvp.Value.Contains(fileExtension))
.Select(kvp => kvp.Key));
// if we at this point have no allowed media types, add all media types that allow any file extension
if (allowedMediaTypes.Any() is false)
{
allowedMediaTypes.AddRange(allowedFileExtensionsByMediaType
.Where(kvp => kvp.Value.Any() is false)
.Select(kvp => kvp.Key));
}
return new PagedModel<IMediaType>()
{
Items = allowedMediaTypes.Skip(skip).Take(take),
Total = allowedMediaTypes.Count
};
}
protected override IMediaType CreateContentType(IShortStringHelper shortStringHelper, int parentId)
=> new MediaType(shortStringHelper, parentId);
@@ -56,4 +110,35 @@ internal sealed class MediaTypeEditingService : ContentTypeEditingServiceBase<IM
protected override UmbracoObjectTypes ContentTypeObjectType => UmbracoObjectTypes.MediaType;
protected override UmbracoObjectTypes ContainerObjectType => UmbracoObjectTypes.MediaTypeContainer;
private async Task<IDictionary<IMediaType, IEnumerable<string>>> FetchAllowedFileExtensionsByMediaTypeAsync(IEnumerable<IMediaType> mediaTypes)
{
var allowedFileExtensionsByMediaType = new Dictionary<IMediaType, IEnumerable<string>>();
foreach (IMediaType mediaType in mediaTypes)
{
IPropertyType? propertyType = mediaType
.CompositionPropertyTypes
.FirstOrDefault(propertyType => propertyType is
{
Alias: Constants.Conventions.Media.File,
PropertyEditorAlias: Constants.PropertyEditors.Aliases.UploadField
});
if (propertyType is null)
{
continue;
}
IDataType? dataType = await _dataTypeService.GetAsync(propertyType.DataTypeKey);
FileUploadConfiguration? fileUploadConfiguration = dataType?.ConfigurationAs<FileUploadConfiguration>();
if (fileUploadConfiguration is null)
{
continue;
}
allowedFileExtensionsByMediaType[mediaType] = fileUploadConfiguration.FileExtensions;
}
return allowedFileExtensionsByMediaType;
}
}

View File

@@ -0,0 +1,78 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
public partial class MediaTypeEditingServiceTests
{
[TestCase("jpg", Constants.Conventions.MediaTypes.Image)]
[TestCase("png", Constants.Conventions.MediaTypes.Image)]
[TestCase("svg", Constants.Conventions.MediaTypes.VectorGraphicsAlias)]
[TestCase("pdf", Constants.Conventions.MediaTypes.ArticleAlias)]
[TestCase("doc", Constants.Conventions.MediaTypes.ArticleAlias)]
[TestCase("docx", Constants.Conventions.MediaTypes.ArticleAlias)]
[TestCase("mp4", Constants.Conventions.MediaTypes.VideoAlias)]
[TestCase("webm", Constants.Conventions.MediaTypes.VideoAlias)]
[TestCase("ogv", Constants.Conventions.MediaTypes.VideoAlias)]
[TestCase("mp3", Constants.Conventions.MediaTypes.AudioAlias)]
[TestCase("weba", Constants.Conventions.MediaTypes.AudioAlias)]
[TestCase("oga", Constants.Conventions.MediaTypes.AudioAlias)]
[TestCase("opus", Constants.Conventions.MediaTypes.AudioAlias)]
[TestCase("abc", Constants.Conventions.MediaTypes.File)]
[TestCase("123", Constants.Conventions.MediaTypes.File)]
public async Task Can_Get_Default_Allowed_Media_Types(string fileExtension, string expectedMediaTypeAlias)
{
var allowedMediaTypes = await MediaTypeEditingService.GetMediaTypesForFileExtensionAsync(fileExtension, 0, 100);
Assert.AreEqual(1, allowedMediaTypes.Total);
Assert.AreEqual(expectedMediaTypeAlias, allowedMediaTypes.Items.First().Alias);
}
[TestCase("jpg")]
[TestCase(".jpg")]
public async Task Ignores_Heading_Period_For_Allowed_Media_Types(string fileExtension)
{
var allowedMediaTypes = await MediaTypeEditingService.GetMediaTypesForFileExtensionAsync(fileExtension, 0, 100);
Assert.AreEqual(1, allowedMediaTypes.Total);
Assert.AreEqual(Constants.Conventions.MediaTypes.Image, allowedMediaTypes.Items.First().Alias);
}
[Test]
public async Task Can_Yield_Multiple_Allowed_Media_Types()
{
var mediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.ArticleAlias)!;
var uploadPropertyType = mediaType.PropertyTypes.Single(pt => pt.Alias == Constants.Conventions.Media.File);
var dataTypeService = GetRequiredService<IDataTypeService>();
var dataType = (await dataTypeService.GetAsync(uploadPropertyType.DataTypeKey))!;
dataType.ConfigurationData["fileExtensions"] = new[] { "pdf", "jpg" };
await dataTypeService.UpdateAsync(dataType, Constants.Security.SuperUserKey);
var allowedMediaTypes = await MediaTypeEditingService.GetMediaTypesForFileExtensionAsync("jpg", 0, 100);
Assert.AreEqual(2, allowedMediaTypes.Total);
Assert.AreEqual(Constants.Conventions.MediaTypes.Image, allowedMediaTypes.Items.First().Alias);
Assert.AreEqual(Constants.Conventions.MediaTypes.ArticleAlias, allowedMediaTypes.Items.Last().Alias);
}
[Test]
public async Task Get_Get_Media_Types_For_FileExtensions_Using_Skip_Take()
{
var mediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.ArticleAlias)!;
var uploadPropertyType = mediaType.PropertyTypes.Single(pt => pt.Alias == Constants.Conventions.Media.File);
var dataTypeService = GetRequiredService<IDataTypeService>();
var dataType = (await dataTypeService.GetAsync(uploadPropertyType.DataTypeKey))!;
dataType.ConfigurationData["fileExtensions"] = new[] { "pdf", "jpg" };
await dataTypeService.UpdateAsync(dataType, Constants.Security.SuperUserKey);
var allowedMediaTypes = await MediaTypeEditingService.GetMediaTypesForFileExtensionAsync("jpg", 0, 1);
Assert.AreEqual(2, allowedMediaTypes.Total);
Assert.AreEqual(1, allowedMediaTypes.Items.Count());
Assert.AreEqual(Constants.Conventions.MediaTypes.Image, allowedMediaTypes.Items.First().Alias);
allowedMediaTypes = await MediaTypeEditingService.GetMediaTypesForFileExtensionAsync("jpg", 1, 1);
Assert.AreEqual(2, allowedMediaTypes.Total);
Assert.AreEqual(1, allowedMediaTypes.Items.Count());
Assert.AreEqual(Constants.Conventions.MediaTypes.ArticleAlias, allowedMediaTypes.Items.First().Alias);
}
}

View File

@@ -1,4 +1,8 @@
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
/// <summary>
/// Tests for the media type editing service. Please notice that a lot of functional test is covered by the content type
@@ -6,4 +10,16 @@
/// </summary>
public partial class MediaTypeEditingServiceTests : ContentTypeEditingServiceTestsBase
{
protected override void ConfigureTestServices(IServiceCollection services)
{
base.ConfigureTestServices(services);
services.AddSingleton<IImageUrlGenerator, TestImageUrlGenerator>();
}
private class TestImageUrlGenerator : IImageUrlGenerator
{
public IEnumerable<string> SupportedImageFileTypes => new[] { "jpg", "gif", "png" };
public string? GetImageUrl(ImageUrlGenerationOptions options) => options?.ImageUrl;
}
}