Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Umbraco.Core/Models/DeliveryApi/ApiContentRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ public ApiContentRoute(string path, ApiContentStartItem startItem)

public string Path { get; }

public string? QueryString { get; set; }

public IApiContentStartItem StartItem { get; }
}
5 changes: 5 additions & 0 deletions src/Umbraco.Core/Models/DeliveryApi/IApiContentRoute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@ public interface IApiContentRoute
{
string Path { get; }

public string? QueryString
{
get => null; set { }
}

IApiContentStartItem StartItem { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private void ReplaceLocalLinks(HtmlDocument doc, IPublishedContentCache contentC
link.GetAttributeValue("type", "unknown"),
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"));
link.Attributes["type"]?.Remove();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -58,6 +59,7 @@ private ReplaceStatus ReplaceLocalLink(IPublishedContentCache contentCache, IPub
: null;
if (route != null)
{
route.QueryString = match.Groups["query"].Value.NullOrWhiteSpaceAsNull();
handleContentRoute(route);
return ReplaceStatus.Success;
}
Expand Down Expand Up @@ -105,6 +107,7 @@ private ReplaceStatus ReplaceLegacyLocalLink(IPublishedContentCache contentCache
: null;
if (route != null)
{
route.QueryString = match.Groups["query"].Value.NullOrWhiteSpaceAsNull();
handleContentRoute(route);
return ReplaceStatus.Success;
}
Expand Down Expand Up @@ -140,10 +143,10 @@ protected void ReplaceLocalImages(IPublishedMediaCache mediaCache, string udi, A
handleMediaUrl(_apiMediaUrlProvider.GetUrl(media));
}

[GeneratedRegex("{localLink:(?<udi>umb:.+)}")]
[GeneratedRegex("{localLink:(?<udi>umb:.+)}(?<query>[^\"]*)")]
private static partial Regex LegacyLocalLinkRegex();

[GeneratedRegex("{localLink:(?<guid>.+)}")]
[GeneratedRegex("{localLink:(?<guid>.+)}(?<query>[^\"]*)")]
private static partial Regex LocalLinkRegex();

private enum ReplaceStatus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1320,6 +1320,10 @@ public async Task Validate_OpenApi_Contract()
"path": {
"type": "string"
},
"queryString": {
"type": "string",
"nullable": true
},
"startItem": {
"oneOf": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -127,12 +128,15 @@ 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($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}\"></a></p>", RichTextBlockModel.Empty) as RichTextRootElement;
var element = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}{postfix}\"></a></p>", RichTextBlockModel.Empty) as RichTextRootElement;
Assert.IsNotNull(element);
var link = element.Elements.OfType<RichTextGenericElement>().Single().Elements.Single() as RichTextGenericElement;
Assert.IsNotNull(link);
Expand All @@ -142,6 +146,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);
}
Expand Down Expand Up @@ -176,6 +181,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($"<p><a href=\"https://some.where/else/{postfix}\"></a></p>", RichTextBlockModel.Empty) as RichTextRootElement;
Assert.IsNotNull(element);
var link = element.Elements.OfType<RichTextGenericElement>().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()
{
Expand Down Expand Up @@ -459,12 +480,51 @@ public void ParseMarkup_CanParseContentLink()
{
var parser = CreateRichTextMarkupParser();

var result = parser.Parse($"<p><a href=\"/{{localLink:{_contentKey:N}}}\" type=\"document\"></a></p>");
Assert.IsTrue(result.Contains("href=\"/some-content-path\""));
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_CanParseLegacyContentLink()
{
var parser = CreateRichTextMarkupParser();

var result = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}\"></a></p>");
Assert.IsTrue(result.Contains("href=\"/some-content-path\""));
Assert.IsTrue(result.Contains("data-start-item-path=\"the-root-path\""));
Assert.IsTrue(result.Contains($"data-start-item-id=\"{_contentRootKey:D}\""));
}

[TestCase("#some-anchor")]
[TestCase("?something=true")]
[TestCase("#!some-hashbang")]
[TestCase("?something=true#some-anchor")]
public void ParseMarkup_CanParseContentLink_WithPostfix(string postfix)
{
var parser = CreateRichTextMarkupParser();

var result = parser.Parse($"<p><a href=\"/{{localLink:{_contentKey:N}}}{postfix}\" type=\"document\"></a></p>");
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}\""));
}

[TestCase("#some-anchor")]
[TestCase("?something=true")]
[TestCase("#!some-hashbang")]
[TestCase("?something=true#some-anchor")]
public void ParseMarkup_CanParseLegacyContentLink_WithPostfix(string postfix)
{
var parser = CreateRichTextMarkupParser();

var result = parser.Parse($"<p><a href=\"/{{localLink:umb://document/{_contentKey:N}}}{postfix}\"></a></p>");
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()
{
Expand All @@ -485,6 +545,8 @@ public void ParseMarkup_InvalidLocalLinkYieldsEmptyLink(string href)
}

[TestCase("<p><a href=\"https://some.where/else/\"></a></p>")]
[TestCase("<p><a href=\"https://some.where/else/#some-anchor\"></a></p>")]
[TestCase("<p><a href=\"https://some.where/else/?something=true\"></a></p>")]
[TestCase("<p><img src=\"https://some.where/something.png?rmode=max&amp;width=500\"></p>")]
public void ParseMarkup_CanHandleNonLocalReferences(string html)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,45 @@
Assert.AreEqual(expectedOutput, parsedHtml);
}

[TestCase("#some-anchor")]
[TestCase("?something=true")]
[TestCase("#!some-hashbang")]
[TestCase("?something=true#some-anchor")]
public void Can_Parse_LocalLinks_With_Postfix(string postfix)
{
var key1 = Guid.Parse("eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f");
var data1 = new MockData()
.WithKey(key1)
.WithRoutePath($"/self/{postfix}")
.WithRouteStartPath("self");

var key2 = Guid.Parse("cc143afe-4cbf-46e5-b399-c9f451384373");
var data2 = new MockData()
.WithKey(key2)
.WithRoutePath($"/other/{postfix}")
.WithRouteStartPath("other");

var mockData = new Dictionary<Guid, MockData>
{
{ key1, data1 },
{ key2, data2 },
};

var parser = BuildDefaultSut(mockData);

var html =
$@"<p>Rich text outside of the blocks with a link to <a type=""document"" href=""/{{localLink:eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f}}{postfix}"" title=""itself"">itself</a><br><br></p>
<p>and to the <a type=""document"" href=""/{{localLink:cc143afe-4cbf-46e5-b399-c9f451384373}}{postfix}"" title=""other page"">other page</a></p>";

var expectedOutput =
$@"<p>Rich text outside of the blocks with a link to <a href=""/self/{postfix}"" title=""itself"" data-start-item-path=""self"" data-start-item-id=""eed5fc6b-96fd-45a5-a0f1-b1adfb483c2f"">itself</a><br><br></p>
<p>and to the <a href=""/other/{postfix}"" title=""other page"" data-start-item-path=""other"" data-start-item-id=""cc143afe-4cbf-46e5-b399-c9f451384373"">other page</a></p>";

var parsedHtml = parser.Parse(html);

Assert.AreEqual(expectedOutput, parsedHtml);
}

Check warning on line 119 in tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParserTests.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ New issue: Code Duplication

The module contains 2 functions with similar structure: Can_Parse_LocalLinks,Can_Parse_LocalLinks_With_Postfix. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

[Test]
public void Can_Parse_Inline_LocalImages()
{
Expand Down