From ac48dcccb3b25bb89f9c6347ac9314a076a54b85 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 9 Feb 2024 09:25:59 -0800 Subject: [PATCH 1/4] Use MinimalAPI for comparison --- .../Endpoints/Api/CreateEndpoint.cs | 140 ++++++++++++++++++ .../Endpoints/Api/DeleteEndpoint.cs | 46 ++++++ .../Endpoints/Api/GetEndpoint.cs | 44 ++++++ .../Endpoints/EndpointView.cs | 126 ++++++++++++++++ .../Endpoints/Item/DisplayEndpoint.cs | 48 ++++++ .../OrchardCore.Contents/Startup.cs | 8 + 6 files changed, 412 insertions(+) create mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs new file mode 100644 index 00000000000..879643d305f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs @@ -0,0 +1,140 @@ +using System.Linq; +using System.Security.Claims; +using System.Text.Json.Settings; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Routing; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Handlers; +using OrchardCore.ContentManagement.Metadata; +using OrchardCore.DisplayManagement.ModelBinding; + +namespace OrchardCore.Contents.Endpoints.Api; + +public static class CreateEndpoint +{ + public static IEndpointRouteBuilder AddCreateContentApiEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapPost("api/ep-content", ActionAsync); + + return builder; + } + + private static readonly JsonMergeSettings _updateJsonMergeSettings = new() + { + MergeArrayHandling = MergeArrayHandling.Replace + }; + + [Authorize(AuthenticationSchemes = "Api")] + private static async Task ActionAsync( + ContentItem model, + bool draft, + IContentManager contentManager, + IAuthorizationService authorizationService, + IContentDefinitionManager contentDefinitionManager, + IUpdateModelAccessor updateModelAccessor, + HttpContext httpContext) + { + if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) + { + return Results.Forbid(); + } + + var contentItem = await contentManager.GetAsync(model.ContentItemId, VersionOptions.DraftRequired); + var modelState = updateModelAccessor.ModelUpdater.ModelState; + + if (contentItem == null) + { + if (string.IsNullOrEmpty(model?.ContentType) || await contentDefinitionManager.GetTypeDefinitionAsync(model.ContentType) == null) + { + return Results.BadRequest(); + } + + contentItem = await contentManager.NewAsync(model.ContentType); + contentItem.Owner = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.PublishContent, contentItem)) + { + return Results.Forbid(); + } + + contentItem.Merge(model); + + var result = await contentManager.UpdateValidateAndCreateAsync(contentItem, VersionOptions.Draft); + + if (!result.Succeeded) + { + // Add the validation results to the ModelState to present the errors as part of the response. + AddValidationErrorsToModelState(result, modelState); + } + + // We check the model state after calling all handlers because they trigger WF content events so, even they are not + // intended to add model errors (only drivers), a WF content task may be executed inline and add some model errors. + if (!modelState.IsValid) + { + var errors = modelState.ToDictionary(entry => entry.Key, entry => entry.Value.Errors.Select(x => x.ErrorMessage).ToArray()); + + return Results.ValidationProblem(errors); + } + } + else + { + if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.EditContent, contentItem)) + { + return Results.Forbid(); + } + + contentItem.Merge(model, _updateJsonMergeSettings); + + await contentManager.UpdateAsync(contentItem); + var result = await contentManager.ValidateAsync(contentItem); + + if (!result.Succeeded) + { + // Add the validation results to the ModelState to present the errors as part of the response. + AddValidationErrorsToModelState(result, modelState); + } + + // We check the model state after calling all handlers because they trigger WF content events so, even they are not + // intended to add model errors (only drivers), a WF content task may be executed inline and add some model errors. + if (!modelState.IsValid) + { + var errors = modelState.ToDictionary(entry => entry.Key, entry => entry.Value.Errors.Select(x => x.ErrorMessage).ToArray()); + + return Results.ValidationProblem(errors); + } + } + + if (!draft) + { + await contentManager.PublishAsync(contentItem); + } + else + { + await contentManager.SaveDraftAsync(contentItem); + } + + return Results.Ok(contentItem); + } + + private static void AddValidationErrorsToModelState(ContentValidateResult result, ModelStateDictionary modelState) + { + foreach (var error in result.Errors) + { + if (error.MemberNames != null && error.MemberNames.Any()) + { + foreach (var memberName in error.MemberNames) + { + modelState.AddModelError(memberName, error.ErrorMessage); + } + } + else + { + modelState.AddModelError(string.Empty, error.ErrorMessage); + } + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs new file mode 100644 index 00000000000..424a2a38a79 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using OrchardCore.ContentManagement; + +namespace OrchardCore.Contents.Endpoints.Api; + +public static class DeleteEndpoint +{ + public static IEndpointRouteBuilder AddDeleteContentApiEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapDelete("api/ep-content/{contentItemId}", ActionAsync); + + return builder; + } + + [Authorize(AuthenticationSchemes = "Api")] + private static async Task ActionAsync(string contentItemId, + IContentManager contentManager, + IAuthorizationService authorizationService, + HttpContext httpContext) + { + if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) + { + return Results.Forbid(); + } + + var contentItem = await contentManager.GetAsync(contentItemId); + + if (contentItem == null) + { + return Results.NotFound(); + } + + if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.DeleteContent, contentItem)) + { + return Results.Forbid(); + } + + await contentManager.RemoveAsync(contentItem); + + return Results.Ok(contentItem); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs new file mode 100644 index 00000000000..3c6e5d18267 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using OrchardCore.ContentManagement; + +namespace OrchardCore.Contents.Endpoints.Api; + +public static class GetEndpoint +{ + public static IEndpointRouteBuilder AddGetContentApiContentEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapGet("api/ep-content/{contentItemId}", ActionAsync); + + return builder; + } + + [Authorize(AuthenticationSchemes = "Api")] + private static async Task ActionAsync(string contentItemId, + IContentManager contentManager, + IAuthorizationService authorizationService, + HttpContext httpContext) + { + if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) + { + return Results.Forbid(); + } + + var contentItem = await contentManager.GetAsync(contentItemId); + + if (contentItem == null) + { + return Results.NotFound(); + } + + if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.ViewContent, contentItem)) + { + return Results.Forbid(); + } + + return Results.Ok(contentItem); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs new file mode 100644 index 00000000000..788a6937a84 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OrchardCore.DisplayManagement.ModelBinding; + +namespace OrchardCore.Contents.Endpoints; + +public class EndpointView : IResult +{ + private const string ModelKey = "Model"; + + public string ViewName { get; } + + public T Model { get; set; } + + public EndpointView(string viewName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(viewName); + + ViewName = viewName; + } + + public EndpointView(string viewName, T model) + : this(viewName) + { + Model = model; + } + + public async Task ExecuteAsync(HttpContext httpContext) + { + var viewEngineOptions = httpContext.RequestServices.GetService>().Value; + + var viewEngines = viewEngineOptions.ViewEngines; + + if (viewEngines.Count == 0) + { + throw new InvalidOperationException(string.Format("'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.", + typeof(MvcViewOptions).FullName, + nameof(MvcViewOptions.ViewEngines), + typeof(IViewEngine).FullName)); + } + + var viewEngine = viewEngines[0]; + + var viewEngineResult = viewEngine.GetView(executingFilePath: null, ViewName, isMainPage: true); + var modelStateAccessor = httpContext.RequestServices.GetService(); + + var actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor(), modelStateAccessor.ModelUpdater.ModelState); + + if (viewEngineResult.Success) + { + await WriteAsync(httpContext, viewEngineResult.View, actionContext, modelStateAccessor.ModelUpdater.ModelState); + + return; + } + + var findViewResult = viewEngine.FindView(actionContext, ViewName, isMainPage: true); + if (findViewResult.Success) + { + await WriteAsync(httpContext, viewEngineResult.View, actionContext, modelStateAccessor.ModelUpdater.ModelState); + + return; + } + + httpContext.Response.StatusCode = 404; + + var searchedLocations = viewEngineResult.SearchedLocations.Concat(findViewResult.SearchedLocations); + var errorMessage = string.Join(System.Environment.NewLine, + new[] { $"Unable to find view '{ViewName}'. The following locations were searched:" }.Concat(searchedLocations)); + + var bytes = Encoding.UTF8.GetBytes(errorMessage); + await httpContext.Response.Body.WriteAsync(bytes); + } + + private async Task WriteAsync(HttpContext httpContext, IView view, ActionContext actionContext, ModelStateDictionary modelState) + { + var modelMetadataProvider = httpContext.RequestServices.GetRequiredService(); + + var viewDataDictionary = new ViewDataDictionary(modelMetadataProvider, modelState); + + if (Model is not null) + { + viewDataDictionary.Add(ModelKey, Model); + } + + var tempData = httpContext.RequestServices.GetService(); + var streamWriter = new StreamWriter(httpContext.Response.Body); + + var viewContext = new ViewContext( + actionContext, + view, + viewDataDictionary, + new TempDataDictionary(httpContext, tempData), + streamWriter, + new HtmlHelperOptions()); + + await view.RenderAsync(viewContext); + } +} + +public class EndpointView : EndpointView +{ + public EndpointView(string viewName) + : base(viewName) + { + + } + + + public EndpointView(string viewName, object model) + : base(viewName, model) + { + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs new file mode 100644 index 00000000000..b4215c1b1b2 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using OrchardCore.ContentManagement; +using OrchardCore.ContentManagement.Display; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.ModelBinding; + +namespace OrchardCore.Contents.Endpoints.Item; + +public static class DisplayEndpoint +{ + public static IEndpointRouteBuilder AddDisplayContentEndpoint(this IEndpointRouteBuilder builder) + { + builder.MapGet("ep-contents/contentitems/{contentItemId}", GetAsync); + + return builder; + } + + private static async Task GetAsync( + string contentItemId, + string jsonPath, + IContentManager contentManager, + IContentItemDisplayManager contentItemDisplayManager, + IUpdateModelAccessor updateModelAccessor, + IAuthorizationService authorizationService, + HttpContext httpContext) + { + var contentItem = await contentManager.GetAsync(contentItemId, jsonPath); + + if (contentItem == null) + { + return TypedResults.NotFound(); + } + + if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.ViewContent, contentItem)) + { + return TypedResults.Forbid(); + } + + var model = await contentItemDisplayManager.BuildDisplayAsync(contentItem, updateModelAccessor.ModelUpdater); + + // ~/OrchardCore.Contents/Views/Item/Display.cshtml + return new EndpointView("/Display.cshtml", model); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs index 15ad58e1776..e3af4e3d776 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs @@ -20,6 +20,8 @@ using OrchardCore.Contents.Controllers; using OrchardCore.Contents.Deployment; using OrchardCore.Contents.Drivers; +using OrchardCore.Contents.Endpoints.Api; +using OrchardCore.Contents.Endpoints.Item; using OrchardCore.Contents.Feeds.Builders; using OrchardCore.Contents.Handlers; using OrchardCore.Contents.Indexing; @@ -223,6 +225,12 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde { var itemControllerName = typeof(ItemController).ControllerName(); + routes.AddDisplayContentEndpoint(); + + routes.AddCreateContentApiEndpoint() + .AddGetContentApiContentEndpoint() + .AddDeleteContentApiEndpoint(); + routes.MapAreaControllerRoute( name: "DisplayContentItem", areaName: "OrchardCore.Contents", From dc9454774fa3bde533cf0563f907b3039957a47b Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 9 Feb 2024 09:37:08 -0800 Subject: [PATCH 2/4] Remove the API controller --- .../Controllers/ApiController.cs | 200 ------------------ .../Endpoints/Api/CreateEndpoint.cs | 2 +- .../Endpoints/Api/DeleteEndpoint.cs | 2 +- .../Endpoints/Api/GetEndpoint.cs | 2 +- 4 files changed, 3 insertions(+), 203 deletions(-) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs deleted file mode 100644 index 8b0826f11c1..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Controllers/ApiController.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Linq; -using System.Net; -using System.Security.Claims; -using System.Text.Json.Nodes; -using System.Text.Json.Settings; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Localization; -using OrchardCore.ContentManagement; -using OrchardCore.ContentManagement.Handlers; -using OrchardCore.ContentManagement.Metadata; - -namespace OrchardCore.Contents.Controllers -{ - [Route("api/content")] - [ApiController] - [Authorize(AuthenticationSchemes = "Api"), IgnoreAntiforgeryToken, AllowAnonymous] - public class ApiController : Controller - { - private static readonly JsonMergeSettings _updateJsonMergeSettings = new() { MergeArrayHandling = MergeArrayHandling.Replace }; - - private readonly IContentManager _contentManager; - private readonly IContentDefinitionManager _contentDefinitionManager; - private readonly IAuthorizationService _authorizationService; - protected readonly IStringLocalizer S; - - public ApiController( - IContentManager contentManager, - IContentDefinitionManager contentDefinitionManager, - IAuthorizationService authorizationService, - IStringLocalizer stringLocalizer) - { - _contentManager = contentManager; - _contentDefinitionManager = contentDefinitionManager; - _authorizationService = authorizationService; - S = stringLocalizer; - } - - [Route("{contentItemId}"), HttpGet] - public async Task Get(string contentItemId) - { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.AccessContentApi)) - { - return this.ChallengeOrForbid("Api"); - } - - var contentItem = await _contentManager.GetAsync(contentItemId); - - if (contentItem == null) - { - return NotFound(); - } - - if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ViewContent, contentItem)) - { - return this.ChallengeOrForbid("Api"); - } - - return Ok(contentItem); - } - - [HttpDelete] - [Route("{contentItemId}")] - public async Task Delete(string contentItemId) - { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.AccessContentApi)) - { - return this.ChallengeOrForbid("Api"); - } - - var contentItem = await _contentManager.GetAsync(contentItemId); - - if (contentItem == null) - { - return NoContent(); - } - - if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.DeleteContent, contentItem)) - { - return this.ChallengeOrForbid("Api"); - } - - await _contentManager.RemoveAsync(contentItem); - - return Ok(contentItem); - } - - [HttpPost] - public async Task Post(ContentItem model, bool draft = false) - { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.AccessContentApi)) - { - return this.ChallengeOrForbid("Api"); - } - - // It is really important to keep the proper method calls order with the ContentManager - // so that all event handlers gets triggered in the right sequence. - - var contentItem = await _contentManager.GetAsync(model.ContentItemId, VersionOptions.DraftRequired); - - if (contentItem == null) - { - if (string.IsNullOrEmpty(model?.ContentType) || await _contentDefinitionManager.GetTypeDefinitionAsync(model.ContentType) == null) - { - return BadRequest(); - } - - contentItem = await _contentManager.NewAsync(model.ContentType); - contentItem.Owner = User.FindFirstValue(ClaimTypes.NameIdentifier); - - if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.PublishContent, contentItem)) - { - return this.ChallengeOrForbid("Api"); - } - - contentItem.Merge(model); - - var result = await _contentManager.UpdateValidateAndCreateAsync(contentItem, VersionOptions.Draft); - - if (!result.Succeeded) - { - // Add the validation results to the ModelState to present the errors as part of the response. - AddValidationErrorsToModelState(result); - } - - // We check the model state after calling all handlers because they trigger WF content events so, even they are not - // intended to add model errors (only drivers), a WF content task may be executed inline and add some model errors. - if (!ModelState.IsValid) - { - return ValidationProblem(new ValidationProblemDetails(ModelState) - { - Title = S["One or more validation errors occurred."], - Detail = string.Join(", ", ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))), - Status = (int)HttpStatusCode.BadRequest, - }); - } - } - else - { - if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditContent, contentItem)) - { - return this.ChallengeOrForbid("Api"); - } - - contentItem.Merge(model, _updateJsonMergeSettings); - - await _contentManager.UpdateAsync(contentItem); - var result = await _contentManager.ValidateAsync(contentItem); - - if (!result.Succeeded) - { - // Add the validation results to the ModelState to present the errors as part of the response. - AddValidationErrorsToModelState(result); - } - - // We check the model state after calling all handlers because they trigger WF content events so, even they are not - // intended to add model errors (only drivers), a WF content task may be executed inline and add some model errors. - if (!ModelState.IsValid) - { - return ValidationProblem(new ValidationProblemDetails(ModelState) - { - Title = S["One or more validation errors occurred."], - Detail = string.Join(", ", ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))), - Status = (int)HttpStatusCode.BadRequest, - }); - } - } - - if (!draft) - { - await _contentManager.PublishAsync(contentItem); - } - else - { - await _contentManager.SaveDraftAsync(contentItem); - } - - return Ok(contentItem); - } - - private void AddValidationErrorsToModelState(ContentValidateResult result) - { - foreach (var error in result.Errors) - { - if (error.MemberNames != null && error.MemberNames.Any()) - { - foreach (var memberName in error.MemberNames) - { - ModelState.AddModelError(memberName, error.ErrorMessage); - } - } - else - { - ModelState.AddModelError(string.Empty, error.ErrorMessage); - } - } - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs index 879643d305f..92627e85591 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs @@ -18,7 +18,7 @@ public static class CreateEndpoint { public static IEndpointRouteBuilder AddCreateContentApiEndpoint(this IEndpointRouteBuilder builder) { - builder.MapPost("api/ep-content", ActionAsync); + builder.MapPost("api/content", ActionAsync); return builder; } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs index 424a2a38a79..c7cee337b5e 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs @@ -11,7 +11,7 @@ public static class DeleteEndpoint { public static IEndpointRouteBuilder AddDeleteContentApiEndpoint(this IEndpointRouteBuilder builder) { - builder.MapDelete("api/ep-content/{contentItemId}", ActionAsync); + builder.MapDelete("api/content/{contentItemId}", ActionAsync); return builder; } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs index 3c6e5d18267..55f06fb9a1a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs @@ -11,7 +11,7 @@ public static class GetEndpoint { public static IEndpointRouteBuilder AddGetContentApiContentEndpoint(this IEndpointRouteBuilder builder) { - builder.MapGet("api/ep-content/{contentItemId}", ActionAsync); + builder.MapGet("api/content/{contentItemId}", ActionAsync); return builder; } From 3e8196340f213f8e63f3e843ec3852cc84ec8987 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 27 Feb 2024 15:57:21 -0800 Subject: [PATCH 3/4] remove display --- .../Endpoints/Api/CreateEndpoint.cs | 2 +- .../Endpoints/EndpointView.cs | 126 ------------------ .../Endpoints/Item/DisplayEndpoint.cs | 48 ------- .../OrchardCore.Contents.csproj | 5 +- .../OrchardCore.Contents/Startup.cs | 3 - 5 files changed, 4 insertions(+), 180 deletions(-) delete mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs index 92627e85591..9dadc79fe09 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs @@ -25,7 +25,7 @@ public static IEndpointRouteBuilder AddCreateContentApiEndpoint(this IEndpointRo private static readonly JsonMergeSettings _updateJsonMergeSettings = new() { - MergeArrayHandling = MergeArrayHandling.Replace + MergeArrayHandling = MergeArrayHandling.Replace, }; [Authorize(AuthenticationSchemes = "Api")] diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs deleted file mode 100644 index 788a6937a84..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/EndpointView.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.ModelBinding; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewEngines; -using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using OrchardCore.DisplayManagement.ModelBinding; - -namespace OrchardCore.Contents.Endpoints; - -public class EndpointView : IResult -{ - private const string ModelKey = "Model"; - - public string ViewName { get; } - - public T Model { get; set; } - - public EndpointView(string viewName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(viewName); - - ViewName = viewName; - } - - public EndpointView(string viewName, T model) - : this(viewName) - { - Model = model; - } - - public async Task ExecuteAsync(HttpContext httpContext) - { - var viewEngineOptions = httpContext.RequestServices.GetService>().Value; - - var viewEngines = viewEngineOptions.ViewEngines; - - if (viewEngines.Count == 0) - { - throw new InvalidOperationException(string.Format("'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.", - typeof(MvcViewOptions).FullName, - nameof(MvcViewOptions.ViewEngines), - typeof(IViewEngine).FullName)); - } - - var viewEngine = viewEngines[0]; - - var viewEngineResult = viewEngine.GetView(executingFilePath: null, ViewName, isMainPage: true); - var modelStateAccessor = httpContext.RequestServices.GetService(); - - var actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor(), modelStateAccessor.ModelUpdater.ModelState); - - if (viewEngineResult.Success) - { - await WriteAsync(httpContext, viewEngineResult.View, actionContext, modelStateAccessor.ModelUpdater.ModelState); - - return; - } - - var findViewResult = viewEngine.FindView(actionContext, ViewName, isMainPage: true); - if (findViewResult.Success) - { - await WriteAsync(httpContext, viewEngineResult.View, actionContext, modelStateAccessor.ModelUpdater.ModelState); - - return; - } - - httpContext.Response.StatusCode = 404; - - var searchedLocations = viewEngineResult.SearchedLocations.Concat(findViewResult.SearchedLocations); - var errorMessage = string.Join(System.Environment.NewLine, - new[] { $"Unable to find view '{ViewName}'. The following locations were searched:" }.Concat(searchedLocations)); - - var bytes = Encoding.UTF8.GetBytes(errorMessage); - await httpContext.Response.Body.WriteAsync(bytes); - } - - private async Task WriteAsync(HttpContext httpContext, IView view, ActionContext actionContext, ModelStateDictionary modelState) - { - var modelMetadataProvider = httpContext.RequestServices.GetRequiredService(); - - var viewDataDictionary = new ViewDataDictionary(modelMetadataProvider, modelState); - - if (Model is not null) - { - viewDataDictionary.Add(ModelKey, Model); - } - - var tempData = httpContext.RequestServices.GetService(); - var streamWriter = new StreamWriter(httpContext.Response.Body); - - var viewContext = new ViewContext( - actionContext, - view, - viewDataDictionary, - new TempDataDictionary(httpContext, tempData), - streamWriter, - new HtmlHelperOptions()); - - await view.RenderAsync(viewContext); - } -} - -public class EndpointView : EndpointView -{ - public EndpointView(string viewName) - : base(viewName) - { - - } - - - public EndpointView(string viewName, object model) - : base(viewName, model) - { - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs deleted file mode 100644 index b4215c1b1b2..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Item/DisplayEndpoint.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using OrchardCore.ContentManagement; -using OrchardCore.ContentManagement.Display; -using OrchardCore.DisplayManagement; -using OrchardCore.DisplayManagement.ModelBinding; - -namespace OrchardCore.Contents.Endpoints.Item; - -public static class DisplayEndpoint -{ - public static IEndpointRouteBuilder AddDisplayContentEndpoint(this IEndpointRouteBuilder builder) - { - builder.MapGet("ep-contents/contentitems/{contentItemId}", GetAsync); - - return builder; - } - - private static async Task GetAsync( - string contentItemId, - string jsonPath, - IContentManager contentManager, - IContentItemDisplayManager contentItemDisplayManager, - IUpdateModelAccessor updateModelAccessor, - IAuthorizationService authorizationService, - HttpContext httpContext) - { - var contentItem = await contentManager.GetAsync(contentItemId, jsonPath); - - if (contentItem == null) - { - return TypedResults.NotFound(); - } - - if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.ViewContent, contentItem)) - { - return TypedResults.Forbid(); - } - - var model = await contentItemDisplayManager.BuildDisplayAsync(contentItem, updateModelAccessor.ModelUpdater); - - // ~/OrchardCore.Contents/Views/Item/Display.cshtml - return new EndpointView("/Display.cshtml", model); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj b/src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj index 7f436fa8e27..09d1bbb2447 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Contents/OrchardCore.Contents.csproj @@ -4,9 +4,10 @@ true OrchardCore Contents - $(OCCMSDescription) + + $(OCCMSDescription) - Provides Content Management services. + Provides Content Management services. $(PackageTags) OrchardCoreCMS ContentManagement diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs index e3af4e3d776..ec6c9bb3d7c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs @@ -21,7 +21,6 @@ using OrchardCore.Contents.Deployment; using OrchardCore.Contents.Drivers; using OrchardCore.Contents.Endpoints.Api; -using OrchardCore.Contents.Endpoints.Item; using OrchardCore.Contents.Feeds.Builders; using OrchardCore.Contents.Handlers; using OrchardCore.Contents.Indexing; @@ -225,8 +224,6 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde { var itemControllerName = typeof(ItemController).ControllerName(); - routes.AddDisplayContentEndpoint(); - routes.AddCreateContentApiEndpoint() .AddGetContentApiContentEndpoint() .AddDeleteContentApiEndpoint(); From 572af205814233cd1dd7b848a5eb2e2231222656 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Fri, 22 Mar 2024 14:55:10 -0700 Subject: [PATCH 4/4] Fix tests --- .../Endpoints/Api/CreateEndpoint.cs | 25 ++++++++++-------- .../Endpoints/Api/DeleteEndpoint.cs | 18 ++++++++----- .../Endpoints/Api/GetEndpoint.cs | 24 ++++++++++------- .../OrchardCore.Contents/Startup.cs | 8 +++--- .../Extensions/HttpContextExtensions.cs | 26 ++++++++++++------- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs index 9dadc79fe09..60ca5cdb575 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/CreateEndpoint.cs @@ -11,14 +11,17 @@ using OrchardCore.ContentManagement.Handlers; using OrchardCore.ContentManagement.Metadata; using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; public static class CreateEndpoint { - public static IEndpointRouteBuilder AddCreateContentApiEndpoint(this IEndpointRouteBuilder builder) + public static IEndpointRouteBuilder AddCreateContentEndpoint(this IEndpointRouteBuilder builder) { - builder.MapPost("api/content", ActionAsync); + builder.MapPost("api/content", ActionAsync) + .AllowAnonymous() + .DisableAntiforgery(); return builder; } @@ -31,16 +34,16 @@ public static IEndpointRouteBuilder AddCreateContentApiEndpoint(this IEndpointRo [Authorize(AuthenticationSchemes = "Api")] private static async Task ActionAsync( ContentItem model, - bool draft, IContentManager contentManager, IAuthorizationService authorizationService, IContentDefinitionManager contentDefinitionManager, IUpdateModelAccessor updateModelAccessor, - HttpContext httpContext) + HttpContext httpContext, + bool draft = false) { if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } var contentItem = await contentManager.GetAsync(model.ContentItemId, VersionOptions.DraftRequired); @@ -50,7 +53,7 @@ private static async Task ActionAsync( { if (string.IsNullOrEmpty(model?.ContentType) || await contentDefinitionManager.GetTypeDefinitionAsync(model.ContentType) == null) { - return Results.BadRequest(); + return TypedResults.BadRequest(); } contentItem = await contentManager.NewAsync(model.ContentType); @@ -58,7 +61,7 @@ private static async Task ActionAsync( if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.PublishContent, contentItem)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } contentItem.Merge(model); @@ -77,14 +80,14 @@ private static async Task ActionAsync( { var errors = modelState.ToDictionary(entry => entry.Key, entry => entry.Value.Errors.Select(x => x.ErrorMessage).ToArray()); - return Results.ValidationProblem(errors); + return TypedResults.ValidationProblem(errors, detail: string.Join(", ", modelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage)))); } } else { if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.EditContent, contentItem)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } contentItem.Merge(model, _updateJsonMergeSettings); @@ -104,7 +107,7 @@ private static async Task ActionAsync( { var errors = modelState.ToDictionary(entry => entry.Key, entry => entry.Value.Errors.Select(x => x.ErrorMessage).ToArray()); - return Results.ValidationProblem(errors); + return TypedResults.ValidationProblem(errors, detail: string.Join(", ", modelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage)))); } } @@ -117,7 +120,7 @@ private static async Task ActionAsync( await contentManager.SaveDraftAsync(contentItem); } - return Results.Ok(contentItem); + return TypedResults.Ok(contentItem); } private static void AddValidationErrorsToModelState(ContentValidateResult result, ModelStateDictionary modelState) diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs index c7cee337b5e..e3b2b58505b 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/DeleteEndpoint.cs @@ -4,43 +4,47 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using OrchardCore.ContentManagement; +using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; public static class DeleteEndpoint { - public static IEndpointRouteBuilder AddDeleteContentApiEndpoint(this IEndpointRouteBuilder builder) + public static IEndpointRouteBuilder AddDeleteContentEndpoint(this IEndpointRouteBuilder builder) { - builder.MapDelete("api/content/{contentItemId}", ActionAsync); + builder.MapDelete("api/content/{contentItemId}", ActionAsync) + .AllowAnonymous() + .DisableAntiforgery(); return builder; } [Authorize(AuthenticationSchemes = "Api")] - private static async Task ActionAsync(string contentItemId, + private static async Task ActionAsync( + string contentItemId, IContentManager contentManager, IAuthorizationService authorizationService, HttpContext httpContext) { if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } var contentItem = await contentManager.GetAsync(contentItemId); if (contentItem == null) { - return Results.NotFound(); + return TypedResults.NotFound(); } if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.DeleteContent, contentItem)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } await contentManager.RemoveAsync(contentItem); - return Results.Ok(contentItem); + return TypedResults.Ok(contentItem); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs index 55f06fb9a1a..851ce7a07f3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Endpoints/Api/GetEndpoint.cs @@ -4,41 +4,45 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using OrchardCore.ContentManagement; +using OrchardCore.Modules; namespace OrchardCore.Contents.Endpoints.Api; public static class GetEndpoint { - public static IEndpointRouteBuilder AddGetContentApiContentEndpoint(this IEndpointRouteBuilder builder) + public static IEndpointRouteBuilder AddGetContentEndpoint(this IEndpointRouteBuilder builder) { - builder.MapGet("api/content/{contentItemId}", ActionAsync); + builder.MapGet("api/content/{contentItemId}", ActionAsync) + .AllowAnonymous() + .DisableAntiforgery(); return builder; } [Authorize(AuthenticationSchemes = "Api")] - private static async Task ActionAsync(string contentItemId, - IContentManager contentManager, - IAuthorizationService authorizationService, - HttpContext httpContext) + private static async Task ActionAsync( + string contentItemId, + IContentManager contentManager, + IAuthorizationService authorizationService, + HttpContext httpContext) { if (!await authorizationService.AuthorizeAsync(httpContext.User, Permissions.AccessContentApi)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } var contentItem = await contentManager.GetAsync(contentItemId); if (contentItem == null) { - return Results.NotFound(); + return TypedResults.NotFound(); } if (!await authorizationService.AuthorizeAsync(httpContext.User, CommonPermissions.ViewContent, contentItem)) { - return Results.Forbid(); + return httpContext.ChallengeOrForbid("Api"); } - return Results.Ok(contentItem); + return TypedResults.Ok(contentItem); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs index e3f798bb39c..7023c50b7f2 100644 --- a/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Contents/Startup.cs @@ -213,11 +213,11 @@ static async Task GetContentByHandleAsync(LiquidTemplateContext cont public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) { - var itemControllerName = typeof(ItemController).ControllerName(); + routes.AddGetContentEndpoint() + .AddCreateContentEndpoint() + .AddDeleteContentEndpoint(); - routes.AddCreateContentApiEndpoint() - .AddGetContentApiContentEndpoint() - .AddDeleteContentApiEndpoint(); + var itemControllerName = typeof(ItemController).ControllerName(); routes.MapAreaControllerRoute( name: "DisplayContentItem", diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Extensions/HttpContextExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Extensions/HttpContextExtensions.cs index e89ee753a2c..8a683303cc7 100644 --- a/src/OrchardCore/OrchardCore.Abstractions/Modules/Extensions/HttpContextExtensions.cs +++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Extensions/HttpContextExtensions.cs @@ -1,17 +1,23 @@ using Microsoft.AspNetCore.Http; using OrchardCore.Environment.Shell.Scope; -namespace OrchardCore.Modules +namespace OrchardCore.Modules; + +public static class HttpContextExtensions { - public static class HttpContextExtensions + /// + /// Makes aware of the current . + /// + public static HttpContext UseShellScopeServices(this HttpContext httpContext) + { + httpContext.RequestServices = new ShellScopeServices(httpContext.RequestServices); + return httpContext; + } + + public static IResult ChallengeOrForbid(this HttpContext httpContext, params string[] authenticationSchemes) { - /// - /// Makes aware of the current . - /// - public static HttpContext UseShellScopeServices(this HttpContext httpContext) - { - httpContext.RequestServices = new ShellScopeServices(httpContext.RequestServices); - return httpContext; - } + return httpContext.User?.Identity?.IsAuthenticated == true + ? TypedResults.Forbid(properties: null, authenticationSchemes) + : TypedResults.Challenge(properties: null, authenticationSchemes); } }