Media Delivery API (#14692)
* Introduce media API - controllers, services, tests, Swagger docs * Add path to media API response + add "by path" endpoint * Review comments * Implement filtering and sorting * Add explicit media access configuration * Cleanup * Adding default case as in the MediaApiControllerBase * Update src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Swap sort order calculation to align with Content API * Add CreateDate and UpdateDate to media responses * Mirror Content Delivery API behavior for empty children selector --------- Co-authored-by: Elitsa <elm@umbraco.dk> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
This commit is contained in:
@@ -17,12 +17,14 @@ public class ConfigureUmbracoDeliveryApiSwaggerGenOptions: IConfigureOptions<Swa
|
||||
{
|
||||
Title = DeliveryApiConfiguration.ApiTitle,
|
||||
Version = "Latest",
|
||||
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationArticleLink})."
|
||||
Description = $"You can find out more about the {DeliveryApiConfiguration.ApiTitle} in [the documentation]({DeliveryApiConfiguration.ApiDocumentationContentArticleLink})."
|
||||
});
|
||||
|
||||
swaggerGenOptions.DocumentFilter<MimeTypeDocumentFilter>(DeliveryApiConfiguration.ApiName);
|
||||
|
||||
swaggerGenOptions.OperationFilter<SwaggerDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerDocumentationFilter>();
|
||||
swaggerGenOptions.OperationFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.OperationFilter<SwaggerMediaDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerContentDocumentationFilter>();
|
||||
swaggerGenOptions.ParameterFilter<SwaggerMediaDocumentationFilter>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ internal static class DeliveryApiConfiguration
|
||||
|
||||
internal const string ApiName = "delivery";
|
||||
|
||||
internal const string ApiDocumentationArticleLink = "https://docs.umbraco.com/umbraco-cms/v/12.latest/reference/content-delivery-api";
|
||||
internal const string ApiDocumentationContentArticleLink = "https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api";
|
||||
|
||||
// TODO: update this when the Media article is out
|
||||
internal const string ApiDocumentationMediaArticleLink = "https://docs.umbraco.com/umbraco-cms/reference/content-delivery-api";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByIdMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
public ByIdMediaApiController(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
{
|
||||
IPublishedContent? media = PublishedMediaCache.GetById(id);
|
||||
|
||||
if (media is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
}
|
||||
|
||||
return Ok(BuildApiMediaWithCrops(media));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ByPathMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
|
||||
public ByPathMediaApiController(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
|
||||
IApiMediaQueryService apiMediaQueryService)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a media item by its path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path of the media item.</param>
|
||||
/// <returns>The media item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(ApiMediaWithCropsResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByPath(string path)
|
||||
{
|
||||
path = DecodePath(path);
|
||||
|
||||
IPublishedContent? media = _apiMediaQueryService.GetByPath(path);
|
||||
if (media is null)
|
||||
{
|
||||
return await Task.FromResult(NotFound());
|
||||
}
|
||||
|
||||
return Ok(BuildApiMediaWithCrops(media));
|
||||
}
|
||||
}
|
||||
@@ -48,15 +48,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByRoute(string path = "")
|
||||
{
|
||||
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
|
||||
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
|
||||
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
|
||||
// an upcoming version of ASP.NET Core.
|
||||
// See also https://github.com/dotnet/aspnetcore/issues/11544
|
||||
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = WebUtility.UrlDecode(path);
|
||||
}
|
||||
path = DecodePath(path);
|
||||
|
||||
path = path.TrimStart("/");
|
||||
path = path.Length == 0 ? "/" : path;
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[DeliveryApiAccess]
|
||||
[VersionedDeliveryApiRoute("content")]
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
@@ -39,5 +39,9 @@ public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
.WithTitle("Sort option not found")
|
||||
.WithDetail("One of the attempted 'sort' options does not exist")
|
||||
.Build()),
|
||||
_ => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unknown content query status")
|
||||
.WithDetail($"Content query status \"{status}\" was not expected here")
|
||||
.Build()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
using Asp.Versioning;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Attributes;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[DeliveryApiAccess]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
|
||||
[MapToApi(DeliveryApiConfiguration.ApiName)]
|
||||
public abstract class DeliveryApiControllerBase : Controller
|
||||
{
|
||||
protected string DecodePath(string path)
|
||||
{
|
||||
// OpenAPI does not allow reserved chars as "in:path" parameters, so clients based on the Swagger JSON will URL
|
||||
// encode the path. Normally, ASP.NET Core handles that encoding with an automatic decoding - apparently just not
|
||||
// for forward slashes, for whatever reason... so we need to deal with those. Hopefully this will be addressed in
|
||||
// an upcoming version of ASP.NET Core.
|
||||
// See also https://github.com/dotnet/aspnetcore/issues/11544
|
||||
if (path.Contains("%2F", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = WebUtility.UrlDecode(path);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[DeliveryApiMediaAccess]
|
||||
[VersionedDeliveryApiRoute("media")]
|
||||
[ApiExplorerSettings(GroupName = "Media")]
|
||||
public abstract class MediaApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaWithCropsResponseBuilder _apiMediaWithCropsResponseBuilder;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private IPublishedMediaCache? _publishedMediaCache;
|
||||
|
||||
protected MediaApiControllerBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_apiMediaWithCropsResponseBuilder = apiMediaWithCropsResponseBuilder;
|
||||
}
|
||||
|
||||
protected IPublishedMediaCache PublishedMediaCache => _publishedMediaCache
|
||||
??= _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
|
||||
?? throw new InvalidOperationException("Could not obtain the published media cache");
|
||||
|
||||
protected ApiMediaWithCropsResponse BuildApiMediaWithCrops(IPublishedContent media)
|
||||
=> _apiMediaWithCropsResponseBuilder.Build(media);
|
||||
|
||||
protected IActionResult ApiMediaQueryOperationStatusResult(ApiMediaQueryOperationStatus status) =>
|
||||
status switch
|
||||
{
|
||||
ApiMediaQueryOperationStatus.FilterOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Filter option not found")
|
||||
.WithDetail("One of the attempted 'filter' options does not exist")
|
||||
.Build()),
|
||||
ApiMediaQueryOperationStatus.SelectorOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Selector option not found")
|
||||
.WithDetail("The attempted 'fetch' option does not exist")
|
||||
.Build()),
|
||||
ApiMediaQueryOperationStatus.SortOptionNotFound => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Sort option not found")
|
||||
.WithDetail("One of the attempted 'sort' options does not exist")
|
||||
.Build()),
|
||||
_ => BadRequest(new ProblemDetailsBuilder()
|
||||
.WithTitle("Unknown media query status")
|
||||
.WithDetail($"Media query status \"{status}\" was not expected here")
|
||||
.Build()),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class QueryMediaApiController : MediaApiControllerBase
|
||||
{
|
||||
private readonly IApiMediaQueryService _apiMediaQueryService;
|
||||
|
||||
public QueryMediaApiController(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IApiMediaWithCropsResponseBuilder apiMediaWithCropsResponseBuilder,
|
||||
IApiMediaQueryService apiMediaQueryService)
|
||||
: base(publishedSnapshotAccessor, apiMediaWithCropsResponseBuilder)
|
||||
=> _apiMediaQueryService = apiMediaQueryService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of media item(s) from query.
|
||||
/// </summary>
|
||||
/// <param name="fetch">Optional fetch query parameter value.</param>
|
||||
/// <param name="filter">Optional filter query parameters values.</param>
|
||||
/// <param name="sort">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>The paged result of the media item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<ApiMediaWithCropsResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public async Task<IActionResult> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
{
|
||||
Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> queryAttempt = _apiMediaQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
|
||||
if (queryAttempt.Success is false)
|
||||
{
|
||||
return ApiMediaQueryOperationStatusResult(queryAttempt.Status);
|
||||
}
|
||||
|
||||
PagedModel<Guid> pagedResult = queryAttempt.Result;
|
||||
IPublishedContent[] mediaItems = pagedResult.Items.Select(PublishedMediaCache.GetById).WhereNotNull().ToArray();
|
||||
|
||||
var model = new PagedViewModel<ApiMediaWithCropsResponse>
|
||||
{
|
||||
Total = pagedResult.Total,
|
||||
Items = mediaItems.Select(BuildApiMediaWithCrops)
|
||||
};
|
||||
|
||||
return await Task.FromResult(Ok(model));
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class DeliveryApiMediaAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiMediaAccessAttribute()
|
||||
: base(typeof(DeliveryApiMediaAccessFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiMediaAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
|
||||
public DeliveryApiMediaAccessFilter(IApiAccessService apiAccessService)
|
||||
=> _apiAccessService = apiAccessService;
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (_apiAccessService.HasMediaAccess())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFilterBase<ContentApiControllerBase>
|
||||
{
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationContentArticleLink;
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Language",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
|
||||
}
|
||||
});
|
||||
|
||||
AddApiKey(operation);
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Preview",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = "boolean" }
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Start-Item",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
}
|
||||
|
||||
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true, "content");
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false, "content");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Select all ancestors of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
|
||||
},
|
||||
{
|
||||
"Select all ancestors of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:id") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Filter by content type",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by level",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,217 +1,18 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
[Obsolete($"Superseded by {nameof(SwaggerContentDocumentationFilter)} and {nameof(SwaggerMediaDocumentationFilter)}. Will be removed in V14.")]
|
||||
public class SwaggerDocumentationFilter : IOperationFilter, IParameterFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (context.MethodInfo.HasMapToApiAttribute(DeliveryApiConfiguration.ApiName) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString("") } },
|
||||
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
|
||||
{
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1") }
|
||||
},
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Language",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Default", new OpenApiExample { Value = new OpenApiString("") } },
|
||||
{ "English culture", new OpenApiExample { Value = new OpenApiString("en-us") } }
|
||||
}
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Preview",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
Schema = new OpenApiSchema { Type = "boolean" }
|
||||
});
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Start-Item",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
// retained for backwards compat
|
||||
}
|
||||
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (context.DocumentName != DeliveryApiConfiguration.ApiName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the content items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched content items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found content items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true);
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
// retained for backwards compat
|
||||
}
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DeliveryApiConfiguration.ApiDocumentationArticleLink}#query-parameters) for more details on this.";
|
||||
|
||||
private string PaginationDescription(bool skip) => $"Specifies the number of found content items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Select all", new OpenApiExample { Value = new OpenApiString("") } },
|
||||
{
|
||||
"Select all ancestors of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:id") }
|
||||
},
|
||||
{
|
||||
"Select all ancestors of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("ancestors:path") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by id",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:id") }
|
||||
},
|
||||
{
|
||||
"Select all descendants of a node by path",
|
||||
new OpenApiExample { Value = new OpenApiString("descendants:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString("") } },
|
||||
{
|
||||
"Filter by content type",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("contentType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString("") } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by level",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("level:asc"), new OpenApiString("level:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerDocumentationFilterBase<TBaseController> : IOperationFilter, IParameterFilter
|
||||
where TBaseController : Controller
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context.MethodInfo))
|
||||
{
|
||||
ApplyOperation(operation, context);
|
||||
}
|
||||
}
|
||||
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (CanApply(context.ParameterInfo.Member))
|
||||
{
|
||||
ApplyParameter(parameter, context);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract string DocumentationLink { get; }
|
||||
|
||||
protected abstract void ApplyOperation(OpenApiOperation operation, OperationFilterContext context);
|
||||
|
||||
protected abstract void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context);
|
||||
|
||||
protected void AddQueryParameterDocumentation(OpenApiParameter parameter, Dictionary<string, OpenApiExample> examples, string description)
|
||||
{
|
||||
parameter.Description = QueryParameterDescription(description);
|
||||
parameter.Examples = examples;
|
||||
}
|
||||
|
||||
protected void AddExpand(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "expand",
|
||||
In = ParameterLocation.Query,
|
||||
Required = false,
|
||||
Description = QueryParameterDescription("Defines the properties that should be expanded in the response"),
|
||||
Schema = new OpenApiSchema { Type = "string" },
|
||||
Examples = new Dictionary<string, OpenApiExample>
|
||||
{
|
||||
{ "Expand none", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{ "Expand all", new OpenApiExample { Value = new OpenApiString("all") } },
|
||||
{
|
||||
"Expand specific property",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1") }
|
||||
},
|
||||
{
|
||||
"Expand specific properties",
|
||||
new OpenApiExample { Value = new OpenApiString("property:alias1,alias2") }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
Schema = new OpenApiSchema { Type = "string" }
|
||||
});
|
||||
|
||||
protected string PaginationDescription(bool skip, string itemType)
|
||||
=> $"Specifies the number of found {itemType} items to {(skip ? "skip" : "take")}. Use this to control pagination of the response.";
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
|
||||
private bool CanApply(MemberInfo member)
|
||||
=> member.DeclaringType?.Implements<TBaseController>() is true;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal sealed class SwaggerMediaDocumentationFilter : SwaggerDocumentationFilterBase<MediaApiControllerBase>
|
||||
{
|
||||
protected override string DocumentationLink => DeliveryApiConfiguration.ApiDocumentationMediaArticleLink;
|
||||
|
||||
protected override void ApplyOperation(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
operation.Parameters ??= new List<OpenApiParameter>();
|
||||
|
||||
AddExpand(operation);
|
||||
|
||||
AddApiKey(operation);
|
||||
}
|
||||
|
||||
protected override void ApplyParameter(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
switch (parameter.Name)
|
||||
{
|
||||
case "fetch":
|
||||
AddQueryParameterDocumentation(parameter, FetchQueryParameterExamples(), "Specifies the media items to fetch");
|
||||
break;
|
||||
case "filter":
|
||||
AddQueryParameterDocumentation(parameter, FilterQueryParameterExamples(), "Defines how to filter the fetched media items");
|
||||
break;
|
||||
case "sort":
|
||||
AddQueryParameterDocumentation(parameter, SortQueryParameterExamples(), "Defines how to sort the found media items");
|
||||
break;
|
||||
case "skip":
|
||||
parameter.Description = PaginationDescription(true, "media");
|
||||
break;
|
||||
case "take":
|
||||
parameter.Description = PaginationDescription(false, "media");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, OpenApiExample> FetchQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{
|
||||
"Select all children at root level",
|
||||
new OpenApiExample { Value = new OpenApiString("children:/") }
|
||||
},
|
||||
{
|
||||
"Select all children of a media item by id",
|
||||
new OpenApiExample { Value = new OpenApiString("children:id") }
|
||||
},
|
||||
{
|
||||
"Select all children of a media item by path",
|
||||
new OpenApiExample { Value = new OpenApiString("children:path") }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> FilterQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default filter", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Filter by media type",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("mediaType:alias1") } }
|
||||
},
|
||||
{
|
||||
"Filter by name",
|
||||
new OpenApiExample { Value = new OpenApiArray { new OpenApiString("name:nodeName") } }
|
||||
}
|
||||
};
|
||||
|
||||
private Dictionary<string, OpenApiExample> SortQueryParameterExamples() =>
|
||||
new()
|
||||
{
|
||||
{ "Default sort", new OpenApiExample { Value = new OpenApiString(string.Empty) } },
|
||||
{
|
||||
"Sort by create date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("createDate:asc"), new OpenApiString("createDate:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by name",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray { new OpenApiString("name:asc"), new OpenApiString("name:desc") }
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by sort order",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("sortOrder:asc"), new OpenApiString("sortOrder:desc")
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"Sort by update date",
|
||||
new OpenApiExample
|
||||
{
|
||||
Value = new OpenApiArray
|
||||
{
|
||||
new OpenApiString("updateDate:asc"), new OpenApiString("updateDate:desc")
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -23,8 +23,13 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
|
||||
/// <inheritdoc />
|
||||
public bool HasPreviewAccess() => IfEnabled(HasValidApiKey);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasMediaAccess() => IfMediaEnabled(() => _deliveryApiSettings is { PublicAccess: true, Media.PublicAccess: true } || HasValidApiKey());
|
||||
|
||||
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
|
||||
|
||||
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
|
||||
|
||||
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
|
||||
}
|
||||
|
||||
191
src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
Normal file
191
src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
/// <inheritdoc />
|
||||
internal sealed class ApiMediaQueryService : IApiMediaQueryService
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly ILogger<ApiMediaQueryService> _logger;
|
||||
|
||||
public ApiMediaQueryService(IPublishedSnapshotAccessor publishedSnapshotAccessor, ILogger<ApiMediaQueryService> logger)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
{
|
||||
var emptyResult = new PagedModel<Guid>();
|
||||
|
||||
IEnumerable<IPublishedContent>? source = GetSource(fetch);
|
||||
if (source is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(ApiMediaQueryOperationStatus.SelectorOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
source = ApplyFilters(source, filters);
|
||||
if (source is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(ApiMediaQueryOperationStatus.FilterOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
source = ApplySorts(source, sorts);
|
||||
if (source is null)
|
||||
{
|
||||
return Attempt.FailWithStatus(ApiMediaQueryOperationStatus.SortOptionNotFound, emptyResult);
|
||||
}
|
||||
|
||||
return PagedResult(source, skip, take);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPublishedContent? GetByPath(string path)
|
||||
=> TryGetByPath(path, GetRequiredPublishedMediaCache());
|
||||
|
||||
private IPublishedMediaCache GetRequiredPublishedMediaCache()
|
||||
=> _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Media
|
||||
?? throw new InvalidOperationException("Could not obtain the published media cache");
|
||||
|
||||
private IPublishedContent? TryGetByPath(string path, IPublishedMediaCache mediaCache)
|
||||
{
|
||||
var segments = path.Split(Constants.CharArrays.ForwardSlash, StringSplitOptions.RemoveEmptyEntries);
|
||||
IEnumerable<IPublishedContent> currentChildren = mediaCache.GetAtRoot();
|
||||
IPublishedContent? resolvedMedia = null;
|
||||
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
resolvedMedia = currentChildren.FirstOrDefault(c => segment.InvariantEquals(c.Name));
|
||||
if (resolvedMedia is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
currentChildren = resolvedMedia.Children;
|
||||
}
|
||||
|
||||
return resolvedMedia;
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent>? GetSource(string? fetch)
|
||||
{
|
||||
const string childrenOfParameter = "children:";
|
||||
|
||||
if (fetch?.StartsWith(childrenOfParameter, StringComparison.OrdinalIgnoreCase) is not true)
|
||||
{
|
||||
_logger.LogInformation($"The current implementation of {nameof(IApiMediaQueryService)} expects \"{childrenOfParameter}[id/path]\" in the \"{nameof(fetch)}\" query option");
|
||||
return null;
|
||||
}
|
||||
|
||||
var childrenOf = fetch.TrimStart(childrenOfParameter);
|
||||
if (childrenOf.IsNullOrWhiteSpace())
|
||||
{
|
||||
// this mirrors the current behavior of the Content Delivery API :-)
|
||||
return Array.Empty<IPublishedContent>();
|
||||
}
|
||||
|
||||
IPublishedMediaCache mediaCache = GetRequiredPublishedMediaCache();
|
||||
if (childrenOf.Trim(Constants.CharArrays.ForwardSlash).Length == 0)
|
||||
{
|
||||
return mediaCache.GetAtRoot();
|
||||
}
|
||||
|
||||
IPublishedContent? parent = Guid.TryParse(childrenOf, out Guid parentKey)
|
||||
? mediaCache.GetById(parentKey)
|
||||
: TryGetByPath(childrenOf, mediaCache);
|
||||
|
||||
return parent?.Children ?? Array.Empty<IPublishedContent>();
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent>? ApplyFilters(IEnumerable<IPublishedContent> source, IEnumerable<string> filters)
|
||||
{
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
var parts = filter.Split(':');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
// invalid filter
|
||||
_logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (parts[0])
|
||||
{
|
||||
case "mediaType":
|
||||
source = source.Where(c => c.ContentType.Alias == parts[1]);
|
||||
break;
|
||||
case "name":
|
||||
source = source.Where(c => c.Name.InvariantContains(parts[1]));
|
||||
break;
|
||||
default:
|
||||
// unknown filter
|
||||
_logger.LogInformation($"The \"{nameof(filters)}\" query option \"{filter}\" is not supported");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
private IEnumerable<IPublishedContent>? ApplySorts(IEnumerable<IPublishedContent> source, IEnumerable<string> sorts)
|
||||
{
|
||||
foreach (var sort in sorts)
|
||||
{
|
||||
var parts = sort.Split(':');
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
// invalid sort
|
||||
_logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not valid");
|
||||
return null;
|
||||
}
|
||||
|
||||
Func<IPublishedContent, object> keySelector;
|
||||
switch (parts[0])
|
||||
{
|
||||
case "createDate":
|
||||
keySelector = content => content.CreateDate;
|
||||
break;
|
||||
case "updateDate":
|
||||
keySelector = content => content.UpdateDate;
|
||||
break;
|
||||
case "name":
|
||||
keySelector = content => content.Name.ToLowerInvariant();
|
||||
break;
|
||||
case "sortOrder":
|
||||
keySelector = content => content.SortOrder;
|
||||
break;
|
||||
default:
|
||||
// unknown sort
|
||||
_logger.LogInformation($"The \"{nameof(sorts)}\" query option \"{sort}\" is not supported");
|
||||
return null;
|
||||
}
|
||||
|
||||
source = parts[1].StartsWith("asc")
|
||||
? source.OrderBy(keySelector)
|
||||
: source.OrderByDescending(keySelector);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
private Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> PagedResult(IEnumerable<IPublishedContent> children, int skip, int take)
|
||||
{
|
||||
IPublishedContent[] childrenAsArray = children as IPublishedContent[] ?? children.ToArray();
|
||||
var result = new PagedModel<Guid>
|
||||
{
|
||||
Total = childrenAsArray.Length,
|
||||
Items = childrenAsArray.Skip(skip).Take(take).Select(child => child.Key)
|
||||
};
|
||||
|
||||
return Attempt.SucceedWithStatus(ApiMediaQueryOperationStatus.Success, result);
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,40 @@ public class DeliveryApiSettings
|
||||
/// <value><c>true</c> if the Delivery API should output rich text values as JSON; <c>false</c> they should be output as HTML (default).</value>
|
||||
[DefaultValue(StaticRichTextOutputAsJson)]
|
||||
public bool RichTextOutputAsJson { get; set; } = StaticRichTextOutputAsJson;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the settings for the Media APIs of the Delivery API.
|
||||
/// </summary>
|
||||
public MediaSettings Media { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for the Media APIs of the Delivery API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Delivery API settings (as configured in <see cref="DeliveryApiSettings"/>) supersede these settings in levels of restriction.
|
||||
/// I.e. the Media APIs cannot be enabled, if the Delivery API is disabled.
|
||||
/// </remarks>
|
||||
public class MediaSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Media APIs of the Delivery API should be enabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Media APIs should be enabled; otherwise, <c>false</c>.</value>
|
||||
/// <remarks>
|
||||
/// Setting this to <c>true</c> will have no effect if the Delivery API itself is disabled through <see cref="DeliveryApiSettings"/>
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticEnabled)]
|
||||
public bool Enabled { get; set; } = StaticEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Media APIs of the Delivery API (if enabled) should be
|
||||
/// publicly available or should require an API key for access.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Media APIs should be publicly available; <c>false</c> if an API key should be required for access.</value>
|
||||
/// <remarks>
|
||||
/// Setting this to <c>true</c> will have no effect if the Delivery API itself has public access disabled through <see cref="DeliveryApiSettings"/>
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticPublicAccess)]
|
||||
public bool PublicAccess { get; set; } = StaticPublicAccess;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,4 +11,9 @@ public interface IApiAccessService
|
||||
/// Retrieves information on whether or not the API currently allows preview access.
|
||||
/// </summary>
|
||||
bool HasPreviewAccess();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information on whether or not the API currently allows access to media.
|
||||
/// </summary>
|
||||
bool HasMediaAccess() => false;
|
||||
}
|
||||
|
||||
29
src/Umbraco.Core/DeliveryApi/IApiMediaQueryService.cs
Normal file
29
src/Umbraco.Core/DeliveryApi/IApiMediaQueryService.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// Service that handles querying of the Media APIs.
|
||||
/// </summary>
|
||||
public interface IApiMediaQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an attempt with a collection of media ids that passed the search criteria as a paged model.
|
||||
/// </summary>
|
||||
/// <param name="fetch">Optional fetch query parameter value.</param>
|
||||
/// <param name="filters">Optional filter query parameters values.</param>
|
||||
/// <param name="sorts">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <returns>A paged model of media ids that are returned after applying the search queries in an attempt.</returns>
|
||||
Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the media item that matches the supplied path (if any).
|
||||
/// </summary>
|
||||
/// <param name="path">The path to look up.</param>
|
||||
/// <returns>The media item at <see cref="path"/>, or null if it does not exist.</returns>
|
||||
IPublishedContent? GetByPath(string path);
|
||||
}
|
||||
15
src/Umbraco.Core/DeliveryApi/NoopApiMediaQueryService.cs
Normal file
15
src/Umbraco.Core/DeliveryApi/NoopApiMediaQueryService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopApiMediaQueryService : IApiMediaQueryService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Attempt<PagedModel<Guid>, ApiMediaQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
=> Attempt.SucceedWithStatus(ApiMediaQueryOperationStatus.Success, new PagedModel<Guid>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public IPublishedContent? GetByPath(string path) => null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
public enum ApiMediaQueryOperationStatus
|
||||
{
|
||||
Success,
|
||||
FilterOptionNotFound,
|
||||
SelectorOptionNotFound,
|
||||
SortOptionNotFound
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
internal sealed class ApiMediaWithCropsBuilder : ApiMediaWithCropsBuilderBase<ApiMediaWithCrops>, IApiMediaWithCropsBuilder
|
||||
{
|
||||
public ApiMediaWithCropsBuilder(IApiMediaBuilder apiMediaBuilder, IPublishedValueFallback publishedValueFallback)
|
||||
: base(apiMediaBuilder, publishedValueFallback)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ApiMediaWithCrops Create(
|
||||
IPublishedContent media,
|
||||
IApiMedia inner,
|
||||
ImageCropperValue.ImageCropperFocalPoint? focalPoint,
|
||||
IEnumerable<ImageCropperValue.ImageCropperCrop>? crops) =>
|
||||
new ApiMediaWithCrops(inner, focalPoint, crops);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
internal abstract class ApiMediaWithCropsBuilderBase<T>
|
||||
where T : IApiMedia
|
||||
{
|
||||
private readonly IApiMediaBuilder _apiMediaBuilder;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
|
||||
protected ApiMediaWithCropsBuilderBase(IApiMediaBuilder apiMediaBuilder, IPublishedValueFallback publishedValueFallback)
|
||||
{
|
||||
_apiMediaBuilder = apiMediaBuilder;
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
}
|
||||
|
||||
protected abstract T Create(
|
||||
IPublishedContent media,
|
||||
IApiMedia inner,
|
||||
ImageCropperValue.ImageCropperFocalPoint? focalPoint,
|
||||
IEnumerable<ImageCropperValue.ImageCropperCrop>? crops);
|
||||
|
||||
public T Build(MediaWithCrops media)
|
||||
{
|
||||
IApiMedia inner = _apiMediaBuilder.Build(media.Content);
|
||||
|
||||
// make sure we merge crops and focal point defined at media level with the locally defined ones (local ones take precedence in case of a conflict)
|
||||
ImageCropperValue? mediaCrops = media.Content.Value<ImageCropperValue>(_publishedValueFallback, Constants.Conventions.Media.File);
|
||||
ImageCropperValue localCrops = media.LocalCrops;
|
||||
if (mediaCrops is not null)
|
||||
{
|
||||
localCrops = localCrops.Merge(mediaCrops);
|
||||
}
|
||||
|
||||
return Create(media.Content, inner, localCrops.FocalPoint, localCrops.Crops);
|
||||
}
|
||||
|
||||
public T Build(IPublishedContent media)
|
||||
{
|
||||
var mediaWithCrops = new MediaWithCrops(media, _publishedValueFallback, new ImageCropperValue());
|
||||
return Build(mediaWithCrops);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
internal sealed class ApiMediaWithCropsResponseBuilder : ApiMediaWithCropsBuilderBase<ApiMediaWithCropsResponse>, IApiMediaWithCropsResponseBuilder
|
||||
{
|
||||
public ApiMediaWithCropsResponseBuilder(IApiMediaBuilder apiMediaBuilder, IPublishedValueFallback publishedValueFallback)
|
||||
: base(apiMediaBuilder, publishedValueFallback)
|
||||
{
|
||||
}
|
||||
|
||||
protected override ApiMediaWithCropsResponse Create(
|
||||
IPublishedContent media,
|
||||
IApiMedia inner,
|
||||
ImageCropperValue.ImageCropperFocalPoint? focalPoint,
|
||||
IEnumerable<ImageCropperValue.ImageCropperCrop>? crops)
|
||||
{
|
||||
var path = $"/{string.Join("/", PathSegments(media).Reverse())}/";
|
||||
return new ApiMediaWithCropsResponse(inner, focalPoint, crops, path, media.CreateDate, media.UpdateDate);
|
||||
}
|
||||
|
||||
private IEnumerable<string> PathSegments(IPublishedContent media)
|
||||
{
|
||||
IPublishedContent? current = media;
|
||||
while (current != null)
|
||||
{
|
||||
yield return current.Name.ToLowerInvariant();
|
||||
current = current.Parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
public interface IApiMediaWithCropsBuilder
|
||||
{
|
||||
ApiMediaWithCrops Build(MediaWithCrops media);
|
||||
|
||||
ApiMediaWithCrops Build(IPublishedContent media);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
|
||||
public interface IApiMediaWithCropsResponseBuilder
|
||||
{
|
||||
ApiMediaWithCropsResponse Build(IPublishedContent media);
|
||||
}
|
||||
@@ -423,6 +423,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiContentBuilder, ApiContentBuilder>();
|
||||
builder.Services.AddSingleton<IApiContentResponseBuilder, ApiContentResponseBuilder>();
|
||||
builder.Services.AddSingleton<IApiMediaBuilder, ApiMediaBuilder>();
|
||||
builder.Services.AddSingleton<IApiMediaWithCropsBuilder, ApiMediaWithCropsBuilder>();
|
||||
builder.Services.AddSingleton<IApiMediaWithCropsResponseBuilder, ApiMediaWithCropsResponseBuilder>();
|
||||
builder.Services.AddSingleton<IApiContentNameProvider, ApiContentNameProvider>();
|
||||
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, NoopOutputExpansionStrategyAccessor>();
|
||||
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, NoopRequestStartItemProviderAccessor>();
|
||||
@@ -432,6 +434,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IRequestPreviewService, NoopRequestPreviewService>();
|
||||
builder.Services.AddSingleton<IApiAccessService, NoopApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, NoopApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, NoopApiMediaQueryService>();
|
||||
builder.Services.AddSingleton<IApiMediaUrlProvider, ApiMediaUrlProvider>();
|
||||
builder.Services.AddSingleton<IApiContentRouteBuilder, ApiContentRouteBuilder>();
|
||||
builder.Services.AddSingleton<IApiPublishedContentCache, ApiPublishedContentCache>();
|
||||
|
||||
@@ -2,7 +2,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
internal sealed class ApiMediaWithCrops : IApiMedia
|
||||
public class ApiMediaWithCrops : IApiMedia
|
||||
{
|
||||
private readonly IApiMedia _inner;
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public sealed class ApiMediaWithCropsResponse : ApiMediaWithCrops
|
||||
{
|
||||
public ApiMediaWithCropsResponse(
|
||||
IApiMedia inner,
|
||||
ImageCropperValue.ImageCropperFocalPoint? focalPoint,
|
||||
IEnumerable<ImageCropperValue.ImageCropperCrop>? crops,
|
||||
string path,
|
||||
DateTime createDate,
|
||||
DateTime updateDate)
|
||||
: base(inner, focalPoint, crops)
|
||||
{
|
||||
Path = path;
|
||||
CreateDate = createDate;
|
||||
UpdateDate = updateDate;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public DateTime CreateDate { get; }
|
||||
|
||||
public DateTime UpdateDate { get; }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -19,9 +20,9 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
private readonly IPublishedValueFallback _publishedValueFallback;
|
||||
private readonly IApiMediaBuilder _apiMediaBuilder;
|
||||
private readonly IApiMediaWithCropsBuilder _apiMediaWithCropsBuilder;
|
||||
|
||||
[Obsolete("Use constructor that takes all parameters, scheduled for removal in V14")]
|
||||
[Obsolete($"Use constructor that takes {nameof(IApiMediaWithCropsBuilder)}, scheduled for removal in V14")]
|
||||
public MediaPickerWithCropsValueConverter(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
@@ -32,24 +33,52 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID
|
||||
publishedUrlProvider,
|
||||
publishedValueFallback,
|
||||
jsonSerializer,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IApiMediaBuilder>()
|
||||
StaticServiceProvider.Instance.GetRequiredService<IApiMediaWithCropsBuilder>()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Use constructor that takes {nameof(IApiMediaWithCropsBuilder)}, scheduled for removal in V14")]
|
||||
public MediaPickerWithCropsValueConverter(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiMediaBuilder apiMediaBuilder)
|
||||
: this(
|
||||
publishedSnapshotAccessor,
|
||||
publishedUrlProvider,
|
||||
publishedValueFallback,
|
||||
jsonSerializer,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IApiMediaWithCropsBuilder>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Use constructor that takes {nameof(IApiMediaWithCropsBuilder)} and no {nameof(IApiMediaBuilder)}, scheduled for removal in V14")]
|
||||
public MediaPickerWithCropsValueConverter(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiMediaBuilder apiMediaBuilder,
|
||||
IApiMediaWithCropsBuilder apiMediaWithCropsBuilder)
|
||||
: this(publishedSnapshotAccessor, publishedUrlProvider, publishedValueFallback, jsonSerializer, apiMediaWithCropsBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
public MediaPickerWithCropsValueConverter(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IPublishedValueFallback publishedValueFallback,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IApiMediaBuilder apiMediaBuilder)
|
||||
IApiMediaWithCropsBuilder apiMediaWithCropsBuilder)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor ??
|
||||
throw new ArgumentNullException(nameof(publishedSnapshotAccessor));
|
||||
_publishedUrlProvider = publishedUrlProvider;
|
||||
_publishedValueFallback = publishedValueFallback;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_apiMediaBuilder = apiMediaBuilder;
|
||||
_apiMediaWithCropsBuilder = apiMediaWithCropsBuilder;
|
||||
}
|
||||
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType) =>
|
||||
@@ -128,20 +157,7 @@ public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase, ID
|
||||
{
|
||||
var isMultiple = IsMultipleDataType(propertyType.DataType);
|
||||
|
||||
ApiMediaWithCrops ToApiMedia(MediaWithCrops media)
|
||||
{
|
||||
IApiMedia inner = _apiMediaBuilder.Build(media.Content);
|
||||
|
||||
// make sure we merge crops and focal point defined at media level with the locally defined ones (local ones take precedence in case of a conflict)
|
||||
ImageCropperValue? mediaCrops = media.Content.Value<ImageCropperValue>(_publishedValueFallback, Constants.Conventions.Media.File);
|
||||
ImageCropperValue localCrops = media.LocalCrops;
|
||||
if (mediaCrops != null)
|
||||
{
|
||||
localCrops = localCrops.Merge(mediaCrops);
|
||||
}
|
||||
|
||||
return new ApiMediaWithCrops(inner, localCrops.FocalPoint, localCrops.Crops);
|
||||
}
|
||||
ApiMediaWithCrops ToApiMedia(MediaWithCrops media) => _apiMediaWithCropsBuilder.Build(media);
|
||||
|
||||
// NOTE: eventually we might implement this explicitly instead of piggybacking on the default object conversion. however, this only happens once per cache rebuild,
|
||||
// and the performance gain from an explicit implementation is negligible, so... at least for the time being this will do just fine.
|
||||
|
||||
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
@@ -18,16 +19,19 @@ public class MediaPickerWithCropsValueConverterTests : PropertyValueConverterTes
|
||||
var serializer = new JsonNetSerializer();
|
||||
var publishedValueFallback = Mock.Of<IPublishedValueFallback>();
|
||||
var apiUrlProvider = new ApiMediaUrlProvider(PublishedUrlProvider);
|
||||
var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(
|
||||
new ApiMediaBuilder(
|
||||
new ApiContentNameProvider(),
|
||||
apiUrlProvider,
|
||||
publishedValueFallback,
|
||||
CreateOutputExpansionStrategyAccessor()),
|
||||
publishedValueFallback);
|
||||
return new MediaPickerWithCropsValueConverter(
|
||||
PublishedSnapshotAccessor,
|
||||
PublishedUrlProvider,
|
||||
publishedValueFallback,
|
||||
serializer,
|
||||
new ApiMediaBuilder(
|
||||
new ApiContentNameProvider(),
|
||||
apiUrlProvider,
|
||||
Mock.Of<IPublishedValueFallback>(),
|
||||
CreateOutputExpansionStrategyAccessor()));
|
||||
apiMediaWithCropsBuilder);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Infrastructure.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
|
||||
@@ -545,7 +546,10 @@ public class OutputExpansionStrategyTests : PropertyValueConverterTests
|
||||
}
|
||||
});
|
||||
|
||||
MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, Mock.Of<IPublishedValueFallback>(), new JsonNetSerializer(), mediaBuilder);
|
||||
var publishedValueFallback = Mock.Of<IPublishedValueFallback>();
|
||||
var apiMediaWithCropsBuilder = new ApiMediaWithCropsBuilder(mediaBuilder, publishedValueFallback);
|
||||
|
||||
MediaPickerWithCropsValueConverter mediaPickerValueConverter = new MediaPickerWithCropsValueConverter(PublishedSnapshotAccessor, PublishedUrlProvider, publishedValueFallback, new JsonNetSerializer(), apiMediaWithCropsBuilder);
|
||||
var mediaPickerPropertyType = SetupPublishedPropertyType(mediaPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.MediaPicker3, new MediaPicker3Configuration());
|
||||
|
||||
return new PublishedElementPropertyBase(mediaPickerPropertyType, parent, false, PropertyCacheLevel.None, value);
|
||||
|
||||
Reference in New Issue
Block a user