diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/ContentApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/ContentApiControllerBase.cs index 1213def424..834332fcbb 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/ContentApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/ContentApiControllerBase.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; +using Umbraco.Cms.Api.Delivery.Filters; using Umbraco.Cms.Api.Delivery.Routing; using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; @@ -9,6 +10,8 @@ namespace Umbraco.Cms.Api.Delivery.Controllers; [VersionedDeliveryApiRoute("content")] [ApiExplorerSettings(GroupName = "Content")] +[LocalizeFromAcceptLanguageHeader] +[ValidateStartItem] public abstract class ContentApiControllerBase : DeliveryApiControllerBase { protected IApiPublishedContentCache ApiPublishedContentCache { get; } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs index 323211726b..2230e228ce 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/DeliveryApiControllerBase.cs @@ -9,7 +9,6 @@ namespace Umbraco.Cms.Api.Delivery.Controllers; [ApiVersion("1.0")] [DeliveryApiAccess] [JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)] -[LocalizeFromAcceptLanguageHeader] public abstract class DeliveryApiControllerBase : Controller { } diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs index c1a6d8e197..53db806c3e 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/QueryContentApiController.cs @@ -34,6 +34,7 @@ public class QueryContentApiController : ContentApiControllerBase [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Query( string? fetch, [FromQuery] string[] filter, diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/ValidateStartItemAttribute.cs b/src/Umbraco.Cms.Api.Delivery/Filters/ValidateStartItemAttribute.cs new file mode 100644 index 0000000000..8e5d1bbb38 --- /dev/null +++ b/src/Umbraco.Cms.Api.Delivery/Filters/ValidateStartItemAttribute.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Api.Delivery.Filters; + +internal sealed class ValidateStartItemAttribute : TypeFilterAttribute +{ + public ValidateStartItemAttribute() + : base(typeof(ValidateStartItemFilter)) + { + } + + private class ValidateStartItemFilter : IActionFilter + { + private readonly IRequestStartItemProvider _requestStartItemProvider; + + public ValidateStartItemFilter(IRequestStartItemProvider requestStartItemProvider) + => _requestStartItemProvider = requestStartItemProvider; + + public void OnActionExecuting(ActionExecutingContext context) + { + if (_requestStartItemProvider.RequestedStartItem() is null) + { + return; + } + + IPublishedContent? startItem = _requestStartItemProvider.GetStartItem(); + + if (startItem is null) + { + context.Result = new NotFoundObjectResult("The Start-Item could not be found"); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs index c75c50c42c..db9f4c3385 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/ContentTypeFilter.cs @@ -22,7 +22,7 @@ public sealed class ContentTypeFilter : IFilterHandler Value = string.Empty }; - // TODO: do we support negation? + // Support negation if (alias.StartsWith('!')) { filterOption.Value = alias.Substring(1); diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs index 40733de7f0..f909a771d1 100644 --- a/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs +++ b/src/Umbraco.Cms.Api.Delivery/Querying/Filters/NameFilter.cs @@ -22,7 +22,7 @@ public sealed class NameFilter : IFilterHandler Value = string.Empty }; - // TODO: do we support negation? + // Support negation if (value.StartsWith('!')) { filterOption.Value = value.Substring(1); diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs index 8b1c467a63..2ff748dcdc 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestStartItemProvider.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -9,15 +10,20 @@ namespace Umbraco.Cms.Api.Delivery.Services; internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestStartItemProvider { private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; // this provider lifetime is Scope, so we can cache this as a field private IPublishedContent? _requestedStartContent; public RequestStartItemProvider( IHttpContextAccessor httpContextAccessor, - IPublishedSnapshotAccessor publishedSnapshotAccessor) - : base(httpContextAccessor) => + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IVariationContextAccessor variationContextAccessor) + : base(httpContextAccessor) + { _publishedSnapshotAccessor = publishedSnapshotAccessor; + _variationContextAccessor = variationContextAccessor; + } /// public IPublishedContent? GetStartItem() @@ -27,13 +33,14 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS return _requestedStartContent; } - var headerValue = GetHeaderValue("Start-Item"); + var headerValue = RequestedStartItem()?.Trim(Constants.CharArrays.ForwardSlash); if (headerValue.IsNullOrWhiteSpace()) { return null; } - if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false || publishedSnapshot?.Content == null) + if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) == false || + publishedSnapshot?.Content == null) { return null; } @@ -42,8 +49,11 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS _requestedStartContent = Guid.TryParse(headerValue, out Guid key) ? rootContent.FirstOrDefault(c => c.Key == key) - : rootContent.FirstOrDefault(c => c.UrlSegment == headerValue); + : rootContent.FirstOrDefault(c => c.UrlSegment(_variationContextAccessor).InvariantEquals(headerValue)); return _requestedStartContent; } + + /// + public string? RequestedStartItem() => GetHeaderValue("Start-Item"); } diff --git a/src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs b/src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs index 36dfbd525a..6276b59cd9 100644 --- a/src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs +++ b/src/Umbraco.Core/DeliveryApi/IRequestStartItemProvider.cs @@ -5,7 +5,12 @@ namespace Umbraco.Cms.Core.DeliveryApi; public interface IRequestStartItemProvider { /// - /// Gets the requested start item from the "Start-Item" header, if present. + /// Gets the requested start item, if present. /// IPublishedContent? GetStartItem(); + + /// + /// Gets the value of the requested start item, if present. + /// + string? RequestedStartItem(); } diff --git a/src/Umbraco.Core/DeliveryApi/NoopRequestStartItemProvider.cs b/src/Umbraco.Core/DeliveryApi/NoopRequestStartItemProvider.cs index 6f22c43904..46da70cbcf 100644 --- a/src/Umbraco.Core/DeliveryApi/NoopRequestStartItemProvider.cs +++ b/src/Umbraco.Core/DeliveryApi/NoopRequestStartItemProvider.cs @@ -6,4 +6,7 @@ internal sealed class NoopRequestStartItemProvider : IRequestStartItemProvider { /// public IPublishedContent? GetStartItem() => null; + + /// + public string? RequestedStartItem() => null; }