Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OSOE-795: Upgrade to latest OC preview to test System.Text.Json #59

Merged
merged 21 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
126 changes: 94 additions & 32 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
{
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 (string.IsNullOrWhiteSpace(contentItemId) ||
Expand All @@ -78,7 +83,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 @@ -92,7 +97,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 @@ -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(state => state.Errors.Select(error => error.ErrorMessage))),
Status = (int)HttpStatusCode.BadRequest,
});
}

return null;
}
}
18 changes: 2 additions & 16 deletions Lombiq.JsonEditor/Drivers/JsonFieldDisplayDriver.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
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;
Expand Down Expand Up @@ -42,7 +41,7 @@ public override async Task<IDisplayResult> UpdateAsync(JsonField field, IUpdateM

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."]);
}
Expand All @@ -53,17 +52,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;
}
}
}
12 changes: 6 additions & 6 deletions Lombiq.JsonEditor/Lombiq.JsonEditor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
</ItemGroup>

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

<ItemGroup Condition="'$(NuGetBuild)' != 'true'">
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")]
sarahelsaig marked this conversation as resolved.
Show resolved Hide resolved
[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;
}
10 changes: 6 additions & 4 deletions Lombiq.JsonEditor/Settings/JsonFieldSettingsDriver.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using Lombiq.JsonEditor.Fields;
using Lombiq.JsonEditor.Models;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json;
using OrchardCore.ContentManagement.Metadata.Builders;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentTypes.Editors;
using OrchardCore.DisplayManagement.Views;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Lombiq.JsonEditor.Settings;
Expand All @@ -16,8 +18,8 @@ public class JsonFieldSettingsDriver : ContentPartFieldDefinitionDisplayDriver<J
public JsonFieldSettingsDriver(IStringLocalizer<JsonFieldSettingsDriver> stringLocalizer) => T = stringLocalizer;

public override IDisplayResult Edit(ContentPartFieldDefinition model) =>
Initialize<JsonFieldSettings>($"{nameof(JsonFieldSettings)}_Edit", model.PopulateSettings)
.Location("Content");
Initialize<JsonFieldSettings>($"{nameof(JsonFieldSettings)}_Edit", model.CopySettingsTo)
.PlaceInContent();

public override async Task<IDisplayResult> UpdateAsync(
ContentPartFieldDefinition model,
Expand All @@ -28,7 +30,7 @@ public override async Task<IDisplayResult> UpdateAsync(

try
{
JsonConvert.DeserializeObject<JsonEditorOptions>(settings.JsonEditorOptions);
JsonNode.Parse(settings.JsonEditorOptions).ToObject<JsonEditorOptions>();
context.Builder.WithSettings(settings);
}
catch (JsonException)
Expand Down
4 changes: 0 additions & 4 deletions Lombiq.JsonEditor/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection;
using Lombiq.HelpfulLibraries.OrchardCore.Mvc;
using Lombiq.JsonEditor.Constants;
using Lombiq.JsonEditor.Drivers;
using Lombiq.JsonEditor.Fields;
Expand All @@ -11,7 +10,6 @@
using OrchardCore.Admin;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.Contents.Controllers;
using OrchardCore.ContentTypes.Editors;
using OrchardCore.Modules;
using OrchardCore.ResourceManagement;
Expand Down Expand Up @@ -41,8 +39,6 @@ public override void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IContentDisplayDriver, EditJsonActionsMenuContentDisplayDriver>();
services.AddOrchardServices();
services.AddScoped<ApiController>();
services.AddContentSecurityPolicyProvider<JsonEditorContentSecurityPolicyProvider>();
AdminRouteAttributeRouteMapper.AddToServices(services);
}
}
Loading