Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 => "https://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);
}
14 changes: 11 additions & 3 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,8 +47,8 @@ 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);
Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
_logger.LogError(e, "Unexpected exception happened while trying to get oEmbed markup. Provider: {Provider}", matchedProvider.GetType().Name);
return Attempt.FailWithStatus(OEmbedOperationStatus.UnexpectedException, string.Empty, e);
}

return Attempt.FailWithStatus(OEmbedOperationStatus.ProviderReturnedInvalidResult, string.Empty);
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
});
}
}
Loading