diff --git a/src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs b/src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs index 45fcaaba4bce..f5d56fbf2e2a 100644 --- a/src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs +++ b/src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs @@ -10,5 +10,7 @@ public ApiContentRoute(string path, ApiContentStartItem startItem) public string Path { get; } + public string? QueryString { get; set; } + public IApiContentStartItem StartItem { get; } } diff --git a/src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs b/src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs index 1cc2b36b9de9..cfc0b2984a29 100644 --- a/src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs +++ b/src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs @@ -4,5 +4,10 @@ public interface IApiContentRoute { string Path { get; } + public string? QueryString + { + get => null; set { } + } + IApiContentStartItem StartItem { get; } } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index 42c882986871..263ac9a8b56a 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -56,7 +56,7 @@ private void ReplaceLocalLinks(HtmlDocument doc, IPublishedSnapshot publishedSna link.GetAttributeValue("href", string.Empty), route => { - link.SetAttributeValue("href", route.Path); + link.SetAttributeValue("href", $"{route.Path}{route.QueryString}"); link.SetAttributeValue("data-start-item-path", route.StartItem.Path); link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D")); }, diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index 7723fc835c7f..34e4384c9e87 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DeliveryApi; @@ -41,6 +42,7 @@ protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string hr : null; if (route != null) { + route.QueryString = match.Groups["query"].Value.NullOrWhiteSpaceAsNull(); handled = true; handleContentRoute(route); } @@ -79,6 +81,6 @@ protected void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string u handleMediaUrl(_apiMediaUrlProvider.GetUrl(media)); } - [GeneratedRegex("{localLink:(?umb:.+)}")] + [GeneratedRegex("{localLink:(?umb:.+)}(?[^\"]*)")] private static partial Regex LocalLinkRegex(); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs index d3328f2c1447..5b16458f19e3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs @@ -2269,6 +2269,10 @@ public async Task Validate_OpenApi_Contract() "type": "string", "readOnly": true }, + "queryString": { + "type": "string", + "nullable": true + }, "startItem": { "$ref": "#/components/schemas/IApiContentStartItemModel" } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index f9f3722a0fea..d78fce702de1 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Infrastructure.DeliveryApi; +using Umbraco.Extensions; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -127,12 +128,16 @@ public void ParseElement_DataAttributesDoNotOverwriteExistingAttributes() Assert.AreEqual("the original something", span.Attributes.First().Value); } - [Test] - public void ParseElement_CanParseContentLink() + + [TestCase(null)] + [TestCase("")] + [TestCase("#some-anchor")] + [TestCase("?something=true")] + public void ParseElement_CanParseContentLink(string? postfix) { var parser = CreateRichTextElementParser(); - var element = parser.Parse($"

") as RichTextRootElement; + var element = parser.Parse($"

") as RichTextRootElement; Assert.IsNotNull(element); var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; Assert.IsNotNull(link); @@ -142,6 +147,7 @@ public void ParseElement_CanParseContentLink() var route = link.Attributes.First().Value as IApiContentRoute; Assert.IsNotNull(route); Assert.AreEqual("/some-content-path", route.Path); + Assert.AreEqual(postfix.NullOrWhiteSpaceAsNull(), route.QueryString); Assert.AreEqual(_contentRootKey, route.StartItem.Id); Assert.AreEqual("the-root-path", route.StartItem.Path); } @@ -176,6 +182,22 @@ public void ParseElement_CanHandleNonLocalLink() Assert.AreEqual("https://some.where/else/", link.Attributes.First().Value); } + [TestCase("#some-anchor")] + [TestCase("?something=true")] + public void ParseElement_CanHandleNonLocalLink_WithPostfix(string postfix) + { + var parser = CreateRichTextElementParser(); + + var element = parser.Parse($"

") as RichTextRootElement; + Assert.IsNotNull(element); + var link = element.Elements.OfType().Single().Elements.Single() as RichTextGenericElement; + Assert.IsNotNull(link); + Assert.AreEqual("a", link.Tag); + Assert.AreEqual(1, link.Attributes.Count); + Assert.AreEqual("href", link.Attributes.First().Key); + Assert.AreEqual($"https://some.where/else/{postfix}", link.Attributes.First().Value); + } + [Test] public void ParseElement_LinkTextIsWrappedInTextElement() { @@ -465,6 +487,18 @@ public void ParseMarkup_CanParseContentLink() Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\"")); } + [TestCase("#some-anchor")] + [TestCase("?something=true")] + public void ParseMarkup_CanParseContentLink_WithPostfix(string postfix) + { + var parser = CreateRichTextMarkupParser(); + + var result = parser.Parse($"

"); + Assert.IsTrue(result.Contains($"href=\"/some-content-path{postfix}\"")); + Assert.IsTrue(result.Contains("data-start-item-path=\"the-root-path\"")); + Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\"")); + } + [Test] public void ParseMarkup_CanParseMediaLink() { @@ -485,6 +519,8 @@ public void ParseMarkup_InvalidLocalLinkYieldsEmptyLink(string href) } [TestCase("

")] + [TestCase("

")] + [TestCase("

")] [TestCase("

")] public void ParseMarkup_CanHandleNonLocalReferences(string html) {