diff --git a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs index a8a5781288f9..f79d29023b32 100644 --- a/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs +++ b/src/Umbraco.Core/Templates/HtmlImageSourceParser.cs @@ -136,7 +136,11 @@ public string EnsureImageSources(string text) // Re-sign the URL so a rotated HMAC secret key doesn't break previously-authored images. // No-op when HMAC isn't configured. - var refreshedSrc = _imageUrlTokenGenerator.RefreshSignature($"{mediaUrl}{src.Groups[2].Value}"); + var queryString = src.Groups[2].Value; + var composedUrl = queryString.IsNullOrWhiteSpace() + ? mediaUrl + : mediaUrl.AppendQueryStringToUrl(queryString); + var refreshedSrc = _imageUrlTokenGenerator.RefreshSignature(composedUrl); return match.Value.Replace(src.Value, $"src=\"{refreshedSrc}\""); }); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index 75de44914c11..85a5f8d24948 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -126,7 +126,10 @@ private void ReplaceLocalImages(HtmlDocument doc, IPublishedMediaCache mediaCach // Re-sign the URL so a rotated HMAC secret key doesn't break previously-authored images. // No-op when HMAC isn't configured. - var refreshedSrc = _imageUrlTokenGenerator.RefreshSignature($"{mediaUrl}{currentImageQueryString}"); + var refreshedSrc = _imageUrlTokenGenerator.RefreshSignature( + currentImageQueryString.IsNullOrWhiteSpace() + ? mediaUrl + : mediaUrl.AppendQueryStringToUrl(currentImageQueryString)); image.SetAttributeValue("src", refreshedSrc); image.Attributes.Remove("data-udi"); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs index 2fc1e1d1f777..033753a1e19e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs @@ -264,4 +264,46 @@ public void EnsureImageSources_Noop_Signer_Leaves_Src_Unchanged() @"", result); } + + [Test] + public void Ensure_ImageSources_Processing_With_Cache_Buster() + { + var sourceHtml = """
"""; + var fakeMediaUrl = "/media/1001/image.jpg?rand=1234"; + + var parser = new HtmlImageSourceParser(guid => fakeMediaUrl, NoopSigner()); + var actual = parser.EnsureImageSources(sourceHtml); + + Assert.AreEqual( + """
""", + actual); + } + + [Test] + public void Ensure_ImageSources_With_Cache_Buster_And_No_Query_String() + { + var sourceHtml = """
"""; + var fakeMediaUrl = "/media/1001/image.jpg?rand=1234"; + + var parser = new HtmlImageSourceParser(guid => fakeMediaUrl, NoopSigner()); + var actual = parser.EnsureImageSources(sourceHtml); + + Assert.AreEqual( + """
""", + actual); + } + + [Test] + public void Ensure_ImageSources_With_No_Query_String_Does_Not_Append_Trailing_Query() + { + var sourceHtml = """
"""; + var fakeMediaUrl = "/media/1001/image.jpg"; + + var parser = new HtmlImageSourceParser(guid => fakeMediaUrl, NoopSigner()); + var actual = parser.EnsureImageSources(sourceHtml); + + Assert.AreEqual( + """
""", + actual); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs index 4ae164428dfd..5c69132a8243 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs @@ -180,6 +180,31 @@ public void Can_Parse_Inline_LocalImages() Assert.AreEqual(expectedOutput, parsedHtml); } + [Test] + public void Can_Parse_Inline_LocalImages_With_Cache_Buster() + { + var key1 = Guid.Parse("395bdc0e8f4d4ad4af7f3a3f6265651e"); + var data1 = new MockData() + .WithKey(key1) + .WithMediaUrl("https://localhost:44331/media/bdofwokn/77gtp8fbrxmgkefatp10aw.webp?rand=1234"); + + var mockData = new Dictionary + { + { key1, data1 }, + }; + var parser = BuildDefaultSut(mockData); + + var markup = + @"

An image

\n

"; + + var expectedOutput = + @"

An image

\n

"; + + var parsedHtml = parser.Parse(markup); + + Assert.AreEqual(expectedOutput, parsedHtml); + } + [Test] public void LocalImages_Are_ReSigned_Via_TokenGenerator() {