-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
LMBQ-124: Orchard 1 Recipe Migration feature
- Loading branch information
Showing
20 changed files
with
1,838 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
module.exports = { | ||
// Setting root=true prevents ESLint from taking into account .eslintrc files higher up in the directory tree. | ||
root: true, | ||
|
||
// The following path may have to be adjusted to your directory structure. | ||
extends: './node_modules/nodejs-extensions/config/.eslintrc.lombiq-base.js', | ||
|
||
// Add custom rules and overrides here. | ||
rules: { | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = { | ||
// The following path may have to be adjusted to your directory structure. | ||
extends: './node_modules/nodejs-extensions/config/.stylelintrc.lombiq-base.js', | ||
|
||
// Add custom rules and overrides here. | ||
rules: { | ||
}, | ||
}; |
61 changes: 61 additions & 0 deletions
61
Lombiq.HelpfulExtensions/Controllers/OrchardRecipeMigrationAdminController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
using AngleSharp.Io; | ||
using Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.Localization; | ||
using OrchardCore.Admin; | ||
using OrchardCore.DisplayManagement.Notify; | ||
using OrchardCore.Modules; | ||
using System; | ||
using System.IO; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Controllers; | ||
|
||
[Admin] | ||
[Feature(FeatureIds.OrchardRecipeMigration)] | ||
public class OrchardRecipeMigrationAdminController : Controller | ||
{ | ||
private readonly INotifier _notifier; | ||
private readonly IOrchardExportToRecipeConverter _converter; | ||
private readonly IHtmlLocalizer<OrchardRecipeMigrationAdminController> H; | ||
public OrchardRecipeMigrationAdminController( | ||
INotifier notifier, | ||
IOrchardExportToRecipeConverter converter, | ||
IHtmlLocalizer<OrchardRecipeMigrationAdminController> localizer) | ||
{ | ||
_notifier = notifier; | ||
_converter = converter; | ||
H = localizer; | ||
} | ||
|
||
public IActionResult Index() => View(); | ||
|
||
[HttpPost] | ||
[ValidateAntiForgeryToken] | ||
public async Task<IActionResult> Convert(IFormFile file) | ||
{ | ||
Stream stream; | ||
string json; | ||
|
||
try | ||
{ | ||
stream = file.OpenReadStream(); | ||
} | ||
catch (Exception) | ||
{ | ||
await _notifier.ErrorAsync(H["Please add a file to import."]); | ||
return Redirect(nameof(Index)); | ||
} | ||
|
||
await using (stream) | ||
{ | ||
json = await _converter.ConvertAsync(XDocument.Load(stream)); | ||
} | ||
|
||
Response.Headers.Add("Content-Disposition", "attachment;filename=export.recipe.json"); | ||
return Content(json, MimeTypeNames.ApplicationJson, Encoding.UTF8); | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Models/OrchardIds.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
using OrchardCore.ContentManagement; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Models; | ||
|
||
/// <summary> | ||
/// Stores Orchard 1 ID and container ID. | ||
/// </summary> | ||
public class OrchardIds : ContentPart | ||
{ | ||
public string ExportId { get; set; } | ||
public string Parent { get; set; } | ||
} |
35 changes: 35 additions & 0 deletions
35
Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Navigation/AdminMenu.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Controllers; | ||
using Lombiq.HelpfulLibraries.OrchardCore.Navigation; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.Localization; | ||
using OrchardCore.Navigation; | ||
using System; | ||
using System.Threading.Tasks; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Navigation; | ||
|
||
public class AdminMenu : INavigationProvider | ||
{ | ||
private readonly IHttpContextAccessor _hca; | ||
private readonly IStringLocalizer T; | ||
|
||
public AdminMenu(IHttpContextAccessor hca, IStringLocalizer<AdminMenu> stringLocalizer) | ||
{ | ||
_hca = hca; | ||
T = stringLocalizer; | ||
} | ||
|
||
public Task BuildNavigationAsync(string name, NavigationBuilder builder) | ||
{ | ||
if (!name.EqualsOrdinalIgnoreCase("admin")) return Task.CompletedTask; | ||
|
||
builder.Add(T["Configuration"], configuration => configuration | ||
.Add(T["Import/Export"], importExport => importExport | ||
.Add(T["Orchard 1 Recipe Migration"], T["Orchard 1 Recipe Migration"], migration => migration | ||
.Action<OrchardRecipeMigrationAdminController>(_hca.HttpContext, controller => controller.Index()) | ||
.LocalNav() | ||
))); | ||
|
||
return Task.CompletedTask; | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
...fulExtensions/Extensions/OrchardRecipeMigration/Services/CommonOrchardContentConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
using Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Models; | ||
using OrchardCore.Alias.Models; | ||
using OrchardCore.Autoroute.Models; | ||
using OrchardCore.ContentManagement; | ||
using OrchardCore.Html.Models; | ||
using OrchardCore.Title.Models; | ||
using System; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
public class CommonOrchardContentConverter : IOrchardContentConverter | ||
{ | ||
public int Order => 0; | ||
|
||
public bool IsApplicable(XElement element) => true; | ||
|
||
public Task ImportAsync(XElement element, ContentItem contentItem) | ||
{ | ||
bool HasElementAttribute(string elementName, string attributeName, out string value) | ||
{ | ||
value = element.Element(elementName)?.Attribute(attributeName)?.Value.Trim(); | ||
return !string.IsNullOrWhiteSpace(value); | ||
} | ||
|
||
var exportId = element.Attribute("Id")?.Value.Trim(); | ||
contentItem.Alter<OrchardIds>(ids => ids.ExportId = exportId); | ||
if (exportId?.StartsWithOrdinal("/alias=") == true) | ||
{ | ||
AlterIfPartExists<AliasPart>(contentItem, part => part.Alias = GetAlias(exportId)); | ||
} | ||
|
||
if (element.Attribute("Status")?.Value == "Published") | ||
{ | ||
contentItem.Published = true; | ||
contentItem.Latest = true; | ||
} | ||
|
||
ImportCommonPart(contentItem, element); | ||
|
||
if (element.Element("AutoroutePart") is { } autoroutePart && | ||
autoroutePart.Attribute("Alias")?.Value is { } autoroutePartAlias) | ||
{ | ||
AlterIfPartExists<AutoroutePart>(contentItem, part => | ||
{ | ||
part.Path = autoroutePartAlias; | ||
part.SetHomepage = autoroutePart.Attribute("PromoteToHomePage")?.Value == "true"; | ||
}); | ||
} | ||
|
||
if (HasElementAttribute("BodyPart", "Text", out var body)) | ||
{ | ||
AlterIfPartExists<HtmlBodyPart>(contentItem, part => part.Html = body); | ||
} | ||
|
||
if (HasElementAttribute("TitlePart", "Title", out var title)) | ||
{ | ||
contentItem.DisplayText = title; | ||
AlterIfPartExists<TitlePart>(contentItem, part => part.Title = title); | ||
} | ||
|
||
if (HasElementAttribute("IdentityPart", "Identifier", out var id)) | ||
{ | ||
if (id.Length < 26) id = new StringBuilder(id).Append('0', 26 - id.Length).ToString(); | ||
contentItem.ContentItemId = id[..26]; | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private static string GetAlias(string value) => value.Split("/alias=").Last().Trim().Replace("\\/", "/"); | ||
|
||
private static void AlterIfPartExists<TPart>(ContentItem contentItem, Action<TPart> action) | ||
where TPart : ContentPart, new() | ||
{ | ||
if (contentItem.Has<TPart>()) contentItem.Alter(action); | ||
} | ||
|
||
public static void ImportCommonPart(ContentItem contentItem, XElement parentElement) | ||
{ | ||
if (parentElement.Element("CommonPart") is not { } commonPart) return; | ||
|
||
string Attribute(string name) => commonPart.Attribute(name)?.Value; | ||
|
||
DateTime? Date(string name) => DateTime.TryParse(Attribute(name), out var date) ? date : null; | ||
|
||
if (Attribute("Owner")?.Replace("/User.UserName=", string.Empty) is { } owner) | ||
{ | ||
contentItem.Author = owner; | ||
} | ||
|
||
contentItem.CreatedUtc = Date("CreatedUtc") ?? contentItem.CreatedUtc; | ||
contentItem.PublishedUtc = Date("PublishedUtc") ?? contentItem.PublishedUtc; | ||
contentItem.ModifiedUtc = Date("ModifiedUtc") ?? contentItem.ModifiedUtc; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...nsions/Extensions/OrchardRecipeMigration/Services/GraphMetadataOrchardContentConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using GraphQL.Execution; | ||
using OrchardCore.ContentManagement; | ||
using System; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
public class GraphMetadataOrchardContentConverter : IOrchardContentConverter | ||
{ | ||
public const string GraphMetadata = nameof(GraphMetadata); | ||
|
||
public bool IsApplicable(XElement element) => element.Element(GraphMetadata) != null; | ||
|
||
public Task ImportAsync(XElement element, ContentItem contentItem) | ||
{ | ||
contentItem.ContentItemId = ToContentItemId( | ||
element.Element(GraphMetadata)!.Attribute("NodeId")?.Value, | ||
element); | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
public static string ToContentItemId(string value, XElement element) | ||
{ | ||
var id = value.ToTechnicalInt(); | ||
if (id < 0) throw new InvalidOperationError($"Missing or corrupted {GraphMetadata} node ID.\n{element}"); | ||
return "assoc" + id.PadZeroes(21); | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
....HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/IOrchardContentConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
using OrchardCore.ContentManagement; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
/// <summary> | ||
/// A converter used to port information from a <Content> item from an Orchard 1 XML export into an Orchard Core | ||
/// <see cref="ContentItem"/>. | ||
/// </summary> | ||
public interface IOrchardContentConverter | ||
{ | ||
/// <summary> | ||
/// Gets the sorting order number, lower numbers are used first. | ||
/// </summary> | ||
int Order => 10; | ||
|
||
/// <summary> | ||
/// Returns <see langword="true"/> if this converter can use the given <paramref name="element"/>. | ||
/// </summary> | ||
bool IsApplicable(XElement element); | ||
|
||
/// <summary> | ||
/// Returns a new <see cref="ContentItem"/> created for this element, or <see langword="null"/> if this converter | ||
/// can't create a content item for the given <paramref name="element"/>. If all converters returned <see | ||
/// langword="null"/>, <see cref="IContentManager.NewAsync"/> is used with the <paramref name="element"/>'s <see | ||
/// cref="XElement.Name"/> as the content type. | ||
/// </summary> | ||
Task<ContentItem> CreateContentItemAsync(XElement element) => Task.FromResult<ContentItem>(null); | ||
|
||
/// <summary> | ||
/// Processes further content in the <paramref name="element"/> to fill the <paramref name="contentItem"/>. | ||
/// </summary> | ||
Task ImportAsync(XElement element, ContentItem contentItem); | ||
} |
18 changes: 18 additions & 0 deletions
18
...q.HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/IOrchardExportConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using OrchardCore.ContentManagement; | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
/// <summary> | ||
/// A converter for the whole Orchard 1 XML export file. | ||
/// </summary> | ||
public interface IOrchardExportConverter | ||
{ | ||
/// <summary> | ||
/// Updates the collection of <paramref name="contentItems"/> using data from the whole XML file. This is invoked | ||
/// before the recipe steps are serialized. | ||
/// </summary> | ||
Task UpdateContentItemsAsync(XDocument document, IList<ContentItem> contentItems); | ||
} |
18 changes: 18 additions & 0 deletions
18
...lExtensions/Extensions/OrchardRecipeMigration/Services/IOrchardExportToRecipeConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
/// <summary> | ||
/// Converts old Orchard 1 export files into Orchard Core recipe files. Besides the built-in content conversion for some | ||
/// common parts, its functionality can be expanded by registering additional services that implement <see | ||
/// cref="IOrchardContentConverter"/> or <see cref="IOrchardExportConverter"/> recipes. | ||
/// </summary> | ||
public interface IOrchardExportToRecipeConverter | ||
{ | ||
/// <summary> | ||
/// Returns a JSON string that contains an Orchard Core recipe file based on the provided Orchard 1 <paramref | ||
/// name="export"/> XML. | ||
/// </summary> | ||
Task<string> ConvertAsync(XDocument export); | ||
} |
38 changes: 38 additions & 0 deletions
38
...ulExtensions/Extensions/OrchardRecipeMigration/Services/ListPartOrchardExportConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
using Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Models; | ||
using OrchardCore.ContentManagement; | ||
using OrchardCore.Lists.Models; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using System.Xml.Linq; | ||
|
||
namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Services; | ||
|
||
/// <summary> | ||
/// A post-processing converter that looks for added <see cref="OrchardIds"/> content parts in the prepared list of | ||
/// content items where the <see cref="OrchardIds.Parent"/> is set by custom converters. If the content item of the | ||
/// parent ID has <see cref="ListPart"/> then the child is assigned to its list. | ||
/// </summary> | ||
public class ListPartOrchardExportConverter : IOrchardExportConverter | ||
{ | ||
public Task UpdateContentItemsAsync(XDocument document, IList<ContentItem> contentItems) | ||
{ | ||
var itemsById = contentItems | ||
.Where(item => !string.IsNullOrEmpty(item.As<OrchardIds>()?.ExportId)) | ||
.ToDictionary(item => item.As<OrchardIds>().ExportId); | ||
|
||
foreach (var item in itemsById.Values.Where(item => !string.IsNullOrEmpty(item.As<OrchardIds>().Parent))) | ||
{ | ||
var parentId = item.As<OrchardIds>().Parent; | ||
if (!itemsById.TryGetValue(parentId, out var parent) || !parent.Has<ListPart>()) continue; | ||
|
||
item.Alter<ContainedPart>(part => | ||
{ | ||
part.ListContentItemId = parent.ContentItemId; | ||
part.ListContentType = parent.ContentType; | ||
}); | ||
} | ||
|
||
return Task.CompletedTask; | ||
} | ||
} |
Oops, something went wrong.