Content Delivery API (#14051)
* Add the core parts of the headless PoC * Add Content API project (WIP - loads of TODOs and dupes that need to be fixed!) * Rename the content API project and namespaces * Fixed bad merge * Rename everything "Headless" to "ContentApi" or "Api" * Refactor Content + Media: Key => Id, Name not nullable * Make Content API property return value types independent of datatype configuration * Clean up refactorings * First stab at an expansion strategy using content picker as example implementation * Use named JSON options for content API serialization * Proper inclusion and registration of the content API * Introduce API media builder * Make MNTP return API content/media depending on configuration (instead of links) and support output expansion * Content API: Get by controllers (#13740) * Adding ContentApiControllerBase * Adding get by id and url controllers * Change route of get all test controller * Rename to ContentApiController * Refactoring * Removing test controller * Content API: Add start-node header value to deal with url collisions (#13746) * Use start-node header value to deal with url collisions * Cleanup * Rename "url" param to "path" * Adding a start node service to get the start-node header value * Trim '/' from both beginning and end * Content API: Support Accept-Language header (#13831) * Move the content API JSON type resolver to an appropriate namespace * Add localization based on Accept-Language header * Content API: Output expansion (#13848) * Implement request based output expansion strategy + expansion output cache at property level * Slighty leaner implementation for default output expansion strategy * Clarify the code a bit * Fix bad merge * Encapsulate content API dependencies in the DI * Support multi-site and multi-culture routing + a little rename/refactor (#13882) * Support multi-site and multi-culture routing + a little rename/refactor * Make the by route controller handle root nodes * Rename Url to Path in API content output * Add a few comments for magic route creation * Rename services from "Default" to "Noop" * Ensure that Umbraco can boot without adding "AddContentApi()" to ConfigureServices * Moved incorrectly placed media builder * Fix API routes (#13915) * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Include umbraco properties (width, height, ...) in the Media Properties collection (#14023) * Move umbraco properties (width, height, ...) to the Properties collection of the API Media model * Don't output the umbracoFile property of media items * Add content type deny list (#14025) * Create a deny list of content types and utilize it for output generation * Add unit tests * Dedicated property cache level for Content API (#14027) * Support redirect tracking (#14033) * Create a deny list of content types and utilize it for output generation * Add unit tests * Handle redirect tracking in the content API * Include start item routing info for redirects * Add cultures and their routes to the API output (#14038) * Create a deny list of content types and utilize it for output generation * Add unit tests * Handle redirect tracking in the content API * Include start item routing info for redirects * Add culture routes to root output (for HREFLANG support) * Rename redirect service method to better match its purpose * Review changes * Delivery API: Query controller (#14041) * Initial commit * Custom ContentAPIFieldDefinitionCollection * Make index IUmbracoContentIndex * Add querying for children by parent id (key) * Add missing interface * Adding querying endpoint * Test code * Compose unpublishedValueSet, so that you get the correct data in the ContentAPI index * Renaming * Fix ancestorKeys index values * Adding IApiQueryExtensionService to be able to query the ContentAPI index in a generic way * Fix IApiQueryService and clean up QueryContentApiController using it * Support querying for path * Fix content API indexing * Fix default sorting * Implement concrete QueryOption implementations * Introduce new ExecuteQuery that uses the Core OptionHandlers * Implement ExecuteQuery * Change ExecuteQuery signature and implementation * Implement demo sorting and fetching * Add query option handlers and collection builder for them * Cleanup * Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)" This reverts commit 78e1f748e55383baecd123d06457111e18f13365. * Revert "Delivery API: Content routing and structure (#13984)" This reverts commit a0292ae5350362dd6c1c5bc9763deda928c78a75. * Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases" This reverts commit 6b7c37a5bf7871bee93a2b2640bbc6ef591f14db. * Revert "Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992)" This reverts commit 78e1f748e55383baecd123d06457111e18f13365. * Revert "Delivery API: Content routing and structure (#13984)" This reverts commit a0292ae5350362dd6c1c5bc9763deda928c78a75. * Revert "Fix multi URL picker value converter trying to access disposed objects in edge cases" This reverts commit 6b7c37a5bf7871bee93a2b2640bbc6ef591f14db. * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Test commit * Refactored interfaces for the query handlers and for the selectors (that will handle the value of the fetch query option) * Implemented a base class for the query options * Refactored the names of the selectors and made use of the base class * Refactored the ApiQueryService * Refactored the QueryContentApiController.cs * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Fixing merge gone wrong * Fix multi URL picker value converter trying to access disposed objects in edge cases * Delivery API: Content routing and structure (#13984) * Introduce content route instead of content path, rename and rework start item (previously start node) handling * Strip out start node path in generated route path * Make the start-item header take precedence over the request domain * Conditionally enabling the delivery API + add protection and preview support + refactor all services to be singletons + ensure no-op implementations for all required services (#13992) * Make fetching work with the new setup * Moving files to dedicated folders * Removing ? for array * Rename selector query method * Implement FilterHandler and some filters * Implement SortHandler and sort some sorts * Refactoring * Adding more fields to index due to querying * Appending filtering and sorting queries * Implementing a new ISelectorHandler without Examine types * Re-implementing the collection to have a dedicated one for the selectors * Implementing a new IFilterHandler without Examine types & refactoring the filters implementing it * Adding a new collection dedicated to filters * Renaming the old collection * Implementing a new ISortHandler without Examine types & refactoring the sorts implementing it * Adding a new collection for the sorts & adding all collections to UmbracoBuilder.Collections * Refactoring the service to use the new collections and types * Refactoring the fields in ContentApiFieldDefinitionCollection * Remove nullability in Handlers * Don't return null for selector * Add TODO for having the filters support negation * Changing the SortType to FieldType with our custom types on the SortOption * Fix AncestorsSelector * Fix ApiQueryService * Documentation * Fix Swagger docs * Refactor the QueryContentApiController * Adding handling for the IApiContentResponse in the JsonTypeResolver * Refactor the service to use a safe fallback value in Examine queries * Adding Noop for the IApiQueryService * Cleanup * Remove comment * Fix name field for indexing * Don't inherit QueryOptionBase in filters * Fix casing for API index constant + swap FIXME with TODO * Add TODO for handling missing fetch with start-item header * Rename query handler parameters to not leak source (i.e. query string) --------- Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: Elitsa <> Co-authored-by: Zeegaan <nge@umbraco.dk> * Delivery API: Adding pagination to query endpoint (#14083) * Adding pagination to query endpoint * Optimize the paging using Examine directly * Fix comment * Remove skip/take code duplication --------- Co-authored-by: kjac <kja@umbraco.dk> * Add missing CompatibilitySuppressions.xml * Make Delivery API packable * Make Api.Common packable * Renamed extension method and namespace so it is discoverable * Untangle ApiVersion configuration into api.common, so delivery api do not require the management api to boot. * configure options in management api * RTE output as JSON for Content API (#14067) * Conditionally serve RTE output as JSON instead of HTML * Fixed merge * Rename to Delivery API (#14119) * Rename ContentApi to DeliveryApi * Rename delivery API index implementation * Update comments from "Content API" to "Delivery API" * Rename project from Content to Delivery * Add dedicated controller base for content delivery API * Rename delivery API content index to include "content" specifically * Fix compat suppressions --------- Co-authored-by: kjac <kja@umbraco.dk> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Zeegaan <nge@umbraco.dk>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureApiExplorerOptions : IConfigureOptions<ApiExplorerOptions>
|
||||
{
|
||||
private readonly IOptions<ApiVersioningOptions> _apiVersioningOptions;
|
||||
|
||||
public ConfigureApiExplorerOptions(IOptions<ApiVersioningOptions> apiVersioningOptions)
|
||||
{
|
||||
_apiVersioningOptions = apiVersioningOptions;
|
||||
}
|
||||
|
||||
public void Configure(ApiExplorerOptions options)
|
||||
{
|
||||
options.DefaultApiVersion = _apiVersioningOptions.Value.DefaultApiVersion;
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
options.AddApiVersionParametersWhenVersionNeutral = true;
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Versioning;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.Configuration;
|
||||
|
||||
public class ConfigureApiVersioningOptions : IConfigureOptions<ApiVersioningOptions>
|
||||
{
|
||||
public void Configure(ApiVersioningOptions options)
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.ReportApiVersions = true;
|
||||
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.UseApiBehavior = false;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,12 @@ public class ConfigureMvcOptions : IConfigureOptions<MvcOptions>
|
||||
|
||||
public void Configure(MvcOptions options)
|
||||
{
|
||||
// these MVC options may be applied more than once; let's make sure we only execute once.
|
||||
if (options.Conventions.Any(convention => convention is UmbracoBackofficeToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Replace the BackOfficeToken in routes.
|
||||
|
||||
var backofficePath = _globalSettings.Value.UmbracoPath.TrimStart(Constants.CharArrays.TildeForwardSlash);
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
<PropertyGroup>
|
||||
<Title>Umbraco CMS - API Common</Title>
|
||||
<Description>Contains the bits and pieces that are shared between the Umbraco CMS APIs.</Description>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsPackable>true</IsPackable>
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
<AssemblyName>Umbraco.Cms.Api.Common</AssemblyName>
|
||||
<RootNamespace>Umbraco.Cms.Api.Common</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" />
|
||||
<!-- <PackageReference Include="Microsoft.AspNetCore.App" Version="2.2.8" />-->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj" />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
public class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
|
||||
{
|
||||
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
public class RequestContextRequestStartItemProviderAccessor : RequestContextServiceAccessorBase<IRequestStartItemProvider>, IRequestStartItemProviderAccessor
|
||||
{
|
||||
public RequestContextRequestStartItemProviderAccessor(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Accessors;
|
||||
|
||||
public abstract class RequestContextServiceAccessorBase<T>
|
||||
where T : class
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
|
||||
=> _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
|
||||
{
|
||||
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
|
||||
return requestStartNodeService is not null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
public class ByIdContentApiController : ContentApiControllerBase
|
||||
{
|
||||
public ByIdContentApiController(IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilderBuilder)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by id.
|
||||
/// </summary>
|
||||
/// <param name="id">The unique identifier of the content item.</param>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{id:guid}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
{
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetById(id);
|
||||
|
||||
if (contentItem is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await Task.FromResult(Ok(ApiContentResponseBuilder.Build(contentItem)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
public class ByRouteContentApiController : ContentApiControllerBase
|
||||
{
|
||||
private readonly IRequestRoutingService _requestRoutingService;
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
_requestRoutingService = requestRoutingService;
|
||||
_requestRedirectService = requestRedirectService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by route.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the content item.</param>
|
||||
/// <remarks>
|
||||
/// Optional URL segment for the root content item
|
||||
/// can be added through the "start-item" header.
|
||||
/// </remarks>
|
||||
/// <returns>The content item or not found result.</returns>
|
||||
[HttpGet("item/{*path}")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByRoute(string path = "/")
|
||||
{
|
||||
var contentRoute = _requestRoutingService.GetContentRoute(path);
|
||||
|
||||
IPublishedContent? contentItem = ApiPublishedContentCache.GetByRoute(contentRoute);
|
||||
if (contentItem is not null)
|
||||
{
|
||||
return await Task.FromResult(Ok(ApiContentResponseBuilder.Build(contentItem)));
|
||||
}
|
||||
|
||||
IApiContentRoute? redirectRoute = _requestRedirectService.GetRedirectRoute(path);
|
||||
return redirectRoute != null
|
||||
? RedirectTo(redirectRoute)
|
||||
: NotFound();
|
||||
}
|
||||
|
||||
private IActionResult RedirectTo(IApiContentRoute redirectRoute)
|
||||
{
|
||||
Response.Headers.Add("Location-Start-Item-Path", redirectRoute.StartItem.Path);
|
||||
Response.Headers.Add("Location-Start-Item-Id", redirectRoute.StartItem.Id.ToString("D"));
|
||||
return RedirectPermanent(redirectRoute.Path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[VersionedDeliveryApiRoute("content")]
|
||||
[ApiExplorerSettings(GroupName = "Content")]
|
||||
public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
{
|
||||
protected IApiPublishedContentCache ApiPublishedContentCache { get; }
|
||||
|
||||
protected IApiContentResponseBuilder ApiContentResponseBuilder { get; }
|
||||
|
||||
protected ContentApiControllerBase(IApiPublishedContentCache apiPublishedContentCache, IApiContentResponseBuilder apiContentResponseBuilder)
|
||||
{
|
||||
ApiPublishedContentCache = apiPublishedContentCache;
|
||||
ApiContentResponseBuilder = apiContentResponseBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Core;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[DeliveryApiAccess]
|
||||
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
|
||||
[LocalizeFromAcceptLanguageHeader]
|
||||
public abstract class DeliveryApiControllerBase : Controller
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
public class QueryContentApiController : ContentApiControllerBase
|
||||
{
|
||||
private readonly IApiContentQueryService _apiContentQueryService;
|
||||
|
||||
public QueryContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
|
||||
IApiContentQueryService apiContentQueryService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
|
||||
=> _apiContentQueryService = apiContentQueryService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of content 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 content item(s).</returns>
|
||||
[HttpGet]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedViewModel<IApiContentResponse>>> Query(
|
||||
string? fetch,
|
||||
[FromQuery] string[] filter,
|
||||
[FromQuery] string[] sort,
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
{
|
||||
PagedModel<Guid> pagedResult = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(pagedResult.Items);
|
||||
IApiContentResponse[] apiContentItems = contentItems.Select(ApiContentResponseBuilder.Build).ToArray();
|
||||
|
||||
var model = new PagedViewModel<IApiContentResponse>
|
||||
{
|
||||
Total = pagedResult.Total,
|
||||
Items = apiContentItems
|
||||
};
|
||||
|
||||
return await Task.FromResult(Ok(model));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.Configuration;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
using Umbraco.Cms.Api.Delivery.Json;
|
||||
using Umbraco.Cms.Api.Delivery.Rendering;
|
||||
using Umbraco.Cms.Api.Delivery.Services;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class UmbracoBuilderExtensions
|
||||
{
|
||||
public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
|
||||
builder.Services.AddScoped<IOutputExpansionStrategy, RequestContextOutputExpansionStrategy>();
|
||||
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
|
||||
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
|
||||
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
|
||||
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
|
||||
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
|
||||
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
|
||||
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureApiVersioningOptions>();
|
||||
builder.Services.AddApiVersioning();
|
||||
builder.Services.ConfigureOptions<ConfigureApiExplorerOptions>();
|
||||
builder.Services.AddVersionedApiExplorer();
|
||||
|
||||
builder
|
||||
.Services
|
||||
.ConfigureOptions<ConfigureMvcOptions>()
|
||||
.AddControllers()
|
||||
.AddJsonOptions(Constants.JsonOptionsNames.DeliveryApi, options =>
|
||||
{
|
||||
// all Delivery API specific JSON options go here
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||
options.JsonSerializerOptions.TypeInfoResolver = new DeliveryApiJsonTypeResolver();
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public class DeliveryApiAccessAttribute : TypeFilterAttribute
|
||||
{
|
||||
public DeliveryApiAccessAttribute()
|
||||
: base(typeof(DeliveryApiAccessFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class DeliveryApiAccessFilter : IActionFilter
|
||||
{
|
||||
private readonly IApiAccessService _apiAccessService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
|
||||
public DeliveryApiAccessFilter(IApiAccessService apiAccessService, IRequestPreviewService requestPreviewService)
|
||||
{
|
||||
_apiAccessService = apiAccessService;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var hasAccess = _requestPreviewService.IsPreview()
|
||||
? _apiAccessService.HasPreviewAccess()
|
||||
: _apiAccessService.HasPublicAccess();
|
||||
|
||||
if (hasAccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
public class LocalizeFromAcceptLanguageHeaderAttribute : TypeFilterAttribute
|
||||
{
|
||||
public LocalizeFromAcceptLanguageHeaderAttribute()
|
||||
: base(typeof(LocalizeFromAcceptLanguageHeaderAttributeFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private class LocalizeFromAcceptLanguageHeaderAttributeFilter : IActionFilter
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
|
||||
public LocalizeFromAcceptLanguageHeaderAttributeFilter(IRequestCultureService requestCultureService)
|
||||
=> _requestCultureService = requestCultureService;
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
var requestedCulture = _requestCultureService.GetRequestedCulture();
|
||||
if (requestedCulture.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_requestCultureService.SetRequestCulture(requestedCulture);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Json;
|
||||
|
||||
// see https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0
|
||||
// TODO: if this type resolver is to be used for extendable content models (custom IApiContent implementations) we need to work out an extension model for known derived types
|
||||
public class DeliveryApiJsonTypeResolver : DefaultJsonTypeInfoResolver
|
||||
{
|
||||
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||
{
|
||||
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
|
||||
|
||||
if (jsonTypeInfo.Type == typeof(IApiContent))
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContent));
|
||||
}
|
||||
else if (jsonTypeInfo.Type == typeof(IApiContentResponse))
|
||||
{
|
||||
ConfigureJsonPolymorphismOptions(jsonTypeInfo, typeof(ApiContentResponse));
|
||||
}
|
||||
|
||||
return jsonTypeInfo;
|
||||
}
|
||||
|
||||
private void ConfigureJsonPolymorphismOptions(JsonTypeInfo jsonTypeInfo, Type derivedType)
|
||||
=> jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||
{
|
||||
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
|
||||
DerivedTypes = { new JsonDerivedType(derivedType) }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
|
||||
|
||||
internal sealed class ContentTypeFilter : IFilterHandler
|
||||
{
|
||||
private const string ContentTypeSpecifier = "contentType:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(ContentTypeSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FilterOption BuildFilterOption(string filter)
|
||||
{
|
||||
var alias = filter.Substring(ContentTypeSpecifier.Length);
|
||||
|
||||
var filterOption = new FilterOption
|
||||
{
|
||||
FieldName = "__NodeTypeAlias",
|
||||
Value = string.Empty
|
||||
};
|
||||
|
||||
// TODO: do we support negation?
|
||||
if (alias.StartsWith('!'))
|
||||
{
|
||||
filterOption.Value = alias.Substring(1);
|
||||
filterOption.Operator = FilterOperation.IsNot;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterOption.Value = alias;
|
||||
filterOption.Operator = FilterOperation.Is;
|
||||
}
|
||||
|
||||
return filterOption;
|
||||
}
|
||||
}
|
||||
38
src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs
Normal file
38
src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Filters;
|
||||
|
||||
internal sealed class NameFilter : IFilterHandler
|
||||
{
|
||||
private const string NameSpecifier = "name:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(NameSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FilterOption BuildFilterOption(string filter)
|
||||
{
|
||||
var value = filter.Substring(NameSpecifier.Length);
|
||||
|
||||
var filterOption = new FilterOption
|
||||
{
|
||||
FieldName = "name",
|
||||
Value = string.Empty
|
||||
};
|
||||
|
||||
// TODO: do we support negation?
|
||||
if (value.StartsWith('!'))
|
||||
{
|
||||
filterOption.Value = value.Substring(1);
|
||||
filterOption.Operator = FilterOperation.IsNot;
|
||||
}
|
||||
else
|
||||
{
|
||||
filterOption.Value = value;
|
||||
filterOption.Operator = FilterOperation.Is;
|
||||
}
|
||||
|
||||
return filterOption;
|
||||
}
|
||||
}
|
||||
39
src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs
Normal file
39
src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying;
|
||||
|
||||
public abstract class QueryOptionBase
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestRoutingService _requestRoutingService;
|
||||
|
||||
public QueryOptionBase(IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRequestRoutingService requestRoutingService)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestRoutingService = requestRoutingService;
|
||||
}
|
||||
|
||||
public Guid? GetGuidFromQuery(string queryStringValue)
|
||||
{
|
||||
if (Guid.TryParse(queryStringValue, out Guid id))
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
if (!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) ||
|
||||
publishedSnapshot?.Content is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the passed value is a path of a content item
|
||||
var contentRoute = _requestRoutingService.GetContentRoute(queryStringValue);
|
||||
IPublishedContent? contentItem = publishedSnapshot.Content.GetByRoute(contentRoute);
|
||||
|
||||
return contentItem?.Key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
|
||||
|
||||
internal sealed class AncestorsSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string AncestorsSpecifier = "ancestors:";
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
|
||||
public AncestorsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService) =>
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(AncestorsSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SelectorOption BuildSelectorOption(string selector)
|
||||
{
|
||||
var fieldValue = selector.Substring(AncestorsSpecifier.Length);
|
||||
Guid? id = GetGuidFromQuery(fieldValue);
|
||||
|
||||
if (id is null ||
|
||||
!_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) ||
|
||||
publishedSnapshot?.Content is null)
|
||||
{
|
||||
// Setting the Value to "" since that would yield no results.
|
||||
// It won't be appropriate to return null here since if we reached this,
|
||||
// it means that CanHandle() returned true, meaning that this Selector should be able to handle the selector value
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = "id",
|
||||
Value = string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
// With the previous check we made sure that if we reach this, we already made sure that there is a valid content item
|
||||
IPublishedContent contentItem = publishedSnapshot.Content.GetById((Guid)id)!; // so it can't be null
|
||||
IEnumerable<Guid> ancestorKeys = contentItem.Ancestors().Select(a => a.Key);
|
||||
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = "id",
|
||||
Value = string.Join(" ", ancestorKeys)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
|
||||
|
||||
internal sealed class ChildrenSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string ChildrenSpecifier = "children:";
|
||||
|
||||
public ChildrenSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(ChildrenSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SelectorOption BuildSelectorOption(string selector)
|
||||
{
|
||||
var fieldValue = selector.Substring(ChildrenSpecifier.Length);
|
||||
Guid? id = GetGuidFromQuery(fieldValue);
|
||||
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = "parentKey",
|
||||
Value = id.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Selectors;
|
||||
|
||||
internal sealed class DescendantsSelector : QueryOptionBase, ISelectorHandler
|
||||
{
|
||||
private const string DescendantsSpecifier = "descendants:";
|
||||
|
||||
public DescendantsSelector(IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IRequestRoutingService requestRoutingService)
|
||||
: base(publishedSnapshotAccessor, requestRoutingService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(DescendantsSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SelectorOption BuildSelectorOption(string selector)
|
||||
{
|
||||
var fieldValue = selector.Substring(DescendantsSpecifier.Length);
|
||||
Guid? id = GetGuidFromQuery(fieldValue);
|
||||
|
||||
return new SelectorOption
|
||||
{
|
||||
FieldName = "ancestorKeys",
|
||||
Value = id.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/LevelSort.cs
Normal file
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/LevelSort.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
|
||||
|
||||
internal sealed class LevelSort : ISortHandler
|
||||
{
|
||||
private const string SortOptionSpecifier = "level:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SortOption BuildSortOption(string sort)
|
||||
{
|
||||
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
|
||||
|
||||
return new SortOption
|
||||
{
|
||||
FieldName = "level",
|
||||
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
|
||||
FieldType = FieldType.Number
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/NameSort.cs
Normal file
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/NameSort.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
|
||||
|
||||
internal sealed class NameSort : ISortHandler
|
||||
{
|
||||
private const string SortOptionSpecifier = "name:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SortOption BuildSortOption(string sort)
|
||||
{
|
||||
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
|
||||
|
||||
return new SortOption
|
||||
{
|
||||
FieldName = "name",
|
||||
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
|
||||
FieldType = FieldType.String
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/PathSort.cs
Normal file
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/PathSort.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
|
||||
|
||||
internal sealed class PathSort : ISortHandler
|
||||
{
|
||||
private const string SortOptionSpecifier = "path:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SortOption BuildSortOption(string sort)
|
||||
{
|
||||
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
|
||||
|
||||
return new SortOption
|
||||
{
|
||||
FieldName = "path",
|
||||
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
|
||||
FieldType = FieldType.String
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/SortOrderSort.cs
Normal file
26
src/Umbraco.Cms.Api.Delivery/Querying/Sorts/SortOrderSort.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Querying.Sorts;
|
||||
|
||||
internal sealed class SortOrderSort : ISortHandler
|
||||
{
|
||||
private const string SortOptionSpecifier = "sortOrder:";
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanHandle(string query)
|
||||
=> query.StartsWith(SortOptionSpecifier, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public SortOption BuildSortOption(string sort)
|
||||
{
|
||||
var sortDirection = sort.Substring(SortOptionSpecifier.Length);
|
||||
|
||||
return new SortOption
|
||||
{
|
||||
FieldName = "sortOrder",
|
||||
Direction = sortDirection.StartsWith("asc") ? Direction.Ascending : Direction.Descending,
|
||||
FieldType = FieldType.Number
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Rendering;
|
||||
|
||||
internal sealed class RequestContextOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
{
|
||||
private readonly bool _expandAll;
|
||||
private readonly string[] _expandAliases;
|
||||
|
||||
private ExpansionState _state;
|
||||
|
||||
public RequestContextOutputExpansionStrategy(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
(bool ExpandAll, string[] ExpanedAliases) initialState = InitialRequestState(httpContextAccessor);
|
||||
_expandAll = initialState.ExpandAll;
|
||||
_expandAliases = initialState.ExpanedAliases;
|
||||
_state = ExpansionState.Initial;
|
||||
}
|
||||
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties);
|
||||
|
||||
public IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties)
|
||||
=> properties.ToDictionary(
|
||||
p => p.Alias,
|
||||
p => p.GetDeliveryApiValue(_state == ExpansionState.Expanding));
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
{
|
||||
// in the initial state, content properties should always be rendered (expanded if the requests dictates it).
|
||||
// this corresponds to the root level of a content item, i.e. when the initial content rendering starts.
|
||||
if (_state == ExpansionState.Initial)
|
||||
{
|
||||
// update state to pending so we don't end up here the next time around
|
||||
_state = ExpansionState.Pending;
|
||||
var rendered = content.Properties.ToDictionary(
|
||||
property => property.Alias,
|
||||
property =>
|
||||
{
|
||||
// update state to expanding if the property should be expanded (needed for nested elements)
|
||||
if (_expandAll || _expandAliases.Contains(property.Alias))
|
||||
{
|
||||
_state = ExpansionState.Expanding;
|
||||
}
|
||||
|
||||
var value = property.GetDeliveryApiValue(_state == ExpansionState.Expanding);
|
||||
|
||||
// always revert to pending after rendering the property value
|
||||
_state = ExpansionState.Pending;
|
||||
return value;
|
||||
});
|
||||
_state = ExpansionState.Initial;
|
||||
return rendered;
|
||||
}
|
||||
|
||||
// in an expanding state, properties should always be rendered as collapsed.
|
||||
// this corresponds to properties of a content based property placed directly below a root level property that is being expanded
|
||||
// (i.e. properties for picked content for an expanded content picker at root level).
|
||||
if (_state == ExpansionState.Expanding)
|
||||
{
|
||||
_state = ExpansionState.Expanded;
|
||||
var rendered = content.Properties.ToDictionary(
|
||||
property => property.Alias,
|
||||
property => property.GetDeliveryApiValue(false));
|
||||
_state = ExpansionState.Expanding;
|
||||
return rendered;
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
private (bool ExpandAll, string[] ExpanedAliases) InitialRequestState(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
string? toExpand = httpContextAccessor.HttpContext?.Request.Query["expand"];
|
||||
if (toExpand.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new(false, Array.Empty<string>());
|
||||
}
|
||||
|
||||
const string propertySpecifier = "property:";
|
||||
return new(
|
||||
toExpand == "all",
|
||||
toExpand.StartsWith(propertySpecifier)
|
||||
? toExpand.Substring(propertySpecifier.Length).Split(Constants.CharArrays.Comma)
|
||||
: Array.Empty<string>());
|
||||
}
|
||||
|
||||
private enum ExpansionState
|
||||
{
|
||||
Initial,
|
||||
Pending,
|
||||
Expanding,
|
||||
Expanded
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Api.Common.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Routing;
|
||||
|
||||
public class VersionedDeliveryApiRouteAttribute : BackOfficeRouteAttribute
|
||||
{
|
||||
public VersionedDeliveryApiRouteAttribute(string template)
|
||||
: base($"delivery/api/v{{version:apiVersion}}/{template.TrimStart('/')}")
|
||||
{
|
||||
}
|
||||
}
|
||||
30
src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs
Normal file
30
src/Umbraco.Cms.Api.Delivery/Services/ApiAccessService.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
|
||||
{
|
||||
private DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
public ApiAccessService(IHttpContextAccessor httpContextAccessor, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
|
||||
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPublicAccess() => IfEnabled(() => _deliveryApiSettings.PublicAccess || HasValidApiKey());
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPreviewAccess() => IfEnabled(HasValidApiKey);
|
||||
|
||||
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
|
||||
|
||||
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
|
||||
}
|
||||
182
src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs
Normal file
182
src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Examine;
|
||||
using Examine.Search;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class ApiContentQueryService : IApiContentQueryService // Examine-specific implementation - can be swapped out
|
||||
{
|
||||
private readonly IExamineManager _examineManager;
|
||||
private readonly SelectorHandlerCollection _selectorHandlers;
|
||||
private readonly FilterHandlerCollection _filterHandlers;
|
||||
private readonly SortHandlerCollection _sortHandlers;
|
||||
private readonly string _fallbackGuidValue;
|
||||
|
||||
public ApiContentQueryService(
|
||||
IExamineManager examineManager,
|
||||
SelectorHandlerCollection selectorHandlers,
|
||||
FilterHandlerCollection filterHandlers,
|
||||
SortHandlerCollection sortHandlers)
|
||||
{
|
||||
_examineManager = examineManager;
|
||||
_selectorHandlers = selectorHandlers;
|
||||
_filterHandlers = filterHandlers;
|
||||
_sortHandlers = sortHandlers;
|
||||
|
||||
// A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
|
||||
// It is set to a random guid since this would be highly unlikely to yield any results
|
||||
_fallbackGuidValue = Guid.NewGuid().ToString("D");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
{
|
||||
var emptyResult = new PagedModel<Guid>();
|
||||
|
||||
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.DeliveryApiContentIndexName, out IIndex? apiIndex))
|
||||
{
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
IQuery baseQuery = apiIndex.Searcher.CreateQuery();
|
||||
|
||||
// Handle Selecting
|
||||
IBooleanOperation? queryOperation = HandleSelector(fetch, baseQuery);
|
||||
|
||||
// If no Selector could be found, we return no results
|
||||
if (queryOperation is null)
|
||||
{
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// Handle Filtering
|
||||
HandleFiltering(filters, queryOperation);
|
||||
|
||||
// Handle Sorting
|
||||
IOrdering? sortQuery = HandleSorting(sorts, queryOperation);
|
||||
|
||||
ISearchResults? results = (sortQuery ?? DefaultSort(queryOperation))?.Execute(QueryOptions.SkipTake(skip, take));
|
||||
|
||||
if (results is null)
|
||||
{
|
||||
return emptyResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
Guid[] items = results.Select(x => Guid.Parse(x.Id)).ToArray();
|
||||
return new PagedModel<Guid>(results.TotalItemCount, items);
|
||||
}
|
||||
}
|
||||
|
||||
private IBooleanOperation? HandleSelector(string? fetch, IQuery baseQuery)
|
||||
{
|
||||
IBooleanOperation? queryOperation;
|
||||
|
||||
if (fetch is not null)
|
||||
{
|
||||
ISelectorHandler? selectorHandler = _selectorHandlers.FirstOrDefault(h => h.CanHandle(fetch));
|
||||
SelectorOption? selector = selectorHandler?.BuildSelectorOption(fetch);
|
||||
|
||||
if (selector is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var value = string.IsNullOrWhiteSpace(selector.Value) == false
|
||||
? selector.Value
|
||||
: _fallbackGuidValue;
|
||||
queryOperation = baseQuery.Field(selector.FieldName, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: If no params or no fetch value, get everything from the index - make a default selector and register it by the end of the collection
|
||||
// TODO: This selects everything without regard to the current start-item header - make sure we honour that if it is present
|
||||
// This is a temp Examine solution
|
||||
queryOperation = baseQuery.Field("__IndexType", "content");
|
||||
}
|
||||
|
||||
return queryOperation;
|
||||
}
|
||||
|
||||
private void HandleFiltering(IEnumerable<string> filters, IBooleanOperation queryOperation)
|
||||
{
|
||||
foreach (var filterValue in filters)
|
||||
{
|
||||
IFilterHandler? filterHandler = _filterHandlers.FirstOrDefault(h => h.CanHandle(filterValue));
|
||||
FilterOption? filter = filterHandler?.BuildFilterOption(filterValue);
|
||||
|
||||
if (filter is not null)
|
||||
{
|
||||
var value = string.IsNullOrWhiteSpace(filter.Value) == false
|
||||
? filter.Value
|
||||
: _fallbackGuidValue;
|
||||
|
||||
switch (filter.Operator)
|
||||
{
|
||||
case FilterOperation.Is:
|
||||
queryOperation.And().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
case FilterOperation.IsNot:
|
||||
queryOperation.Not().Field(filter.FieldName,
|
||||
(IExamineValue)new ExamineValue(Examineness.Explicit,
|
||||
value)); // TODO: doesn't work for explicit word(s) match
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.Contains:
|
||||
break;
|
||||
// TODO: Fix
|
||||
case FilterOperation.DoesNotContain:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IOrdering? HandleSorting(IEnumerable<string> sorts, IBooleanOperation queryCriteria)
|
||||
{
|
||||
IOrdering? orderingQuery = null;
|
||||
|
||||
foreach (var sortValue in sorts)
|
||||
{
|
||||
ISortHandler? sortHandler = _sortHandlers.FirstOrDefault(h => h.CanHandle(sortValue));
|
||||
SortOption? sort = sortHandler?.BuildSortOption(sortValue);
|
||||
|
||||
if (sort is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
SortType sortType = sort.FieldType switch
|
||||
{
|
||||
FieldType.Number => // TODO: do we need more explicit types like float, long, double
|
||||
SortType.Int,
|
||||
FieldType.Date =>
|
||||
// The field definition type should be FieldDefinitionTypes.DateTime
|
||||
SortType.Long,
|
||||
_ => SortType.String
|
||||
};
|
||||
|
||||
orderingQuery = sort.Direction switch
|
||||
{
|
||||
Direction.Ascending => queryCriteria.OrderBy(new SortableField(sort.FieldName, sortType)),
|
||||
Direction.Descending => queryCriteria.OrderByDescending(new SortableField(sort.FieldName, sortType)),
|
||||
_ => orderingQuery
|
||||
};
|
||||
}
|
||||
|
||||
return orderingQuery;
|
||||
}
|
||||
|
||||
private IOrdering? DefaultSort(IBooleanOperation queryCriteria)
|
||||
{
|
||||
var defaultSorts = new[] { "path:asc", "sortOrder:asc" };
|
||||
|
||||
return HandleSorting(defaultSorts, queryCriteria);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestCultureService : RequestHeaderHandler, IRequestCultureService
|
||||
{
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
public RequestCultureService(IHttpContextAccessor httpContextAccessor, IVariationContextAccessor variationContextAccessor)
|
||||
: base(httpContextAccessor) =>
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? GetRequestedCulture() => GetHeaderValue(HeaderNames.AcceptLanguage);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRequestCulture(string culture)
|
||||
{
|
||||
if (_variationContextAccessor.VariationContext?.Culture == culture)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_variationContextAccessor.VariationContext = new VariationContext(culture);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal abstract class RequestHeaderHandler
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
protected RequestHeaderHandler(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
protected string? GetHeaderValue(string headerName)
|
||||
{
|
||||
HttpContext httpContext = _httpContextAccessor.HttpContext ??
|
||||
throw new InvalidOperationException("Could not obtain an HTTP context");
|
||||
|
||||
return httpContext.Request.Headers[headerName];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPreviewService
|
||||
{
|
||||
public RequestPreviewService(IHttpContextAccessor httpContextAccessor)
|
||||
: base(httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => GetHeaderValue("Preview") == "true";
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
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.Routing;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedirectService
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
private readonly IRedirectUrlService _redirectUrlService;
|
||||
private readonly IApiPublishedContentCache _apiPublishedContentCache;
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public RequestRedirectService(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
|
||||
IRequestCultureService requestCultureService,
|
||||
IRedirectUrlService redirectUrlService,
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentRouteBuilder apiContentRouteBuilder,
|
||||
IOptions<GlobalSettings> globalSettings)
|
||||
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor)
|
||||
{
|
||||
_requestCultureService = requestCultureService;
|
||||
_redirectUrlService = redirectUrlService;
|
||||
_apiPublishedContentCache = apiPublishedContentCache;
|
||||
_apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
public IApiContentRoute? GetRedirectRoute(string requestedPath)
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
// must append the root content url segment if it is not hidden by config, because
|
||||
// the URL tracking is based on the actual URL, including the root content url segment
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
{
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem?.UrlSegment != null)
|
||||
{
|
||||
requestedPath = $"{startItem.UrlSegment.EnsureStartsWith("/")}{requestedPath}";
|
||||
}
|
||||
}
|
||||
|
||||
var culture = _requestCultureService.GetRequestedCulture();
|
||||
|
||||
// append the configured domain content ID to the path if we have a domain bound request,
|
||||
// because URL tracking registers the tracked url like "{domain content ID}/{content path}"
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri != null)
|
||||
{
|
||||
requestedPath = GetContentRoute(domainAndUri, contentRoute);
|
||||
culture ??= domainAndUri.Culture;
|
||||
}
|
||||
|
||||
// important: redirect URLs are always tracked without trailing slashes
|
||||
IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture);
|
||||
IPublishedContent? content = redirectUrl != null
|
||||
? _apiPublishedContentCache.GetById(redirectUrl.ContentKey)
|
||||
: null;
|
||||
|
||||
return content != null
|
||||
? _apiContentRouteBuilder.Build(content)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestRoutingService : RoutingServiceBase, IRequestRoutingService
|
||||
{
|
||||
private readonly IRequestCultureService _requestCultureService;
|
||||
|
||||
public RequestRoutingService(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IRequestStartItemProviderAccessor requestStartItemProviderAccessor,
|
||||
IRequestCultureService requestCultureService)
|
||||
: base(publishedSnapshotAccessor, httpContextAccessor, requestStartItemProviderAccessor) =>
|
||||
_requestCultureService = requestCultureService;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetContentRoute(string requestedPath)
|
||||
{
|
||||
requestedPath = requestedPath.EnsureStartsWith("/");
|
||||
|
||||
// do we have an explicit start item?
|
||||
IPublishedContent? startItem = GetStartItem();
|
||||
if (startItem != null)
|
||||
{
|
||||
// the content cache can resolve content by the route "{root ID}/{content path}", which is what we construct here
|
||||
return $"{startItem.Id}{requestedPath}";
|
||||
}
|
||||
|
||||
// construct the (assumed) absolute URL for the requested content, and use that
|
||||
// to look for a domain configuration that would match the URL
|
||||
Uri contentRoute = GetDefaultRequestUri(requestedPath);
|
||||
DomainAndUri? domainAndUri = GetDomainAndUriForRoute(contentRoute);
|
||||
if (domainAndUri == null)
|
||||
{
|
||||
// no start item was found and no domain could be resolved, we will return the requested path
|
||||
// as route and hope the content cache can resolve that (it likely can)
|
||||
return requestedPath;
|
||||
}
|
||||
|
||||
// the Accept-Language header takes precedence over configured domain culture
|
||||
if (domainAndUri.Culture != null && _requestCultureService.GetRequestedCulture().IsNullOrWhiteSpace())
|
||||
{
|
||||
_requestCultureService.SetRequestCulture(domainAndUri.Culture);
|
||||
}
|
||||
|
||||
// when resolving content from a configured domain, the content cache expects the content route
|
||||
// to be "{domain content ID}/{content path}", which is what we construct here
|
||||
return GetContentRoute(domainAndUri, contentRoute);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
|
||||
// this provider lifetime is Scope, so we can cache this as a field
|
||||
private IPublishedContent? _requestedStartContent;
|
||||
|
||||
public RequestStartItemProvider(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
: base(httpContextAccessor) =>
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IPublishedContent? GetStartItem()
|
||||
{
|
||||
if (_requestedStartContent != null)
|
||||
{
|
||||
return _requestedStartContent;
|
||||
}
|
||||
|
||||
var headerValue = GetHeaderValue("Start-Item");
|
||||
if (headerValue.IsNullOrWhiteSpace())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false || publishedSnapshot?.Content == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IEnumerable<IPublishedContent> rootContent = publishedSnapshot.Content.GetAtRoot();
|
||||
|
||||
_requestedStartContent = Guid.TryParse(headerValue, out Guid key)
|
||||
? rootContent.FirstOrDefault(c => c.Key == key)
|
||||
: rootContent.FirstOrDefault(c => c.UrlSegment == headerValue);
|
||||
|
||||
return _requestedStartContent;
|
||||
}
|
||||
}
|
||||
63
src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs
Normal file
63
src/Umbraco.Cms.Api.Delivery/Services/RoutingServiceBase.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal abstract class RoutingServiceBase
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IRequestStartItemProviderAccessor _requestStartItemProviderAccessor;
|
||||
|
||||
protected RoutingServiceBase(
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IRequestStartItemProviderAccessor requestStartItemProviderAccessor)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_requestStartItemProviderAccessor = requestStartItemProviderAccessor;
|
||||
}
|
||||
|
||||
protected Uri GetDefaultRequestUri(string requestedPath)
|
||||
{
|
||||
HttpRequest? request = _httpContextAccessor.HttpContext?.Request;
|
||||
if (request == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not obtain an HTTP request context");
|
||||
}
|
||||
|
||||
// construct the (assumed) absolute URL for the requested content
|
||||
return new Uri($"{request.Scheme}://{request.Host}{requestedPath}", UriKind.Absolute);
|
||||
}
|
||||
|
||||
protected static string GetContentRoute(DomainAndUri domainAndUri, Uri contentRoute)
|
||||
=> $"{domainAndUri.ContentId}{DomainUtilities.PathRelativeToDomain(domainAndUri.Uri, contentRoute.AbsolutePath)}";
|
||||
|
||||
protected DomainAndUri? GetDomainAndUriForRoute(Uri contentUrl)
|
||||
{
|
||||
IDomainCache? domainCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Domains;
|
||||
if (domainCache == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not obtain the domain cache in the current context");
|
||||
}
|
||||
|
||||
IEnumerable<Domain> domains = domainCache.GetAll(false);
|
||||
|
||||
return DomainUtilities.SelectDomain(domains, contentUrl, defaultCulture: domainCache.DefaultCulture);
|
||||
}
|
||||
|
||||
protected IPublishedContent? GetStartItem()
|
||||
{
|
||||
if (_requestStartItemProviderAccessor.TryGetValue(out IRequestStartItemProvider? requestStartItemProvider) is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not obtain an {nameof(IRequestStartItemProvider)} instance");
|
||||
}
|
||||
|
||||
return requestStartItemProvider.GetStartItem();
|
||||
}
|
||||
}
|
||||
23
src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
Normal file
23
src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Title>Umbraco CMS - Delivery API</Title>
|
||||
<Description>Contains the presentation layer for the Umbraco CMS Delivery API.</Description>
|
||||
<IsPackable>true</IsPackable>
|
||||
<EnablePackageValidation>false</EnablePackageValidation>
|
||||
<AssemblyName>Umbraco.Cms.Api.Delivery</AssemblyName>
|
||||
<RootNamespace>Umbraco.Cms.Api.Delivery</RootNamespace>
|
||||
<PackageId>Umbraco.Cms.Api.Delivery</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Common\Umbraco.Cms.Api.Common.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Infrastructure\Umbraco.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.UnitTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -34,7 +34,7 @@ public class ManagementApiComposer : IComposer
|
||||
private const string ApiTitle = "Umbraco Backoffice API";
|
||||
private const string ApiDefaultDocumentName = "v1";
|
||||
|
||||
private ApiVersion DefaultApiVersion => new(1, 0);
|
||||
private ApiVersion DefaultApiVersion => new ApiVersion(1, 0);
|
||||
|
||||
public void Compose(IUmbracoBuilder builder)
|
||||
{
|
||||
@@ -53,14 +53,8 @@ public class ManagementApiComposer : IComposer
|
||||
.AddMappers()
|
||||
.AddBackOfficeAuthentication();
|
||||
|
||||
services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = DefaultApiVersion;
|
||||
options.ReportApiVersions = true;
|
||||
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.UseApiBehavior = false;
|
||||
});
|
||||
services.ConfigureOptions<ConfigureApiVersioningOptions>();
|
||||
services.AddApiVersioning();
|
||||
|
||||
services.AddSwaggerGen(swaggerGenOptions =>
|
||||
{
|
||||
@@ -163,14 +157,9 @@ public class ManagementApiComposer : IComposer
|
||||
swaggerGenOptions.CustomSchemaIds(SchemaIdGenerator.Generate);
|
||||
});
|
||||
|
||||
services.AddVersionedApiExplorer(options =>
|
||||
{
|
||||
options.DefaultApiVersion = DefaultApiVersion;
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
options.AddApiVersionParametersWhenVersionNeutral = true;
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
});
|
||||
|
||||
services.ConfigureOptions<ConfigureApiExplorerOptions>();
|
||||
services.AddVersionedApiExplorer();
|
||||
services.AddControllers()
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JsonPatch.Net" Version="2.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.2" />
|
||||
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.1.1" />
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Umbraco.Cms.Api.Delivery\Umbraco.Cms.Api.Delivery.csproj" />
|
||||
<ProjectReference Include="..\Umbraco.Cms.StaticAssets\Umbraco.Cms.StaticAssets.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
32
src/Umbraco.Core/CompatibilitySuppressions.xml
Normal file
32
src/Umbraco.Core/CompatibilitySuppressions.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
|
||||
<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0005</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Models.PublishedContent.PublishedPropertyBase.GetDeliveryApiValue(System.Boolean,System.String,System.String)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0006</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedProperty.GetDeliveryApiValue(System.Boolean,System.String,System.String)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0006</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.ConvertInterToDeliveryApiObject(Umbraco.Cms.Core.Models.PublishedContent.IPublishedElement,Umbraco.Cms.Core.PropertyEditors.PropertyCacheLevel,System.Object,System.Boolean)</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0006</DiagnosticId>
|
||||
<Target>P:Umbraco.Cms.Core.Models.PublishedContent.IPublishedPropertyType.DeliveryApiCacheLevel</Target>
|
||||
<Left>lib/net7.0/Umbraco.Core.dll</Left>
|
||||
<Right>lib/net7.0/Umbraco.Core.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
51
src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs
Normal file
51
src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for Delivery API settings.
|
||||
/// </summary>
|
||||
[UmbracoOptions(Constants.Configuration.ConfigDeliveryApi)]
|
||||
public class DeliveryApiSettings
|
||||
{
|
||||
private const bool StaticEnabled = false;
|
||||
|
||||
private const bool StaticPublicAccess = true;
|
||||
|
||||
private const bool StaticRichTextOutputAsJson = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Delivery API should be enabled.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Delivery API should be enabled; otherwise, <c>false</c>.</value>
|
||||
[DefaultValue(StaticEnabled)]
|
||||
public bool Enabled { get; set; } = StaticEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Delivery API (if enabled) should be
|
||||
/// publicly available or should require an API key for access.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if the Delivery API should be publicly available; <c>false</c> if an API key should be required for access.</value>
|
||||
[DefaultValue(StaticPublicAccess)]
|
||||
public bool PublicAccess { get; set; } = StaticPublicAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key used for authorizing API access (if the API is not publicly available) and preview access.
|
||||
/// </summary>
|
||||
/// <value>A <c>string</c> representing the API key.</value>
|
||||
public string? ApiKey { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aliases of the content types that may never be exposed through the Delivery API. Content of these
|
||||
/// types will never be returned from any Delivery API endpoint, nor added to the query index.
|
||||
/// </summary>
|
||||
/// <value>The content type aliases that are not to be exposed.</value>
|
||||
public string[] DisallowedContentTypeAliases { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the Delivery API should output rich text values as JSON instead of HTML.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
}
|
||||
@@ -30,6 +30,7 @@ public static partial class Constants
|
||||
public const string ConfigMarketplace = ConfigPrefix + "Marketplace";
|
||||
public const string ConfigLegacyPasswordMigration = ConfigPrefix + "LegacyPasswordMigration";
|
||||
public const string ConfigContent = ConfigPrefix + "Content";
|
||||
public const string ConfigDeliveryApi = ConfigPrefix + "DeliveryApi";
|
||||
public const string ConfigCoreDebug = ConfigCorePrefix + "Debug";
|
||||
public const string ConfigExceptionFilter = ConfigPrefix + "ExceptionFilter";
|
||||
public const string ConfigGlobal = ConfigPrefix + "Global";
|
||||
|
||||
@@ -7,5 +7,6 @@ public static partial class Constants
|
||||
public const string InternalIndexName = "InternalIndex";
|
||||
public const string ExternalIndexName = "ExternalIndex";
|
||||
public const string MembersIndexName = "MembersIndex";
|
||||
public const string DeliveryApiContentIndexName = "DeliveryApiContentIndex";
|
||||
}
|
||||
}
|
||||
|
||||
9
src/Umbraco.Core/Constants-JsonOptionsNames.cs
Normal file
9
src/Umbraco.Core/Constants-JsonOptionsNames.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class JsonOptionsNames
|
||||
{
|
||||
public const string DeliveryApi = "DeliveryApi";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi.Accessors;
|
||||
|
||||
public class NoopOutputExpansionStrategyAccessor : IOutputExpansionStrategyAccessor
|
||||
{
|
||||
public bool TryGetValue([NotNullWhen(true)] out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
{
|
||||
outputExpansionStrategy = new NoopOutputExpansionStrategy();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi.Accessors;
|
||||
|
||||
public sealed class NoopRequestStartItemProviderAccessor : IRequestStartItemProviderAccessor
|
||||
{
|
||||
public bool TryGetValue([NotNullWhen(true)] out IRequestStartItemProvider? requestStartItemProvider)
|
||||
{
|
||||
requestStartItemProvider = new NoopRequestStartItemProvider();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
15
src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs
Normal file
15
src/Umbraco.Core/DeliveryApi/ApiContentBuilder.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiContentBuilder : ApiContentBuilderBase<IApiContent>, IApiContentBuilder
|
||||
{
|
||||
public ApiContentBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IApiContent Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties)
|
||||
=> new ApiContent(id, name, contentType, route, properties);
|
||||
}
|
||||
37
src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs
Normal file
37
src/Umbraco.Core/DeliveryApi/ApiContentBuilderBase.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public abstract class ApiContentBuilderBase<T>
|
||||
where T : IApiContent
|
||||
{
|
||||
private readonly IApiContentNameProvider _apiContentNameProvider;
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
|
||||
|
||||
protected ApiContentBuilderBase(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
{
|
||||
_apiContentNameProvider = apiContentNameProvider;
|
||||
_apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
|
||||
}
|
||||
|
||||
protected abstract T Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties);
|
||||
|
||||
public virtual T Build(IPublishedContent content)
|
||||
{
|
||||
IDictionary<string, object?> properties =
|
||||
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapContentProperties(content)
|
||||
: new Dictionary<string, object?>();
|
||||
|
||||
return Create(
|
||||
content,
|
||||
content.Key,
|
||||
_apiContentNameProvider.GetName(content),
|
||||
content.ContentType.Alias,
|
||||
_apiContentRouteBuilder.Build(content),
|
||||
properties);
|
||||
}
|
||||
}
|
||||
8
src/Umbraco.Core/DeliveryApi/ApiContentNameProvider.cs
Normal file
8
src/Umbraco.Core/DeliveryApi/ApiContentNameProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiContentNameProvider : IApiContentNameProvider
|
||||
{
|
||||
public string GetName(IPublishedContent content) => content.Name;
|
||||
}
|
||||
25
src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs
Normal file
25
src/Umbraco.Core/DeliveryApi/ApiContentResponseBuilder.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiContentResponseBuilder : ApiContentBuilderBase<IApiContentResponse>, IApiContentResponseBuilder
|
||||
{
|
||||
private readonly IApiContentRouteBuilder _apiContentRouteBuilder;
|
||||
|
||||
public ApiContentResponseBuilder(IApiContentNameProvider apiContentNameProvider, IApiContentRouteBuilder apiContentRouteBuilder, IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
: base(apiContentNameProvider, apiContentRouteBuilder, outputExpansionStrategyAccessor)
|
||||
=> _apiContentRouteBuilder = apiContentRouteBuilder;
|
||||
|
||||
protected override IApiContentResponse Create(IPublishedContent content, Guid id, string name, string contentType, IApiContentRoute route, IDictionary<string, object?> properties)
|
||||
{
|
||||
var cultures = content.Cultures.Values
|
||||
.Where(publishedCultureInfo => publishedCultureInfo.Culture.IsNullOrWhiteSpace() == false) // filter out invariant cultures
|
||||
.ToDictionary(
|
||||
publishedCultureInfo => publishedCultureInfo.Culture,
|
||||
publishedCultureInfo => _apiContentRouteBuilder.Build(content, publishedCultureInfo.Culture));
|
||||
|
||||
return new ApiContentResponse(id, name, contentType, route, properties, cultures);
|
||||
}
|
||||
}
|
||||
42
src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs
Normal file
42
src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiContentRouteBuilder : IApiContentRouteBuilder
|
||||
{
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
|
||||
public ApiContentRouteBuilder(IPublishedUrlProvider publishedUrlProvider, IOptions<GlobalSettings> globalSettings, IVariationContextAccessor variationContextAccessor)
|
||||
{
|
||||
_publishedUrlProvider = publishedUrlProvider;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_globalSettings = globalSettings.Value;
|
||||
}
|
||||
|
||||
public IApiContentRoute Build(IPublishedContent content, string? culture = null)
|
||||
{
|
||||
if (content.ItemType != PublishedItemType.Content)
|
||||
{
|
||||
throw new ArgumentException("Content locations can only be built from Content items.", nameof(content));
|
||||
}
|
||||
|
||||
IPublishedContent root = content.Root();
|
||||
var rootPath = root.UrlSegment(_variationContextAccessor, culture) ?? string.Empty;
|
||||
|
||||
var contentPath = _publishedUrlProvider.GetUrl(content, UrlMode.Relative, culture).EnsureStartsWith("/");
|
||||
|
||||
if (_globalSettings.HideTopLevelNodeFromPath == false)
|
||||
{
|
||||
contentPath = contentPath.TrimStart(rootPath.EnsureStartsWith("/")).EnsureStartsWith("/");
|
||||
}
|
||||
|
||||
return new ApiContentRoute(contentPath, new ApiContentStartItem(root.Key, rootPath));
|
||||
}
|
||||
}
|
||||
27
src/Umbraco.Core/DeliveryApi/ApiElementBuilder.cs
Normal file
27
src/Umbraco.Core/DeliveryApi/ApiElementBuilder.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiElementBuilder : IApiElementBuilder
|
||||
{
|
||||
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
|
||||
|
||||
public ApiElementBuilder(IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
{
|
||||
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
|
||||
}
|
||||
|
||||
public IApiElement Build(IPublishedElement element)
|
||||
{
|
||||
IDictionary<string, object?> properties =
|
||||
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapElementProperties(element)
|
||||
: new Dictionary<string, object?>();
|
||||
|
||||
return new ApiElement(
|
||||
element.Key,
|
||||
element.ContentType.Alias,
|
||||
properties);
|
||||
}
|
||||
}
|
||||
35
src/Umbraco.Core/DeliveryApi/ApiMediaBuilder.cs
Normal file
35
src/Umbraco.Core/DeliveryApi/ApiMediaBuilder.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiMediaBuilder : IApiMediaBuilder
|
||||
{
|
||||
private readonly IApiContentNameProvider _apiContentNameProvider;
|
||||
private readonly IApiMediaUrlProvider _apiMediaUrlProvider;
|
||||
private readonly IOutputExpansionStrategyAccessor _outputExpansionStrategyAccessor;
|
||||
|
||||
public ApiMediaBuilder(
|
||||
IApiContentNameProvider apiContentNameProvider,
|
||||
IApiMediaUrlProvider apiMediaUrlProvider,
|
||||
IOutputExpansionStrategyAccessor outputExpansionStrategyAccessor)
|
||||
{
|
||||
_apiContentNameProvider = apiContentNameProvider;
|
||||
_apiMediaUrlProvider = apiMediaUrlProvider;
|
||||
_outputExpansionStrategyAccessor = outputExpansionStrategyAccessor;
|
||||
}
|
||||
|
||||
public IApiMedia Build(IPublishedContent media) =>
|
||||
new ApiMedia(
|
||||
media.Key,
|
||||
_apiContentNameProvider.GetName(media),
|
||||
media.ContentType.Alias,
|
||||
_apiMediaUrlProvider.GetUrl(media),
|
||||
Properties(media));
|
||||
|
||||
// map all media properties except the umbracoFile one, as we've already included the file URL etc. in the output
|
||||
private IDictionary<string, object?> Properties(IPublishedContent media) =>
|
||||
_outputExpansionStrategyAccessor.TryGetValue(out IOutputExpansionStrategy? outputExpansionStrategy)
|
||||
? outputExpansionStrategy.MapProperties(media.Properties.Where(p => p.Alias != Constants.Conventions.Media.File))
|
||||
: new Dictionary<string, object?>();
|
||||
}
|
||||
22
src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs
Normal file
22
src/Umbraco.Core/DeliveryApi/ApiMediaUrlProvider.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiMediaUrlProvider : IApiMediaUrlProvider
|
||||
{
|
||||
private readonly IPublishedUrlProvider _publishedUrlProvider;
|
||||
|
||||
public ApiMediaUrlProvider(IPublishedUrlProvider publishedUrlProvider)
|
||||
=> _publishedUrlProvider = publishedUrlProvider;
|
||||
|
||||
public string GetUrl(IPublishedContent media)
|
||||
{
|
||||
if (media.ItemType != PublishedItemType.Media)
|
||||
{
|
||||
throw new ArgumentException("Media URLs can only be generated from Media items.", nameof(media));
|
||||
}
|
||||
|
||||
return _publishedUrlProvider.GetMediaUrl(media, UrlMode.Relative);
|
||||
}
|
||||
}
|
||||
74
src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs
Normal file
74
src/Umbraco.Core/DeliveryApi/ApiPublishedContentCache.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class ApiPublishedContentCache : IApiPublishedContentCache
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
public ApiPublishedContentCache(IPublishedSnapshotAccessor publishedSnapshotAccessor, IRequestPreviewService requestPreviewService, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettings)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_deliveryApiSettings = deliveryApiSettings.CurrentValue;
|
||||
deliveryApiSettings.OnChange(settings => _deliveryApiSettings = settings);
|
||||
}
|
||||
|
||||
public IPublishedContent? GetByRoute(string route)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPublishedContent? content = contentCache.GetByRoute(_requestPreviewService.IsPreview(), route);
|
||||
return ContentOrNullIfDisallowed(content);
|
||||
}
|
||||
|
||||
public IPublishedContent? GetById(Guid contentId)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
IPublishedContent? content = contentCache.GetById(_requestPreviewService.IsPreview(), contentId);
|
||||
return ContentOrNullIfDisallowed(content);
|
||||
}
|
||||
|
||||
public IEnumerable<IPublishedContent> GetByIds(IEnumerable<Guid> contentIds)
|
||||
{
|
||||
IPublishedContentCache? contentCache = GetContentCache();
|
||||
if (contentCache == null)
|
||||
{
|
||||
return Enumerable.Empty<IPublishedContent>();
|
||||
}
|
||||
|
||||
return contentIds
|
||||
.Select(contentId => contentCache.GetById(_requestPreviewService.IsPreview(), contentId))
|
||||
.WhereNotNull()
|
||||
.Where(IsAllowedContentType)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IPublishedContentCache? GetContentCache() =>
|
||||
_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)
|
||||
? publishedSnapshot?.Content
|
||||
: null;
|
||||
|
||||
private IPublishedContent? ContentOrNullIfDisallowed(IPublishedContent? content)
|
||||
=> content != null && IsAllowedContentType(content)
|
||||
? content
|
||||
: null;
|
||||
|
||||
private bool IsAllowedContentType(IPublishedContent content)
|
||||
=> _deliveryApiSettings.DisallowedContentTypeAliases.InvariantContains(content.ContentType.Alias) is false;
|
||||
}
|
||||
11
src/Umbraco.Core/DeliveryApi/FilterHandlerCollection.cs
Normal file
11
src/Umbraco.Core/DeliveryApi/FilterHandlerCollection.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class FilterHandlerCollection : BuilderCollectionBase<IFilterHandler>
|
||||
{
|
||||
public FilterHandlerCollection(Func<IEnumerable<IFilterHandler>> items)
|
||||
: base(items)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class FilterHandlerCollectionBuilder
|
||||
: LazyCollectionBuilderBase<FilterHandlerCollectionBuilder, FilterHandlerCollection, IFilterHandler>
|
||||
{
|
||||
protected override FilterHandlerCollectionBuilder This => this;
|
||||
}
|
||||
19
src/Umbraco.Core/DeliveryApi/FilterOption.cs
Normal file
19
src/Umbraco.Core/DeliveryApi/FilterOption.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class FilterOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required string Value { get; set; }
|
||||
|
||||
public FilterOperation Operator { get; set; }
|
||||
}
|
||||
|
||||
public enum FilterOperation
|
||||
{
|
||||
Is,
|
||||
IsNot,
|
||||
// TODO: how to handle these in Examine?
|
||||
Contains,
|
||||
DoesNotContain
|
||||
}
|
||||
14
src/Umbraco.Core/DeliveryApi/IApiAccessService.cs
Normal file
14
src/Umbraco.Core/DeliveryApi/IApiAccessService.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiAccessService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves information on whether or not the API currently allows public access.
|
||||
/// </summary>
|
||||
bool HasPublicAccess();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves information on whether or not the API currently allows preview access.
|
||||
/// </summary>
|
||||
bool HasPreviewAccess();
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IApiContentBuilder.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IApiContentBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiContentBuilder
|
||||
{
|
||||
IApiContent Build(IPublishedContent content);
|
||||
}
|
||||
8
src/Umbraco.Core/DeliveryApi/IApiContentNameProvider.cs
Normal file
8
src/Umbraco.Core/DeliveryApi/IApiContentNameProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiContentNameProvider
|
||||
{
|
||||
string GetName(IPublishedContent content);
|
||||
}
|
||||
20
src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs
Normal file
20
src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// Service that handles querying of the Delivery API.
|
||||
/// </summary>
|
||||
public interface IApiContentQueryService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns a collection of item 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 item ids that are returned after applying the search queries.</returns>
|
||||
PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiContentResponseBuilder
|
||||
{
|
||||
IApiContentResponse Build(IPublishedContent content);
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IApiContentRouteBuilder.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IApiContentRouteBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiContentRouteBuilder
|
||||
{
|
||||
IApiContentRoute Build(IPublishedContent content, string? culture = null);
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IApiElementBuilder.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IApiElementBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiElementBuilder
|
||||
{
|
||||
IApiElement Build(IPublishedElement element);
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IApiMediaBuilder.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IApiMediaBuilder.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiMediaBuilder
|
||||
{
|
||||
IApiMedia Build(IPublishedContent media);
|
||||
}
|
||||
8
src/Umbraco.Core/DeliveryApi/IApiMediaUrlProvider.cs
Normal file
8
src/Umbraco.Core/DeliveryApi/IApiMediaUrlProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiMediaUrlProvider
|
||||
{
|
||||
string GetUrl(IPublishedContent media);
|
||||
}
|
||||
12
src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs
Normal file
12
src/Umbraco.Core/DeliveryApi/IApiPublishedContentCache.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IApiPublishedContentCache
|
||||
{
|
||||
IPublishedContent? GetByRoute(string route);
|
||||
|
||||
IPublishedContent? GetById(Guid contentId);
|
||||
|
||||
IEnumerable<IPublishedContent> GetByIds(IEnumerable<Guid> contentIds);
|
||||
}
|
||||
14
src/Umbraco.Core/DeliveryApi/IFilterHandler.cs
Normal file
14
src/Umbraco.Core/DeliveryApi/IFilterHandler.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// A handler that handles filter query parameters.
|
||||
/// </summary>
|
||||
public interface IFilterHandler : IQueryHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a <see cref="FilterOption"/> for the filter query.
|
||||
/// </summary>
|
||||
/// <param name="filter">The filter query (i.e. "contentType:article").</param>
|
||||
/// <returns>A <see cref="FilterOption"/> that can be used when building specific filter queries.</returns>
|
||||
FilterOption BuildFilterOption(string filter);
|
||||
}
|
||||
12
src/Umbraco.Core/DeliveryApi/IOutputExpansionStrategy.cs
Normal file
12
src/Umbraco.Core/DeliveryApi/IOutputExpansionStrategy.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IOutputExpansionStrategy
|
||||
{
|
||||
IDictionary<string, object?> MapElementProperties(IPublishedElement element);
|
||||
|
||||
IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties);
|
||||
|
||||
IDictionary<string, object?> MapContentProperties(IPublishedContent content);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IOutputExpansionStrategyAccessor
|
||||
{
|
||||
bool TryGetValue([NotNullWhen(true)] out IOutputExpansionStrategy? outputExpansionStrategy);
|
||||
}
|
||||
13
src/Umbraco.Core/DeliveryApi/IQueryHandler.cs
Normal file
13
src/Umbraco.Core/DeliveryApi/IQueryHandler.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IQueryHandler : IDiscoverable
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether this query handler can handle the given query.
|
||||
/// </summary>
|
||||
/// <param name="query">The query string to check (i.e. "children:articles", "contentType:article", "name:asc", ...).</param>
|
||||
/// <returns>True if this query handler can handle the given query; otherwise, false.</returns>
|
||||
bool CanHandle(string query);
|
||||
}
|
||||
15
src/Umbraco.Core/DeliveryApi/IRequestCultureService.cs
Normal file
15
src/Umbraco.Core/DeliveryApi/IRequestCultureService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestCultureService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the requested culture from the "Accept-Language" header, if present.
|
||||
/// </summary>
|
||||
string? GetRequestedCulture();
|
||||
|
||||
/// <summary>
|
||||
/// Updates the current request culture if applicable.
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture to use for the current request.</param>
|
||||
void SetRequestCulture(string culture);
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IRequestPreviewService.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IRequestPreviewService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestPreviewService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves information on whether or not to output draft content for preview.
|
||||
/// </summary>
|
||||
bool IsPreview();
|
||||
}
|
||||
11
src/Umbraco.Core/DeliveryApi/IRequestRedirectService.cs
Normal file
11
src/Umbraco.Core/DeliveryApi/IRequestRedirectService.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestRedirectService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the redirect URL (if any) for a requested content path
|
||||
/// </summary>
|
||||
IApiContentRoute? GetRedirectRoute(string requestedPath);
|
||||
}
|
||||
9
src/Umbraco.Core/DeliveryApi/IRequestRoutingService.cs
Normal file
9
src/Umbraco.Core/DeliveryApi/IRequestRoutingService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestRoutingService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the actual route for content in the content cache from a requested content path
|
||||
/// </summary>
|
||||
string GetContentRoute(string requestedPath);
|
||||
}
|
||||
11
src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs
Normal file
11
src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestStartItemProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the requested start item from the "Start-Item" header, if present.
|
||||
/// </summary>
|
||||
IPublishedContent? GetStartItem();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestStartItemProviderAccessor
|
||||
{
|
||||
bool TryGetValue([NotNullWhen(true)] out IRequestStartItemProvider? requestStartItemProvider);
|
||||
}
|
||||
14
src/Umbraco.Core/DeliveryApi/ISelectorHandler.cs
Normal file
14
src/Umbraco.Core/DeliveryApi/ISelectorHandler.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// A handler that handles fetch query parameter.
|
||||
/// </summary>
|
||||
public interface ISelectorHandler : IQueryHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a <see cref="SelectorOption"/> for the selector query.
|
||||
/// </summary>
|
||||
/// <param name="selector">The selector query (i.e. "children:articles").</param>
|
||||
/// <returns>A <see cref="SelectorOption"/> that can be used when building specific search query for requesting a subset of the items.</returns>
|
||||
SelectorOption BuildSelectorOption(string selector);
|
||||
}
|
||||
14
src/Umbraco.Core/DeliveryApi/ISortHandler.cs
Normal file
14
src/Umbraco.Core/DeliveryApi/ISortHandler.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
/// <summary>
|
||||
/// A handler that handles sort query parameters.
|
||||
/// </summary>
|
||||
public interface ISortHandler : IQueryHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a <see cref="SortOption"/> for the sort query.
|
||||
/// </summary>
|
||||
/// <param name="sort">The sort query (i.e. "name:asc").</param>
|
||||
/// <returns>A <see cref="SortOption"/> that can be used when building specific sorting queries.</returns>
|
||||
SortOption BuildSortOption(string sort);
|
||||
}
|
||||
10
src/Umbraco.Core/DeliveryApi/NoopApiAccessService.cs
Normal file
10
src/Umbraco.Core/DeliveryApi/NoopApiAccessService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopApiAccessService : IApiAccessService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool HasPublicAccess() => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool HasPreviewAccess() => false;
|
||||
}
|
||||
10
src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs
Normal file
10
src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Umbraco.New.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopApiContentQueryService : IApiContentQueryService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PagedModel<Guid> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
=> new();
|
||||
}
|
||||
15
src/Umbraco.Core/DeliveryApi/NoopOutputExpansionStrategy.cs
Normal file
15
src/Umbraco.Core/DeliveryApi/NoopOutputExpansionStrategy.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
internal sealed class NoopOutputExpansionStrategy : IOutputExpansionStrategy
|
||||
{
|
||||
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
|
||||
=> MapProperties(element.Properties);
|
||||
|
||||
public IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties)
|
||||
=> properties.ToDictionary(p => p.Alias, p => p.GetDeliveryApiValue(true));
|
||||
|
||||
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
|
||||
=> MapProperties(content.Properties);
|
||||
}
|
||||
12
src/Umbraco.Core/DeliveryApi/NoopRequestCultureService.cs
Normal file
12
src/Umbraco.Core/DeliveryApi/NoopRequestCultureService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopRequestCultureService : IRequestCultureService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string? GetRequestedCulture() => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRequestCulture(string culture)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopRequestPreviewService : IRequestPreviewService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopRequestRedirectService : IRequestRedirectService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IApiContentRoute? GetRedirectRoute(string requestedPath) => null;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopRequestRoutingService : IRequestRoutingService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string GetContentRoute(string requestedPath) => requestedPath;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
internal sealed class NoopRequestStartItemProvider : IRequestStartItemProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IPublishedContent? GetStartItem() => null;
|
||||
}
|
||||
11
src/Umbraco.Core/DeliveryApi/SelectorHandlerCollection.cs
Normal file
11
src/Umbraco.Core/DeliveryApi/SelectorHandlerCollection.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SelectorHandlerCollection : BuilderCollectionBase<ISelectorHandler>
|
||||
{
|
||||
public SelectorHandlerCollection(Func<IEnumerable<ISelectorHandler>> items)
|
||||
: base(items)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SelectorHandlerCollectionBuilder
|
||||
: LazyCollectionBuilderBase<SelectorHandlerCollectionBuilder, SelectorHandlerCollection, ISelectorHandler>
|
||||
{
|
||||
protected override SelectorHandlerCollectionBuilder This => this;
|
||||
}
|
||||
8
src/Umbraco.Core/DeliveryApi/SelectorOption.cs
Normal file
8
src/Umbraco.Core/DeliveryApi/SelectorOption.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SelectorOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public required string Value { get; set; }
|
||||
}
|
||||
11
src/Umbraco.Core/DeliveryApi/SortHandlerCollection.cs
Normal file
11
src/Umbraco.Core/DeliveryApi/SortHandlerCollection.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SortHandlerCollection : BuilderCollectionBase<ISortHandler>
|
||||
{
|
||||
public SortHandlerCollection(Func<IEnumerable<ISortHandler>> items)
|
||||
: base(items)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SortHandlerCollectionBuilder
|
||||
: LazyCollectionBuilderBase<SortHandlerCollectionBuilder, SortHandlerCollection, ISortHandler>
|
||||
{
|
||||
protected override SortHandlerCollectionBuilder This => this;
|
||||
}
|
||||
17
src/Umbraco.Core/DeliveryApi/SortOption.cs
Normal file
17
src/Umbraco.Core/DeliveryApi/SortOption.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public class SortOption
|
||||
{
|
||||
public required string FieldName { get; set; }
|
||||
|
||||
public Direction Direction { get; set; }
|
||||
|
||||
public FieldType FieldType { get; set; }
|
||||
}
|
||||
|
||||
public enum FieldType
|
||||
{
|
||||
String,
|
||||
Number,
|
||||
Date
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Umbraco.Cms.Core.Actions;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.ContentApps;
|
||||
using Umbraco.Cms.Core.Dashboards;
|
||||
using Umbraco.Cms.Core.Editors;
|
||||
@@ -123,6 +124,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
.Append<LottieFiles>();
|
||||
builder.SearchableTrees().Add(() => builder.TypeLoader.GetTypes<ISearchableTree>());
|
||||
builder.BackOfficeAssets();
|
||||
builder.SelectorHandlers().Add(() => builder.TypeLoader.GetTypes<ISelectorHandler>());
|
||||
builder.FilterHandlers().Add(() => builder.TypeLoader.GetTypes<IFilterHandler>());
|
||||
builder.SortHandlers().Add(() => builder.TypeLoader.GetTypes<ISortHandler>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -298,4 +302,22 @@ public static partial class UmbracoBuilderExtensions
|
||||
/// </summary>
|
||||
public static CustomBackOfficeAssetsCollectionBuilder BackOfficeAssets(this IUmbracoBuilder builder)
|
||||
=> builder.WithCollectionBuilder<CustomBackOfficeAssetsCollectionBuilder>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Delivery API selector handler collection builder
|
||||
/// </summary>
|
||||
public static SelectorHandlerCollectionBuilder SelectorHandlers(this IUmbracoBuilder builder)
|
||||
=> builder.WithCollectionBuilder<SelectorHandlerCollectionBuilder>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Delivery API filter handler collection builder
|
||||
/// </summary>
|
||||
public static FilterHandlerCollectionBuilder FilterHandlers(this IUmbracoBuilder builder)
|
||||
=> builder.WithCollectionBuilder<FilterHandlerCollectionBuilder>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Delivery API sort handler collection builder
|
||||
/// </summary>
|
||||
public static SortHandlerCollectionBuilder SortHandlers(this IUmbracoBuilder builder)
|
||||
=> builder.WithCollectionBuilder<SortHandlerCollectionBuilder>();
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddUmbracoOptions<ActiveDirectorySettings>()
|
||||
.AddUmbracoOptions<MarketplaceSettings>()
|
||||
.AddUmbracoOptions<ContentSettings>()
|
||||
.AddUmbracoOptions<DeliveryApiSettings>()
|
||||
.AddUmbracoOptions<CoreDebugSettings>()
|
||||
.AddUmbracoOptions<ExceptionFilterSettings>()
|
||||
.AddUmbracoOptions<GlobalSettings>(optionsBuilder => optionsBuilder.PostConfigure(options =>
|
||||
|
||||
20
src/Umbraco.Core/Models/DeliveryApi/ApiBlockGridArea.cs
Normal file
20
src/Umbraco.Core/Models/DeliveryApi/ApiBlockGridArea.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public class ApiBlockGridArea
|
||||
{
|
||||
public ApiBlockGridArea(string alias, int rowSpan, int columnSpan, IEnumerable<ApiBlockGridItem> items)
|
||||
{
|
||||
Alias = alias;
|
||||
RowSpan = rowSpan;
|
||||
ColumnSpan = columnSpan;
|
||||
Items = items;
|
||||
}
|
||||
|
||||
public string Alias { get; }
|
||||
|
||||
public int RowSpan { get; }
|
||||
|
||||
public int ColumnSpan { get; }
|
||||
|
||||
public IEnumerable<ApiBlockGridItem> Items { get; }
|
||||
}
|
||||
21
src/Umbraco.Core/Models/DeliveryApi/ApiBlockGridItem.cs
Normal file
21
src/Umbraco.Core/Models/DeliveryApi/ApiBlockGridItem.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public class ApiBlockGridItem : ApiBlockItem
|
||||
{
|
||||
public ApiBlockGridItem(IApiElement content, IApiElement? settings, int rowSpan, int columnSpan, int areaGridColumns, IEnumerable<ApiBlockGridArea> areas)
|
||||
: base(content, settings)
|
||||
{
|
||||
RowSpan = rowSpan;
|
||||
ColumnSpan = columnSpan;
|
||||
AreaGridColumns = areaGridColumns;
|
||||
Areas = areas;
|
||||
}
|
||||
|
||||
public int RowSpan { get; }
|
||||
|
||||
public int ColumnSpan { get; }
|
||||
|
||||
public int AreaGridColumns { get; }
|
||||
|
||||
public IEnumerable<ApiBlockGridArea> Areas { get; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user