diff --git a/src/Umbraco.Core/Media/EmbedProviders/X.cs b/src/Umbraco.Core/Media/EmbedProviders/X.cs index 5d4852e722..0375e53fcd 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/X.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/X.cs @@ -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\/.*" }; diff --git a/src/Umbraco.Core/Services/IOEmbedService.cs b/src/Umbraco.Core/Services/IOEmbedService.cs index 8dcc3979fa..6e9599bd0d 100644 --- a/src/Umbraco.Core/Services/IOEmbedService.cs +++ b/src/Umbraco.Core/Services/IOEmbedService.cs @@ -2,7 +2,22 @@ using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; +/// +/// Defines a service for asynchronously retrieving embeddable HTML markup for a specified resource using the oEmbed +/// protocol. +/// public interface IOEmbedService { + /// + /// Asynchronously retrieves the embeddable HTML markup for the specified resource. + /// + /// 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. + /// The URI of the resource to retrieve markup for. Must be a valid, absolute URI. + /// The optional maximum width, in pixels, for the embedded content. If null, the default width is used. + /// The optional maximum height, in pixels, for the embedded content. If null, the default height is used. + /// A token to monitor for cancellation requests. The operation is canceled if the token is triggered. + /// 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. Task> GetMarkupAsync(Uri url, int? width, int? height, CancellationToken cancellationToken); } diff --git a/src/Umbraco.Core/Services/OEmbedService.cs b/src/Umbraco.Core/Services/OEmbedService.cs index dfa164ec82..eba0e02991 100644 --- a/src/Umbraco.Core/Services/OEmbedService.cs +++ b/src/Umbraco.Core/Services/OEmbedService.cs @@ -6,22 +6,30 @@ using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Core.Services; +/// +/// Implements for retrieving embeddable HTML markup using the oEmbed protocol. +/// public class OEmbedService : IOEmbedService { private readonly EmbedProvidersCollection _embedProvidersCollection; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// public OEmbedService(EmbedProvidersCollection embedProvidersCollection, ILogger logger) { _embedProvidersCollection = embedProvidersCollection; _logger = logger; } + /// public async Task> 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) { @@ -39,8 +47,8 @@ public class OEmbedService : IOEmbedService } 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); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/OEmbedServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/OEmbedServiceTests.cs new file mode 100644 index 0000000000..e055466775 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/OEmbedServiceTests.cs @@ -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(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + base.CustomTestSetup(builder); + + // Clear all providers and add only the X provider + builder.EmbedProviders().Clear().Append(); + } + + /// + /// Verifies resolution to https://github.com/umbraco/Umbraco-CMS/issues/21052. + /// + /// + /// Tests marked as [Explicit] as we don't want a random external service call to X to fail during regular test runs. + /// + [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")); + }); + } +}