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"));
+ });
+ }
+}