oEmbed Providers: Updated the X oEmbed provider to use the x.com domain (closes #21052) (#21053)

* Updated the X oEmbed provider to use the x.com domain.

* Fixed issues raised in code review.
This commit is contained in:
Andy Butland
2025-12-05 10:38:58 +01:00
committed by GitHub
parent b6580ed40c
commit 5683ae9e4b
4 changed files with 79 additions and 4 deletions

View File

@@ -12,7 +12,7 @@ public class X : OEmbedProviderBase
{ {
} }
public override string ApiEndpoint => "http://publish.twitter.com/oembed"; public override string ApiEndpoint => "https://publish.x.com/oembed";
public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" }; public override string[] UrlSchemeRegex => new[] { @"(https?:\/\/(www\.)?)(twitter|x)\.com\/.*\/status\/.*" };

View File

@@ -2,7 +2,22 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services; 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 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); Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? width, int? height, CancellationToken cancellationToken);
} }

View File

@@ -6,22 +6,30 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.Services;
/// <summary>
/// Implements <see cref="IOEmbedService"/> for retrieving embeddable HTML markup using the oEmbed protocol.
/// </summary>
public class OEmbedService : IOEmbedService public class OEmbedService : IOEmbedService
{ {
private readonly EmbedProvidersCollection _embedProvidersCollection; private readonly EmbedProvidersCollection _embedProvidersCollection;
private readonly ILogger<OEmbedService> _logger; private readonly ILogger<OEmbedService> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="OEmbedService"/> class.
/// </summary>
public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger<OEmbedService> logger) public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger<OEmbedService> logger)
{ {
_embedProvidersCollection = embedProvidersCollection; _embedProvidersCollection = embedProvidersCollection;
_logger = logger; _logger = logger;
} }
/// <inheritdoc/>
public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken) public async Task<Attempt<string, OEmbedOperationStatus>> GetMarkupAsync(Uri url, int? maxWidth, int? maxHeight, CancellationToken cancellationToken)
{ {
// Find the first provider that supports the URL // Find the first provider that supports the URL
IEmbedProvider? matchedProvider = _embedProvidersCollection 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) if (matchedProvider is null)
{ {
@@ -39,8 +47,8 @@ public class OEmbedService : IOEmbedService
} }
catch (Exception e) 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); return Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
} }
return Attempt.FailWithStatus(OEmbedOperationStatus.ProviderReturnedInvalidResult, string.Empty); return Attempt.FailWithStatus(OEmbedOperationStatus.ProviderReturnedInvalidResult, string.Empty);

View File

@@ -0,0 +1,52 @@
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")]
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"));
});
}
}