Skip to content

Commit

Permalink
Bring over the deleted API POST method.
Browse files Browse the repository at this point in the history
  • Loading branch information
sarahelsaig committed Apr 23, 2024
1 parent 5d86e62 commit 346271e
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 28 deletions.
116 changes: 89 additions & 27 deletions Lombiq.JsonEditor/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using AngleSharp.Common;
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection;
using Lombiq.HelpfulLibraries.OrchardCore.Validation;
using Lombiq.JsonEditor.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -9,31 +9,38 @@
using Microsoft.Extensions.Localization;
using OrchardCore.Admin;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Handlers;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.Contents;
using OrchardCore.Contents.Controllers;
using OrchardCore.DisplayManagement;
using OrchardCore.DisplayManagement.Layout;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.DisplayManagement.Title;
using OrchardCore.Title.ViewModels;
using System;
using System.Linq;
using System.Net;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Settings;
using System.Threading.Tasks;

namespace Lombiq.JsonEditor.Controllers;

public class AdminController : Controller
{
private static readonly JsonMergeSettings _updateJsonMergeSettings = new()
{
MergeArrayHandling = MergeArrayHandling.Replace,
};

private readonly IAuthorizationService _authorizationService;
private readonly IContentManager _contentManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly ILayoutAccessor _layoutAccessor;
private readonly INotifier _notifier;
private readonly IPageTitleBuilder _pageTitleBuilder;
private readonly IShapeFactory _shapeFactory;
private readonly Lazy<ApiController> _contentApiControllerLazy;
private readonly IStringLocalizer<AdminController> T;
private readonly IHtmlLocalizer<AdminController> H;

Expand All @@ -43,8 +50,7 @@ public AdminController(
INotifier notifier,
IPageTitleBuilder pageTitleBuilder,
IShapeFactory shapeFactory,
IOrchardServices<AdminController> services,
Lazy<ApiController> contentApiControllerLazy)
IOrchardServices<AdminController> services)
{
_authorizationService = services.AuthorizationService.Value;
_contentManager = services.ContentManager.Value;
Expand All @@ -53,7 +59,6 @@ public AdminController(
_notifier = notifier;
_pageTitleBuilder = pageTitleBuilder;
_shapeFactory = shapeFactory;
_contentApiControllerLazy = contentApiControllerLazy;
T = services.StringLocalizer.Value;
H = services.HtmlLocalizer.Value;
}
Expand Down Expand Up @@ -132,34 +137,91 @@ public async Task<IActionResult> EditPost(
private Task<bool> CanEditAsync(ContentItem contentItem) =>
_authorizationService.AuthorizeAsync(User, CommonPermissions.EditContent, contentItem);

private async Task<IActionResult> UpdateContentAsync(ContentItem contentItem, bool isDraft)
private Task<IActionResult> UpdateContentAsync(ContentItem contentItem, bool isDraft) =>
PostContentAsync(contentItem, isDraft);

private static bool IsContinue(string submitString) =>
submitString?.EndsWithOrdinalIgnoreCase("AndContinue") == true;

private static string GetName(ContentItem contentItem) =>
string.IsNullOrWhiteSpace(contentItem.DisplayText)
? contentItem.ContentType
: $"\"{contentItem.DisplayText}\"";

// Based on the OrchardCore.Contents.Controllers.ApiController.Post action that was deleted in
// https://github.com/OrchardCMS/OrchardCore/commit/d524386b2f792f35773324ae482247e80a944266 to replace with minimal
// APIs that can't be reused the same way.
private async Task<IActionResult> PostContentAsync(ContentItem model, bool draft)
{
// The Content API Controller requires the AccessContentApi permission. As this isn't an external API request it
// doesn't make sense to require this permission. So we create a temporary claims principal and explicitly grant
// the permission.
var currentUser = User;
HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity(User.Claims.Concat(Permissions.AccessContentApi)));
// 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.

if (await _contentManager.GetAsync(model.ContentItemId, VersionOptions.DraftRequired) is { } contentItem)
{
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 (CheckContentValidationResult(result) is { } problem) return problem;
}
else
{
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);

try
if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.PublishContent, contentItem))
{
return this.ChallengeOrForbid("Api");
}

contentItem.Merge(model);

var result = await _contentManager.UpdateValidateAndCreateAsync(contentItem, VersionOptions.Draft);
if (CheckContentValidationResult(result) is { } problem) return problem;
}

if (draft)
{
// Here the API controller is called directly. The behavior is the same as if we sent a POST request using an
// HTTP client (except the permission bypass above), but it's faster and more resource-efficient.
var contentApiController = _contentApiControllerLazy.Value;
contentApiController.ControllerContext.HttpContext = HttpContext;
return await contentApiController.Post(contentItem, isDraft);
await _contentManager.SaveDraftAsync(contentItem);
}
finally
else
{
// Ensure that the original claims principal is restored, just in case.
HttpContext.User = currentUser;
await _contentManager.PublishAsync(contentItem);
}

return Ok(contentItem);
}

private static bool IsContinue(string submitString) =>
submitString?.EndsWithOrdinalIgnoreCase("AndContinue") == true;
private ActionResult CheckContentValidationResult(ContentValidateResult result)
{
if (!result.Succeeded)
{
// Add the validation results to the ModelState to present the errors as part of the response.
result.AddValidationErrorsToModelState(ModelState);
}

private static string GetName(ContentItem contentItem) =>
string.IsNullOrWhiteSpace(contentItem.DisplayText)
? contentItem.ContentType
: $"\"{contentItem.DisplayText}\"";
// 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 = T["One or more validation errors occurred."],
Detail = string.Join(", ", ModelState.Values.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))),
Status = (int)HttpStatusCode.BadRequest,
});
}

return null;
}
}
1 change: 0 additions & 1 deletion Lombiq.JsonEditor/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IContentDisplayDriver, EditJsonActionsMenuContentDisplayDriver>();
services.AddOrchardServices();
services.AddScoped<ApiController>();
services.AddContentSecurityPolicyProvider<JsonEditorContentSecurityPolicyProvider>();
}
}

0 comments on commit 346271e

Please sign in to comment.