Skip to content

Commit

Permalink
Merge pull request #67 from Lombiq/issue/OSOE-818
Browse files Browse the repository at this point in the history
OSOE-818: Upgrade to Orchard Core 2.0
  • Loading branch information
sarahelsaig authored Sep 25, 2024
2 parents c804c5c + cbd15b4 commit 6eac2f7
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 109 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using Atata;
using Lombiq.Tests.UI.Extensions;
using Lombiq.Tests.UI.Services;
using Newtonsoft.Json.Linq;
using OpenQA.Selenium;
using Shouldly;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Lombiq.JsonEditor.Tests.UI.Extensions;
Expand Down Expand Up @@ -139,7 +139,7 @@ private static async Task TestTreeStyleModeAsync(this UITestContext context)
private static void TestCodeStyleMode(this UITestContext context)
{
// This field is hidden, but its content reflects what's in the editor.
var editorContent = JObject
var editorContent = JsonNode
.Parse(context.Get(By.XPath($"//input[@class='jsonEditor__input']").OfAnyVisibility())
.GetValue());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="10.0.1" />
<PackageReference Include="Lombiq.Tests.UI" Version="11.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
128 changes: 95 additions & 33 deletions Lombiq.JsonEditor/Controllers/AdminController.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
using AngleSharp.Common;
using Lombiq.HelpfulLibraries.OrchardCore.Contents;
using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection;
using Lombiq.HelpfulLibraries.OrchardCore.Mvc;
using Lombiq.HelpfulLibraries.OrchardCore.Validation;
using Lombiq.JsonEditor.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
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
public sealed 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,12 +59,11 @@ public AdminController(
_notifier = notifier;
_pageTitleBuilder = pageTitleBuilder;
_shapeFactory = shapeFactory;
_contentApiControllerLazy = contentApiControllerLazy;
T = services.StringLocalizer.Value;
H = services.HtmlLocalizer.Value;
}

[AdminRoute("Contents/ContentItems/{contentItemId}/Edit/Json")]
[Admin("Contents/ContentItems/{contentItemId}/Edit/Json")]
public async Task<IActionResult> Edit(string contentItemId)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
Expand All @@ -80,7 +85,7 @@ await _contentManager.GetAsync(contentItemId, VersionOptions.Latest) is not { }
await _layoutAccessor.AddShapeToZoneAsync("Title", titleShape);

var definition = await _contentDefinitionManager.GetTypeDefinitionAsync(contentItem.ContentType);
return View(new EditContentItemViewModel(contentItem, definition, JsonConvert.SerializeObject(contentItem)));
return View(new EditContentItemViewModel(contentItem, definition, JsonSerializer.Serialize(contentItem)));
}

[ValidateAntiForgeryToken]
Expand All @@ -96,7 +101,7 @@ public async Task<IActionResult> EditPost(

if (string.IsNullOrWhiteSpace(contentItemId) ||
string.IsNullOrWhiteSpace(json) ||
JsonConvert.DeserializeObject<ContentItem>(json) is not { } contentItem)
JsonSerializer.Deserialize<ContentItem>(json) is not { } contentItem)
{
return NotFound();
}
Expand Down Expand Up @@ -136,34 +141,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(state => state.Errors.Select(error => error.ErrorMessage))),
Status = (int)HttpStatusCode.BadRequest,
});
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Lombiq.JsonEditor.Drivers;

public class EditJsonActionsMenuContentDisplayDriver : ContentDisplayDriver
public sealed class EditJsonActionsMenuContentDisplayDriver : ContentDisplayDriver
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _hca;
Expand Down
30 changes: 7 additions & 23 deletions Lombiq.JsonEditor/Drivers/JsonFieldDisplayDriver.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
using Lombiq.HelpfulLibraries.Common.Utilities;
using Lombiq.JsonEditor.Fields;
using Lombiq.JsonEditor.ViewModels;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
using OrchardCore.DisplayManagement.ModelBinding;
using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using System.Threading.Tasks;

namespace Lombiq.JsonEditor.Drivers;

public class JsonFieldDisplayDriver : ContentFieldDisplayDriver<JsonField>
public sealed class JsonFieldDisplayDriver : ContentFieldDisplayDriver<JsonField>
{
private readonly IStringLocalizer T;

Expand All @@ -36,15 +35,13 @@ public override IDisplayResult Edit(JsonField field, BuildFieldEditorContext con
model.PartFieldDefinition = context.PartFieldDefinition;
});

public override async Task<IDisplayResult> UpdateAsync(JsonField field, IUpdateModel updater, UpdateFieldEditorContext context)
public override async Task<IDisplayResult> UpdateAsync(JsonField field, UpdateFieldEditorContext context)
{
var model = new EditJsonFieldViewModel();
var model = await context.CreateModelAsync<EditJsonFieldViewModel>(Prefix);

if (!await updater.TryUpdateModelAsync(model, Prefix)) return await EditAsync(field, context);

if (!TryParse(model.Value))
if (JsonHelpers.ValidateJsonIfNotNull(model.Value) == false)
{
updater.ModelState.AddModelError(Prefix, T["The input isn't a valid JSON entity."]);
context.Updater.ModelState.AddModelError(Prefix, T["The input isn't a valid JSON entity."]);
}
else
{
Expand All @@ -53,17 +50,4 @@ public override async Task<IDisplayResult> UpdateAsync(JsonField field, IUpdateM

return await EditAsync(field, context);
}

private static bool TryParse(string value)
{
try
{
JObject.Parse(value);
return true;
}
catch (JsonException)
{
return false;
}
}
}
14 changes: 7 additions & 7 deletions Lombiq.JsonEditor/Lombiq.JsonEditor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="OrchardCore.Module.Targets" Version="1.8.0" />
<PackageReference Include="OrchardCore.Contents" Version="1.8.0" />
<PackageReference Include="OrchardCore.ContentManagement" Version="1.8.0" />
<PackageReference Include="OrchardCore.ContentTypes.Abstractions" Version="1.8.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="1.8.0" />
<PackageReference Include="OrchardCore.ContentFields" Version="1.8.0" />
<PackageReference Include="OrchardCore.Module.Targets" Version="2.0.0" />
<PackageReference Include="OrchardCore.Contents" Version="2.0.0" />
<PackageReference Include="OrchardCore.ContentManagement" Version="2.0.0" />
<PackageReference Include="OrchardCore.ContentTypes.Abstractions" Version="2.0.0" />
<PackageReference Include="OrchardCore.DisplayManagement" Version="2.0.0" />
<PackageReference Include="OrchardCore.ContentFields" Version="2.0.0" />
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
Expand All @@ -49,7 +49,7 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="10.0.0" />
<PackageReference Include="Lombiq.HelpfulLibraries.OrchardCore" Version="11.0.0" />
<PackageReference Include="Lombiq.NodeJs.Extensions" Version="2.1.0" />
</ItemGroup>

Expand Down
5 changes: 3 additions & 2 deletions Lombiq.JsonEditor/Models/JsonEditorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Mvc.Localization;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace Lombiq.JsonEditor.Models;

Expand All @@ -29,7 +29,8 @@ public class JsonEditorOptions
/// </summary>
public bool History { get; set; } = true;

[JsonProperty("mode")]
[JsonPropertyName("mode")]
[JsonInclude]
private string ModeString { get; set; } = "tree";

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion Lombiq.JsonEditor/Settings/JsonFieldSettings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
using Lombiq.HelpfulLibraries.Common.Utilities;

namespace Lombiq.JsonEditor.Settings;

public class JsonFieldSettings
public class JsonFieldSettings : ICopier<JsonFieldSettings>
{
public string JsonEditorOptions { get; set; }

public void CopyTo(JsonFieldSettings target) => target.JsonEditorOptions = JsonEditorOptions;
}
Loading

0 comments on commit 6eac2f7

Please sign in to comment.