Skip to content

Commit a282002

Browse files
authored
Support querystring and anchor for local links in Delivery API output (#20142)
* Support querystring and anchor for local links in Delivery API output * Add default implementation for backwards compat * Add default implementation for backwards compat (also on the interface) * Fix default implementation
1 parent 45f7b7a commit a282002

6 files changed

Lines changed: 54 additions & 5 deletions

File tree

src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ public ApiContentRoute(string path, ApiContentStartItem startItem)
1010

1111
public string Path { get; }
1212

13+
public string? QueryString { get; set; }
14+
1315
public IApiContentStartItem StartItem { get; }
1416
}

src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,10 @@ public interface IApiContentRoute
44
{
55
string Path { get; }
66

7+
public string? QueryString
8+
{
9+
get => null; set { }
10+
}
11+
712
IApiContentStartItem StartItem { get; }
813
}

src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private void ReplaceLocalLinks(HtmlDocument doc, IPublishedSnapshot publishedSna
5656
link.GetAttributeValue("href", string.Empty),
5757
route =>
5858
{
59-
link.SetAttributeValue("href", route.Path);
59+
link.SetAttributeValue("href", $"{route.Path}{route.QueryString}");
6060
link.SetAttributeValue("data-start-item-path", route.StartItem.Path);
6161
link.SetAttributeValue("data-start-item-id", route.StartItem.Id.ToString("D"));
6262
},

src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Umbraco.Cms.Core.Models.DeliveryApi;
55
using Umbraco.Cms.Core.Models.PublishedContent;
66
using Umbraco.Cms.Core.PublishedCache;
7+
using Umbraco.Extensions;
78

89
namespace Umbraco.Cms.Infrastructure.DeliveryApi;
910

@@ -41,6 +42,7 @@ protected void ReplaceLocalLinks(IPublishedSnapshot publishedSnapshot, string hr
4142
: null;
4243
if (route != null)
4344
{
45+
route.QueryString = match.Groups["query"].Value.NullOrWhiteSpaceAsNull();
4446
handled = true;
4547
handleContentRoute(route);
4648
}
@@ -79,6 +81,6 @@ protected void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string u
7981
handleMediaUrl(_apiMediaUrlProvider.GetUrl(media));
8082
}
8183

82-
[GeneratedRegex("{localLink:(?<udi>umb:.+)}")]
84+
[GeneratedRegex("{localLink:(?<udi>umb:.+)}(?<query>[^\"]*)")]
8385
private static partial Regex LocalLinkRegex();
8486
}

tests/Umbraco.Tests.Integration/Umbraco.Core/DeliveryApi/OpenApiContractTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,10 @@ public async Task Validate_OpenApi_Contract()
22692269
"type": "string",
22702270
"readOnly": true
22712271
},
2272+
"queryString": {
2273+
"type": "string",
2274+
"nullable": true
2275+
},
22722276
"startItem": {
22732277
"$ref": "#/components/schemas/IApiContentStartItemModel"
22742278
}

tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
1111
using Umbraco.Cms.Core.PublishedCache;
1212
using Umbraco.Cms.Infrastructure.DeliveryApi;
13+
using Umbraco.Extensions;
1314

1415
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi;
1516

@@ -127,12 +128,16 @@ public void ParseElement_DataAttributesDoNotOverwriteExistingAttributes()
127128
Assert.AreEqual("the original something", span.Attributes.First().Value);
128129
}
129130

130-
[Test]
131-
public void ParseElement_CanParseContentLink()
131+
132+
[TestCase(null)]
133+
[TestCase("")]
134+
[TestCase("#some-anchor")]
135+
[TestCase("?something=true")]
136+
public void ParseElement_CanParseContentLink(string? postfix)
132137
{
133138
var parser = CreateRichTextElementParser();
134139

135-
var element = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}\"></a></p>") as RichTextRootElement;
140+
var element = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}{postfix}\"></a></p>") as RichTextRootElement;
136141
Assert.IsNotNull(element);
137142
var link = element.Elements.OfType<RichTextGenericElement>().Single().Elements.Single() as RichTextGenericElement;
138143
Assert.IsNotNull(link);
@@ -142,6 +147,7 @@ public void ParseElement_CanParseContentLink()
142147
var route = link.Attributes.First().Value as IApiContentRoute;
143148
Assert.IsNotNull(route);
144149
Assert.AreEqual("/some-content-path", route.Path);
150+
Assert.AreEqual(postfix.NullOrWhiteSpaceAsNull(), route.QueryString);
145151
Assert.AreEqual(_contentRootKey, route.StartItem.Id);
146152
Assert.AreEqual("the-root-path", route.StartItem.Path);
147153
}
@@ -176,6 +182,22 @@ public void ParseElement_CanHandleNonLocalLink()
176182
Assert.AreEqual("https://some.where/else/", link.Attributes.First().Value);
177183
}
178184

185+
[TestCase("#some-anchor")]
186+
[TestCase("?something=true")]
187+
public void ParseElement_CanHandleNonLocalLink_WithPostfix(string postfix)
188+
{
189+
var parser = CreateRichTextElementParser();
190+
191+
var element = parser.Parse($"<p><a href=\"https://some.where/else/{postfix}\"></a></p>") as RichTextRootElement;
192+
Assert.IsNotNull(element);
193+
var link = element.Elements.OfType<RichTextGenericElement>().Single().Elements.Single() as RichTextGenericElement;
194+
Assert.IsNotNull(link);
195+
Assert.AreEqual("a", link.Tag);
196+
Assert.AreEqual(1, link.Attributes.Count);
197+
Assert.AreEqual("href", link.Attributes.First().Key);
198+
Assert.AreEqual($"https://some.where/else/{postfix}", link.Attributes.First().Value);
199+
}
200+
179201
[Test]
180202
public void ParseElement_LinkTextIsWrappedInTextElement()
181203
{
@@ -465,6 +487,18 @@ public void ParseMarkup_CanParseContentLink()
465487
Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\""));
466488
}
467489

490+
[TestCase("#some-anchor")]
491+
[TestCase("?something=true")]
492+
public void ParseMarkup_CanParseContentLink_WithPostfix(string postfix)
493+
{
494+
var parser = CreateRichTextMarkupParser();
495+
496+
var result = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}{postfix}\"></a></p>");
497+
Assert.IsTrue(result.Contains($"href=\"/some-content-path{postfix}\""));
498+
Assert.IsTrue(result.Contains("data-start-item-path=\"the-root-path\""));
499+
Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\""));
500+
}
501+
468502
[Test]
469503
public void ParseMarkup_CanParseMediaLink()
470504
{
@@ -485,6 +519,8 @@ public void ParseMarkup_InvalidLocalLinkYieldsEmptyLink(string href)
485519
}
486520

487521
[TestCase("<p><a href=\"https://some.where/else/\"></a></p>")]
522+
[TestCase("<p><a href=\"https://some.where/else/#some-anchor\"></a></p>")]
523+
[TestCase("<p><a href=\"https://some.where/else/?something=true\"></a></p>")]
488524
[TestCase("<p><img src=\"https://some.where/something.png?rmode=max&amp;width=500\"></p>")]
489525
public void ParseMarkup_CanHandleNonLocalReferences(string html)
490526
{

0 commit comments

Comments
 (0)