Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
2 changes: 1 addition & 1 deletion src/Umbraco.Core/Media/EmbedProviders/X.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public X(IJsonSerializer jsonSerializer)
{
}

public override string ApiEndpoint => "http://publish.twitter.com/oembed";
public override string ApiEndpoint => "http://publish.x.com/oembed";

public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" };

Expand Down
15 changes: 15 additions & 0 deletions src/Umbraco.Core/Services/IOEmbedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@

namespace Umbraco.Cms.Core.Services;

/// <summary>
/// Defines a service for asynchronously retrieving embeddable HTML markup for a specified resource using the oEmbed
/// protocol.
/// </summary>
public interface IOEmbedService
{
/// <summary>
/// Asynchronously retrieves the embeddable HTML markup for the specified resource.
/// </summary>
/// <remarks>The returned markup is suitable for embedding in web pages. The width and height parameters
/// may be ignored by some providers depending on their capabilities.</remarks>
/// <param name="url">The URI of the resource to retrieve markup for. Must be a valid, absolute URI.</param>
/// <param name="width">The optional maximum width, in pixels, for the embedded content. If null, the default width is used.</param>
/// <param name="height">The optional maximum height, in pixels, for the embedded content. If null, the default height is used.</param>
/// <param name="cancellationToken">A token to monitor for cancellation requests. The operation is canceled if the token is triggered.</param>
/// <returns>A task that represents the asynchronous operation. The result contains an Attempt with the HTML markup if
/// successful, or an oEmbed operation status indicating the reason for failure.</returns>
Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? width, int? height, CancellationToken cancellationToken);
}
12 changes: 10 additions & 2 deletions src/Umbraco.Core/Services/OEmbedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,30 @@

namespace Umbraco.Cms.Core.Services;

/// <summary>
/// Implements <see cref="IOEmbedService"/> for retrieving embeddable HTML markup using the oEmbed protocol.
/// </summary>
public class OEmbedService : IOEmbedService
{
private readonly EmbedProvidersCollection _embedProvidersCollection;
private readonly ILogger<OEmbedService> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="OEmbedService"/> class.
/// </summary>
public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger<OEmbedService> logger)
{
_embedProvidersCollection = embedProvidersCollection;
_logger = logger;
}

/// <inheritdoc/>
public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken)
{
// Find the first provider that supports the URL
IEmbedProvider? matchedProvider = _embedProvidersCollection
.FirstOrDefault(provider => provider.UrlSchemeRegex.Any(regex=>new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));
.FirstOrDefault(provider => provider.UrlSchemeRegex
.Any(regex => new Regex(regex, RegexOptions.IgnoreCase).IsMatch(url.OriginalString)));

if (matchedProvider is null)
{
Expand All @@ -39,7 +47,7 @@ public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url
}
catch (Exception e)
{
_logger.LogError(e, "Unexpected exception happened while trying to get oembed markup. Provider: {Provider}",matchedProvider.GetType().Name);
_logger.LogError(e, "Unexpected exception happened while trying to get oEmbed markup. Provider: {Provider}", matchedProvider.GetType().Name);
Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using NUnit.Framework;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Media.EmbedProviders;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Tests.Common.Testing;
using Umbraco.Cms.Tests.Integration.Testing;

namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;

[TestFixture]
[UmbracoTest(Database = UmbracoTestOptions.Database.None)]
internal sealed class OEmbedServiceTests : UmbracoIntegrationTest
{
private IOEmbedService OEmbedService => GetRequiredService<IOEmbedService>();

protected override void CustomTestSetup(IUmbracoBuilder builder)
{
base.CustomTestSetup(builder);

// Clear all providers and add only the X provider
builder.EmbedProviders().Clear().Append<X>();
}

/// <summary>
/// Verifies resolution to https://github.com/umbraco/Umbraco-CMS/issues/21052.
/// </summary>
/// <remarks>
/// Tests marked as [Explicit] as we don't want a random external service call to X to fail during regular test runs.
/// </remarks>
[Explicit]
[TestCase("https://x.com/THR/status/1995620384344080849?s=20")]
[TestCase("https://x.com/SquareEnix/status/1995780120888705216?s=20")]
[TestCase("https://x.com/sem_sep/status/1991750339427700739?s=20")]
[TestCase("https://x.com/sem_sep/status/1991749404114767914?s=20")]
public async Task GetMarkupAsync_WithXUrls_ReturnsSuccessAndMarkup(string url)
{
// Arrange
var uri = new Uri(url);

// Act
var result = await OEmbedService.GetMarkupAsync(uri, width: null, height: null, CancellationToken.None);

// Assert
Assert.Multiple(() =>
{
Assert.That(result.Success, Is.True);
Assert.That(result.Status, Is.EqualTo(OEmbedOperationStatus.Success));
Assert.That(result.Result, Is.Not.Null.And.Not.Empty);
Assert.That(result.Result, Does.Contain("blockquote"));
});
}
}
Loading