Skip to content

Instantly share code, notes, and snippets.

@abjerner
Last active February 24, 2023 08:10
Show Gist options
  • Save abjerner/b2844b7ec83fbaba6db444ac50892a2d to your computer and use it in GitHub Desktop.
Save abjerner/b2844b7ec83fbaba6db444ac50892a2d to your computer and use it in GitHub Desktop.

MigrationsService

To convert an existing grid value to a new block list value with corresponding blocks, you can use the MigrationsService, which has a Convert(GridDataModel,object?) method.

The first parameter is an instance of GridDataModel from my Skybrud.Umbraco.GridData package. If you don't already have parsed the grid value to a GridDataModel, you can use the package's IGridFactory to create the model via _gridFactory.CreateGridModel(null, null, gridData, false).

The second parameter is an optional owner value (eg. a reference to the page that holds the original grid value). I'm not using this parameter in my examples, but it might be relevant to have if grid controls are also based on data from the page holding the grid value.

The Convert method iterates through the grid, and then checks the type of each grid control through a switch case statement, which will delegate the conversion of each grid editor to a separate methods (helps keeping the code clean 😄). If the method encounters a grid editor it hasn't yet been implemented to recognize, it will throw an exception. I've used a try and error approach, so if throws an exception, I know there is a new scenario that I need to handle.

For this Gist, I'm converting three different types of grid editors - a rich text grid editor, a video and then a quote.

Quote

The ConvertQuote is used to convert a quote based grid control to a corresponding BlockQuote model. Starting out, the method will read out the raw values from the original JSON object. Even if you had strongly typed grid models for the old site, you it might not make sense to copy them over to the new site, which is why I've gone for the raw approach.

After validating that the grid control actually had a value for the quote, I'm creating a new object for the conntet data via _blockListFactory.CreateContentData<BlockQuote>(control). This ensures that the content data is created with the correct content type. This method returns an instance of BlockListContentData, which we can then use to add new properties to. Eg:

var content = _blockListFactory
    .CreateContentData<BlockQuote>(control)
    .Add("quote", quote)
    .Add("origin", byline);

Most if not all our blocks also has a default settings type - eg. where users can hide the block from the frontend (#togglegate). So we can create a new settings data instance like:

var settings = _blockListFactory
    .CreateDefaultSettings(control);

If you have a specific settings type you wish to use, there is also a CreateSettingsData<MySettings>(control) method in the block list factory.

Finally we need to add the content and settings data as a new item to the block list:

blockList.AddItem(content, settings);

which is shorthand for writing:

blockList.Items.Add(new BlockListItem(content, settings));

Rich Text Editor

The RTE based grid editor is a bit more complex, as it may contain reference to both content and media. For my case, I didn't really need to handle content references, but I did need to handle media references.

In Umbraco 7 and 8, media URLs would have a format like /media/{id}/file.ext, but from Umbraco 9 and up, the format is instead /media/{hash}/file.ext. If the migration doesn't convert these references, they will no longer work.

Notice that I've had a seperate migration of media not covered in Gist, so all the media files are in their new, proper locatons (hash based folder structure).

Media references can be both links (<a>) and images (<img>), so my ConvertRte method handles both.

Video

In my case, the old site had a video based grid editor for inserting YouTube videos. The editor supported adding multiple videos, but was configured to only allow one video per grid control. The ConvertVideo method will therefore throw an exception if it encounters multiple videos in the same control.

On the new site, we're using my Limbo.Umbraco.YouTube package, so we've already added the necessary credentials to appsettings.json.

Then via GoogleHttpService (from Skybrud.Social.Google) and .YouTube() (from Skybrud.Social.Google.YouTube), I can fetch information about the video from the YouTube video and save it in the same format as if using the Limbo.Umbraco.YouTube package directly.

In our case, users can also add a bit of extra information with the video, so we a block list in a block list, where the outer block has all the additonal information as well as a property with a new block list for the video.

BlockListFactory

A part of what makes this work, is also the BlockListFactory class and related models. For instance, the factory contains the CreateContentData<T>(Guid key) and CreateContentData<T>(GridControl control) methods, which will create a new BlockListContentData instance representing the content data.

The method is generic, meaning that it will use <T> for finding the correct content type. To make this work, T should be a model extending PublishedElementModel, and ideally be generated by either my Limbo.Umbraco.ModelsBuilder, or the Models builder that ships with Umbraco.

In a similar way, the factory also has the CreateSettingsData<T>(Guid key) and CreateSettingsData<T>(GridControl control) methods for creating new instances of BlockListSettingsData representing the settings data.

BlockListContentData

Each content data and settings data must have unique UDIs. If you have duplicate UDIs across different pages, the content may start to leak from one page to another, which isn't ideal.

So to ensure unique UDIs, you can use the Guid.NewGuid() method for the basis for the UDIs. In my case, I neeed to handle the conversion in a reproduceable way. Eg. if the input hasn't changed, the output should change either. Using Guid.NewGuid() would prevent that from working.

In the old grid model, the individual grid controls don't have any information that uniquely identifies them, but each row does have a unique GUID key, which we can use as a basis for creating unique GUIDs. But as a grid row may contain multiple grid areas, and each area may contain multiple grid controls, we also need to take into account.

For the site I was migrating, the grid was configured in a way where it wasn't possible to add multiple areas to a row, I could skip checking for this. But is was still possibel to add multiple controls to an area. So the BlockListContentData(GridControl,IPublishedContentType) constructor will use the control's index in the parent's list of controls to generate unique GUID keys.

int index = control.Area.Controls.IndexOf(control);
Guid key = index == 0 ? Guid.Parse(control.Row.Id) : SecurityUtils.GetMd5Guid(control.Row.Id + "#" + index);

The first control re-uses the GUID key of the grid row, while the key for any additional control is based on the MD5 hash of a combination of the GUID key of the grid row and the control's index in the area's list of controls. And luckily MD5 hashes can be represented as a GUID.

using System;
using System.Collections.Generic;
using Skybrud.Essentials.Security;
using Skybrud.Umbraco.GridData.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core;
namespace code.Features.BlockList.Models {
/// <summary>
/// Class representing the content data object of a block list item.
/// </summary>
public class BlockListContentData {
#region Properties
/// <summary>
/// Gets the content type of the settings data.
/// </summary>
public IPublishedContentType ContentType { get; }
/// <summary>
/// Gets the UDI of the settings data.
/// </summary>
public GuidUdi Udi { get; }
/// <summary>
/// Gets a dictionary with the properties of the settings data.
/// </summary>
public Dictionary<string, object?> Properties { get; set; } = new();
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance based on the specified <paramref name="key"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="key">The unique key that will be used to form the UDI of the content data.</param>
/// <param name="contentType">The content type of the content data.</param>
public BlockListContentData(Guid key, IPublishedContentType contentType) {
Udi = new GuidUdi("element", key);
ContentType = contentType;
}
/// <summary>
/// Initializes a new instance based on the specified grid <paramref name="control"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="control">The grid control to be converted.</param>
/// <param name="contentType">The content type of the content data.</param>
public BlockListContentData(GridControl control, IPublishedContentType contentType) {
// We need to ensure that each content item has a unique key. Generally we should be able to use the same
// key as the grid row, but even though this shouldn't be allowed in the legacy site, so rows have more
// than one control, in which case we creatively need to generate a unique key for those additional
// controls. Notice that is's important that the calculated key is the same if we repeat it again and again
// again
int index = control.Area.Controls.IndexOf(control);
Guid key = index == 0 ? Guid.Parse(control.Row.Id) : SecurityUtils.GetMd5Guid(control.Row.Id + "#" + index);
// Create an UDI based on the element type and the GUID key
Udi = new GuidUdi("element", key);
// Set the content type
ContentType = contentType;
}
#endregion
#region Member methods
/// <summary>
/// Adds a property with the specified <paramref name="alias"/> and <paramref name="value"/>.
/// </summary>
/// <param name="alias">The alias of the property.</param>
/// <param name="value">The value of the property.</param>
/// <returns>Returns the instance itself - useful for method chaining.</returns>
public BlockListContentData Add(string alias, object? value) {
Properties.Add(alias, value);
return this;
}
#endregion
}
}
using System;
using code.Features.BlockList.Models;
using code.Features.BlockList.Models.Generated;
using Skybrud.Umbraco.GridData.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
namespace code.Features.BlockList {
public class BlockListFactory {
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
#region Constructors
public BlockListFactory(IPublishedSnapshotAccessor publishedSnapshotAccessor) {
_publishedSnapshotAccessor = publishedSnapshotAccessor;
}
#endregion
#region Member methods
/// <summary>
/// Creates and returns a new <see cref="BlockListContentData"/> instance based on the specified <paramref name="key"/>.
/// </summary>
/// <typeparam name="T">The type of the content data.</typeparam>
/// <param name="key">A unique GUID key that will be used to form the UDI of the content data.</param>
/// <returns>An instance of <see cref="BlockListContentData"/> representing the created content data.</returns>
public virtual BlockListContentData CreateContentData<T>(Guid key) where T : PublishedElementModel {
return new BlockListContentData(key, GetModelType<T>());
}
/// <summary>
/// Creates and returns a new <see cref="BlockListContentData"/> instance based on the specified grid <paramref name="control"/>.
/// </summary>
/// <typeparam name="T">The type of the content data.</typeparam>
/// <param name="control">The grid control that is being converted.</param>
/// <returns>An instance of <see cref="BlockListContentData"/> representing the created content data.</returns>
public virtual BlockListContentData CreateContentData<T>(GridControl control) where T : PublishedElementModel {
return new BlockListContentData(control, GetModelType<T>());
}
/// <summary>
/// Creates and returns a instance of <see cref="BlockListSettingsData"/> representing the default settings data.
/// </summary>
/// <param name="control">The grid control that is being converted.</param>
/// <returns>An instance of <see cref="BlockListSettingsData"/> representing the created settings data.</returns>
public virtual BlockListSettingsData CreateDefaultSettings(GridControl control) {
return CreateSettingsData<BlockSettingsGeneral>(control).Add("blockVisibleStatus", 1);
}
/// <summary>
/// Creates and returns a instance of <see cref="BlockListSettingsData"/> based on the specified <paramref name="key"/>.
/// </summary>
/// <typeparam name="T">The type of the settings data.</typeparam>
/// <param name="key">A unique GUID key that will be used to form the UDI of the settings data.</param>
/// <returns>An instance of <see cref="BlockListSettingsData"/> representing the created settings data.</returns>
public virtual BlockListSettingsData CreateSettingsData<T>(Guid key) where T : PublishedElementModel {
return new BlockListSettingsData(key, GetModelType<T>());
}
/// <summary>
/// Creates and returns a new <see cref="BlockListSettingsData"/> instance based on the specified grid <paramref name="control"/>.
/// </summary>
/// <typeparam name="T">The type of the settings data.</typeparam>
/// <param name="control">The grid control that is being converted.</param>
/// <returns>An instance of <see cref="BlockListSettingsData"/> representing the created settings data.</returns>
protected BlockListSettingsData CreateSettingsData<T>(GridControl control) where T : PublishedElementModel {
return new BlockListSettingsData(control, GetModelType<T>());
}
private IPublishedContentType GetModelType<T>() where T : PublishedElementModel {
var t = typeof(T);
var field = t.GetField("ModelTypeAlias");
string alias = (string) field!.GetValue(null)!;
return _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content!.GetContentType(alias)!;
}
#endregion
}
}
namespace code.Features.BlockList.Models {
/// <summary>
/// Class representing a block list item.
/// </summary>
public class BlockListItem {
#region Properties
/// <summary>
/// Gets or sets the content data of the block item.
/// </summary>
public BlockListContentData Content { get; set; }
/// <summary>
/// Gets or sets the settings data of the block item.
/// </summary>
public BlockListSettingsData? Settings { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initialize a new instance based on the specified <paramref name="content"/> data.
/// </summary>
/// <param name="content">The content data of the block item.</param>
public BlockListItem(BlockListContentData content) {
Content = content;
}
/// <summary>
/// Initialize a new instance based on the specified <paramref name="content"/> and <paramref name="settings"/> data.
/// </summary>
/// <param name="content">The content data of the block item.</param>
/// <param name="settings">The settings data of the block item.</param>
public BlockListItem(BlockListContentData content, BlockListSettingsData? settings) {
Content = content;
Settings = settings;
}
#endregion
}
}
using System.Collections.Generic;
using code.Features.BlockList.Json.Converters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace code.Features.BlockList.Models {
/// <summary>
/// Class representing a custom block list model.
/// </summary>
[JsonConverter(typeof(BlockListModelJsonConverter))]
public class BlockListModel {
#region Properties
/// <summary>
/// Gets a list of the items making up the block list.
/// </summary>
public List<BlockListItem> Items { get; } = new();
#endregion
#region Member methods
/// <summary>
/// Appends the specified <paramref name="item"/> to the block list.
/// </summary>
/// <param name="item">The item to be added.</param>
/// <returns>Returns the instance itself - useful for method chaining.</returns>
public BlockListModel AddItem(BlockListItem item) {
Items.Add(item);
return this;
}
/// <summary>
/// Appends a new item based on the specified <paramref name="content"/> data.
/// </summary>
/// <param name="content">The content data of the block list item.</param>
/// <returns>Returns the instance itself - useful for method chaining.</returns>
public BlockListModel AddItem(BlockListContentData content) {
Items.Add(new BlockListItem(content));
return this;
}
/// <summary>
/// Appends a new item based on the specified <paramref name="content"/> and <paramref name="settings"/> data.
/// </summary>
/// <param name="content">The content data of the block list item.</param>
/// <param name="settings">The settings data of the block list item.</param>
/// <returns>Returns the instance itself - useful for method chaining.</returns>
public BlockListModel AddItem(BlockListContentData content, BlockListSettingsData? settings) {
Items.Add(new BlockListItem(content, settings));
return this;
}
public string ToJsonString() {
return JObject.FromObject(this).ToString(Formatting.None);
}
#endregion
}
}
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Collections.Generic;
using System;
using code.Features.BlockList.Models;
namespace code.Features.BlockList.Json.Converters {
/// <summary>
/// Custom JSON converter for serializing instances of <see cref="BlockListModel"/>. This converter does not support deserialization.
/// </summary>
public class BlockListModelJsonConverter : JsonConverter {
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
// This converter only supports "BlockListModel", so if we encounter any other type, we write null instead
if (value is not BlockListModel model) {
writer.WriteNull();
return;
}
JArray layout = new();
JArray contentData = new();
JArray settingsData = new();
foreach (BlockListItem item in model.Items) {
JObject layoutItem = new() {
{"contentUdi", item.Content.Udi.ToString()}
};
if (item.Settings is not null) layoutItem.Add("settingsUdi", item.Settings.Udi.ToString());
layout.Add(layoutItem);
contentData.Add(ConvertData(item.Content));
if (item.Settings is not null) settingsData.Add(ConvertData(item.Settings));
}
JObject blockListValue = new() {
{ "layout", new JObject { { "Umbraco.BlockList", layout } } },
{ "contentData", contentData },
{ "settingsData", settingsData }
};
blockListValue.WriteTo(writer);
}
private static JObject ConvertData(BlockListContentData data) {
JObject json = new() {
{"contentTypeKey", data.ContentType.Key},
{"udi", data.Udi.ToString()}
};
foreach (KeyValuePair<string, object?> property in data.Properties) {
json.Add(property.Key, property.Value is null ? null : JToken.FromObject(property.Value));
}
return json;
}
private static JObject ConvertData(BlockListSettingsData data) {
JObject json = new() {
{"contentTypeKey", data.ContentType.Key},
{"udi", data.Udi.ToString()}
};
foreach (KeyValuePair<string, object?> property in data.Properties) {
json.Add(property.Key, property.Value is null ? null : JToken.FromObject(property.Value));
}
return json;
}
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
throw new NotImplementedException();
}
public override bool CanConvert(Type objectType) {
return false;
}
}
}
using System;
using System.Collections.Generic;
using Skybrud.Essentials.Security;
using Skybrud.Umbraco.GridData.Models;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.PublishedContent;
namespace code.Features.BlockList.Models {
/// <summary>
/// Class representing the settings data object of a block list item.
/// </summary>
public class BlockListSettingsData {
#region Properties
/// <summary>
/// Gets the content type of the settings data.
/// </summary>
public IPublishedContentType ContentType { get; }
/// <summary>
/// Gets the UDI of the settings data.
/// </summary>
public GuidUdi Udi { get; }
/// <summary>
/// Gets a dictionary with the properties of the settings data.
/// </summary>
public Dictionary<string, object?> Properties { get; set; } = new();
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance based on the specified <paramref name="key"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="key">The unique key that will be used to form the UDI of the settings data.</param>
/// <param name="contentType">The content type of the settings data.</param>
public BlockListSettingsData(Guid key, IPublishedContentType contentType) {
Udi = new GuidUdi("element", key);
ContentType = contentType;
}
/// <summary>
/// Initializes a new instance based on the specified grid <paramref name="control"/> and <paramref name="contentType"/>.
/// </summary>
/// <param name="control">The grid control to be converted.</param>
/// <param name="contentType">The content type of the settings data.</param>
public BlockListSettingsData(GridControl control, IPublishedContentType contentType) {
// We need to ensure that each content item has a unique key. Generally we should be able to use the same
// key as the grid row, but even though this shouldn't be allowed in the legacy site, so rows have more
// than one control, in which case we creatively need to generate a unique key for those additional
// controls. Notice that is's important that the calculated key is the same if we repeat it again and again
int index = control.Area.Controls.IndexOf(control);
Guid key = SecurityUtils.GetMd5Guid(control.Row.Id + "#ffs" + (index == 0 ? "" : "#" + index));
// Create an UDI based on the element type and the GUID key
Udi = new GuidUdi("element", key);
// Set the content type
ContentType = contentType;
}
#endregion
#region Member methods
/// <summary>
/// Adds a property with the specified <paramref name="alias"/> and <paramref name="value"/>.
/// </summary>
/// <param name="alias">The alias of the property.</param>
/// <param name="value">The value of the property.</param>
/// <returns>Returns the instance itself - useful for method chaining.</returns>
public BlockListSettingsData Add(string alias, object? value) {
Properties.Add(alias, value);
return this;
}
#endregion
}
}
using Umbraco.Cms.Core.Models.PublishedContent;
namespace code.Features.BlockList.Models.Generated {
public class BlockQuote : PublishedElementModel {
// This class is really just a placeholder in this Gist. Ideally it should be a model
// generated by (Limbo) Models Builder, as the BlockListFactory will look for a
// "ModelTypeAlias" field in order to determine the element type
public BlockQuote(IPublishedElement content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback) { }
}
}
using Umbraco.Cms.Core.Models.PublishedContent;
namespace code.Features.BlockList.Models.Generated {
public class BlockRichTextEditor : PublishedElementModel {
// This class is really just a placeholder in this Gist. Ideally it should be a model
// generated by (Limbo) Models Builder, as the BlockListFactory will look for a
// "ModelTypeAlias" field in order to determine the element type
public BlockRichTextEditor(IPublishedElement content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback) { }
}
}
using Umbraco.Cms.Core.Models.PublishedContent;
namespace code.Features.BlockList.Models.Generated {
public class BlockSettingsGeneral : PublishedElementModel {
// This class is really just a placeholder in this Gist. Ideally it should be a model
// generated by (Limbo) Models Builder, as the BlockListFactory will look for a
// "ModelTypeAlias" field in order to determine the element type
public BlockSettingsGeneral(IPublishedElement content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback) { }
}
}
using Umbraco.Cms.Core.Models.PublishedContent;
namespace code.Features.BlockList.Models.Generated {
public class BlockYouTube : PublishedElementModel {
// This class is really just a placeholder in this Gist. Ideally it should be a model
// generated by (Limbo) Models Builder, as the BlockListFactory will look for a
// "ModelTypeAlias" field in order to determine the element type
public BlockYouTube(IPublishedElement content, IPublishedValueFallback publishedValueFallback) : base(content, publishedValueFallback) { }
}
}
using System;
using code.Features.BlockList;
using code.Features.BlockList.Models;
using HtmlAgilityPack;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Skybrud.Essentials.Json.Newtonsoft.Extensions;
using Skybrud.Umbraco.GridData.Models;
using Newtonsoft.Json.Linq;
using Skybrud.Essentials.Json.Newtonsoft;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Skybrud.Essentials.Security;
using Skybrud.Essentials.Strings;
using Skybrud.Social.Google;
using Skybrud.Social.Google.YouTube;
using Skybrud.Social.Google.YouTube.Models.Videos;
using Skybrud.Social.Google.YouTube.Options.Videos;
using code.Features.BlockList.Models.Generated;
namespace code.Features.Migrations {
public class MigrationsService {
private readonly IConfiguration _configuration;
private readonly IMediaService _mediaService;
private readonly BlockListFactory _blockListFactory;
#region Constructors
public MigrationsService(IConfiguration configuration, IMediaService mediaService, BlockListFactory blockListFactory) {
_configuration = configuration;
_mediaService = mediaService;
_blockListFactory = blockListFactory;
}
#endregion
#region Member methods
public virtual BlockListModel Convert(GridDataModel gridData, object? owner) {
BlockListModel blockList = new();
foreach (GridSection section in gridData.Sections) {
foreach (GridRow row in section.Rows) {
foreach (GridArea area in row.Areas) {
foreach (GridControl control in area.Controls) {
string editorAlias = control.Editor.Alias;
switch (editorAlias) {
case "rte":
ConvertRte(blockList, control);
break;
case "video":
ConvertVideo(blockList, control);
break;
case "quote":
ConvertQuote(blockList, control);
break;
default:
throw new Exception($"Unsupported grid element: {editorAlias}\r\n\r\n{control.JObject}\r\n\r\n");
}
}
}
}
}
return blockList;
}
private void ConvertRte(BlockListModel blockList, GridControl control) {
// Ignore the control if it doesn't gave a value
string? text = control.JObject.GetString("value");
if (string.IsNullOrWhiteSpace(text)) return;
if (text.Contains("umb://media/")) {
HtmlDocument document = new();
document.LoadHtml(text);
var links = document.DocumentNode.Descendants("a");
var images = document.DocumentNode.Descendants("img");
bool modified = false;
if (links is not null) {
foreach (HtmlNode? link in links) {
// Historically Umbraco has added "data-udi" attributes to links, which we can then use to grab
// the UDI of the reference media
string dataUdi = link.GetAttributeValue("data-udi", "");
if (!dataUdi.StartsWith("umb://media/")) continue;
// Get the GUID part of the UDI
Guid mediaKey = Guid.Parse(dataUdi[12..]);
// Get a reference to the media
IMedia? media = _mediaService.GetById(mediaKey);
// Get the URL of the media
string? mediaUrl = media?.GetValue<string>("umbracoFile");
// Can't really do anything if the media doesn't have a valid URL
if (string.IsNullOrWhiteSpace(mediaUrl)) continue;
// If the media URL looks like a JSON object, we parse it and grab the URL from the "src" property
if (JsonUtils.TryParseJsonObject(mediaUrl, out JObject? mediaUrlJson)) {
mediaUrl = mediaUrlJson.GetString("src");
}
// Update the "href" attribute
link.SetAttributeValue("href", mediaUrl);
modified = true;
}
}
if (images is not null) {
foreach (HtmlNode node in images) {
// Historically Umbraco has added "data-udi" attributes to links, which we can then use to grab
// the UDI of the reference media
string dataUdi = node.GetAttributeValue("data-udi", "");
if (!dataUdi.StartsWith("umb://media/")) continue;
// Get the GUID part of the UDI
Guid mediaKey = Guid.Parse(dataUdi[12..]);
// Get a reference to the media
IMedia? media = _mediaService.GetById(mediaKey);
// Get the URL of the media
string? mediaUrl = media?.GetValue<string>("umbracoFile");
// Can't really do anything if the media doesn't have a valid URL
if (string.IsNullOrWhiteSpace(mediaUrl)) continue;
// If the media URL looks like a JSON object, we parse it and grab the URL from the "src" property
if (JsonUtils.TryParseJsonObject(mediaUrl, out JObject? mediaUrlJson)) {
mediaUrl = mediaUrlJson.GetString("src");
}
// Update the "src" attribute
node.SetAttributeValue("src", mediaUrl);
modified = true;
}
}
if (modified) text = document.DocumentNode.OuterHtml;
}
var content = _blockListFactory
.CreateContentData<BlockRichTextEditor>(control)
.Add("title", "")
.Add("showTitleInToc", 0)
.Add("text", text);
var settings = _blockListFactory
.CreateDefaultSettings(control);
blockList.AddItem(content, settings);
}
private void ConvertQuote(BlockListModel blockList, GridControl control) {
// Get the values from the raw JSON
string? quote = control.JObject.GetStringByPath("value[0].quote.value");
string? byline = control.JObject.GetStringByPath("value[0].byline.value");
// Must have a value for the quote
if (string.IsNullOrWhiteSpace(quote)) return;
var content = _blockListFactory
.CreateContentData<BlockQuote>(control)
.Add("quote", quote)
.Add("origin", byline);
var settings = _blockListFactory
.CreateDefaultSettings(control);
blockList.AddItem(content, settings);
}
private void ConvertVideo(BlockListModel blockList, GridControl control) {
// Parse the raw JSOn from the grid control
JObject[] items = control.JObject.GetObjectArrayByPath("value.items");
if (items.Length == 0) return;
if (items.Length > 1) throw new Exception("Video element has multiple videos.");
string? credentials = _configuration.GetSection("Limbo:YouTube:Credentials:0:Key").Value;
string? apiKey = _configuration.GetSection("Limbo:YouTube:Credentials:0:ApiKey").Value;
if (string.IsNullOrWhiteSpace(credentials)) throw new Exception("YouTube credentials doesn't specify a key.");
if (string.IsNullOrWhiteSpace(apiKey)) throw new Exception("YouTube credentials doesn't specify an API key.");
GoogleHttpService google = GoogleHttpService.CreateFromApiKey(apiKey);
foreach (JObject item in items) {
string? url = item.GetString("url");
string? description = item.GetString("description");
if (string.IsNullOrWhiteSpace(url)) throw new Exception("Video item doesn't specify a video URL.");
if (!RegexUtils.IsMatch(url, "v=([a-zA-Z0-9_-]{11})$", out string v)) throw new Exception("YouTube URL is not valid.");
// Videoen findes ikke længere
if (v == "vY1InnKe84o") continue;
var response = google.YouTube().Videos.GetVideos(new YouTubeGetVideoListOptions(v) {
Part = YouTubeVideoParts.ContentDetails + YouTubeVideoParts.Snippet
});
if (response.Body.Items.Count == 0)
throw new Exception($"No YouTube video found with ID {v}.");
YouTubeVideo youTubeVideo = response.Body.Items[0];
string data = youTubeVideo.JObject!.ToString(Formatting.None);
JObject youTubeVideoData = new() {
{"source", url},
{"credentials", new JObject {{"key", credentials}}},
{"video", new JObject{{"_data", data}}}
};
var youTubeBlockContent = _blockListFactory.CreateContentData<BlockYouTube>(control)
.Add("video", youTubeVideoData.ToString(Formatting.None))
.Add("statistic", 1)
.Add("functional", 1)
.Add("marketing", 1);
var youTubeBlockList = new BlockListModel()
.AddItem(youTubeBlockContent);
// The key should be based on something we can reproduce again and again, so important this isn't a random GUID
var videoBlockContentKey = SecurityUtils.GetMd5Guid($"{control.Row.Id}#videoBlockContent");
var videoBlockContent = _blockListFactory.CreateContentData<BlockYouTube>(videoBlockContentKey)
.Add("title", youTubeVideo.Snippet!.Title)
.Add("showTitleInToc", 0)
.Add("text", description)
.Add("video", JObject.FromObject(youTubeBlockList).ToString(Formatting.None));
var settings = _blockListFactory.CreateDefaultSettings(control);
blockList.AddItem(videoBlockContent, settings);
}
}
#endregion
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment