Serverside generated preview URLs (#20021)

* Serverside generated preview URLs

* Add URL provider notation to UrlInfo

* Change preview URL generation to happen at preview time based on provider alias

* Update XML docs

* Always add culture (if available) to preview URL

* Do not log user input (security vulnerability)

* Fix typo

* Re-generate TypeScript client

from Management API

* Deprecated `UmbDocumentPreviewRepository.enter()` (for v19)

Fixed TS errors

Added temp stub for `getPreviewUrl`

* Adds `previewOption` extension-type

* Adds "default" `previewOption` kind

* Relocated "Save and Preview" workspace action

reworked using the "default" `previewOption` kind.

* Added stub for "urlProvider" `previewOption` kind

* Renamed "workspace-action-default-kind.element.ts"

to a more suitable filename.

Exported element so can be reused in other packages,
e.g. documents, for the new "save and preview" feature.

* Refactored "Save and Preview" button

to work with first action's manifest/API.

* Reverted `previewOption` extension-type

Re-engineered to make a "urlProvider" kind for `workspaceActionMenuItem`.
This is to simplify the extension point and surrounding logic.

* Modified `saveAndPreview` Document Workspace Context

to accept a URL Provider Alias.

* Refactored "Save and Preview" button

to extend `UmbWorkspaceActionElement`.

This did mean exposing certain methods/properties to be overridable.

* Used `umbPeekError` to surface any errors to the user

* Renamed `urlProvider` kind to `previewOption`

* Relocated `urlProviderAlias` inside the `meta` property

* also throw an error

* Added missing `await`

* Fix build errors after forward merge

---------

Co-authored-by: leekelleher <leekelleher@gmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Laura Neto <12862535+lauraneto@users.noreply.github.com>
This commit is contained in:
Kenn Jacobsen
2025-10-08 08:27:01 +02:00
committed by GitHub
parent 74328e9496
commit 17a5477242
52 changed files with 787 additions and 222 deletions

View File

@@ -1,4 +1,5 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Tests.Common.Builders;
@@ -29,13 +30,15 @@ internal sealed class PublishedUrlInfoProviderTests : PublishedUrlInfoProviderTe
// Assert the url of subpage is correct
Assert.AreEqual(1, subPageUrls.Count);
Assert.IsTrue(subPageUrls.First().IsUrl);
Assert.AreEqual("/text-page-1/", subPageUrls.First().Text);
Assert.IsNotNull(subPageUrls.First().Url);
Assert.AreEqual("/text-page-1/", subPageUrls.First().Url!.ToString());
Assert.AreEqual(Constants.UrlProviders.Content, subPageUrls.First().Provider);
Assert.AreEqual(Subpage.Key, DocumentUrlService.GetDocumentKeyByRoute("/text-page-1/", "en-US", null, false));
// Assert the url of child of second root is not exposed
Assert.AreEqual(1, childOfSecondRootUrls.Count);
Assert.IsFalse(childOfSecondRootUrls.First().IsUrl);
Assert.IsNull(childOfSecondRootUrls.First().Url);
Assert.AreEqual(Constants.UrlProviders.Content, childOfSecondRootUrls.First().Provider);
// Ensure the url without hide top level is not finding the child of second root
Assert.AreNotEqual(childOfSecondRoot.Key, DocumentUrlService.GetDocumentKeyByRoute("/second-root/text-page-1/", "en-US", null, false));

View File

@@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Tests.Common.Builders;
@@ -35,11 +36,13 @@ internal sealed class PublishedUrlInfoProvider_hidetoplevel_false : PublishedUrl
var childOfSecondRootUrls = await PublishedUrlInfoProvider.GetAllAsync(childOfSecondRoot);
Assert.AreEqual(1, subPageUrls.Count);
Assert.IsTrue(subPageUrls.First().IsUrl);
Assert.AreEqual("/textpage/text-page-1/", subPageUrls.First().Text);
Assert.IsNotNull(subPageUrls.First().Url);
Assert.AreEqual("/textpage/text-page-1/", subPageUrls.First().Url!.ToString());
Assert.AreEqual(Constants.UrlProviders.Content, subPageUrls.First().Provider);
Assert.AreEqual(1, childOfSecondRootUrls.Count);
Assert.IsTrue(childOfSecondRootUrls.First().IsUrl);
Assert.AreEqual("/second-root/text-page-1/", childOfSecondRootUrls.First().Text);
Assert.IsNotNull(childOfSecondRootUrls.First().Url);
Assert.AreEqual("/second-root/text-page-1/", childOfSecondRootUrls.First().Url!.ToString());
Assert.AreEqual(Constants.UrlProviders.Content, childOfSecondRootUrls.First().Provider);
}
}

View File

@@ -233,8 +233,8 @@ internal sealed class DomainAndUrlsTests : UmbracoIntegrationTest
foreach (var culture in Cultures)
{
var domain = GetDomainUrlFromCultureCode(culture);
Assert.IsTrue(rootUrls.Any(x => x.Text == domain));
Assert.IsTrue(rootUrls.Any(x => x.Text == "https://localhost" + domain));
Assert.IsTrue(rootUrls.Any(x => x.Url?.ToString() == domain && x.Message == null));
Assert.IsTrue(rootUrls.Any(x => x.Url?.ToString() == "https://localhost" + domain && x.Message == null));
}
});
}
@@ -263,14 +263,14 @@ internal sealed class DomainAndUrlsTests : UmbracoIntegrationTest
Assert.AreEqual(4, rootUrls.Count());
//We expect two for the domain that is setup
Assert.IsTrue(rootUrls.Any(x => x.IsUrl && x.Text == domain && x.Culture == culture));
Assert.IsTrue(rootUrls.Any(x => x.IsUrl && x.Text == "https://localhost" + domain && x.Culture == culture));
Assert.IsTrue(rootUrls.Any(x => x.Url?.ToString() == domain && x.Culture == culture && x.Message == null));
Assert.IsTrue(rootUrls.Any(x => x.Url?.ToString() == "https://localhost" + domain && x.Culture == culture && x.Message == null));
//We expect the default language to be routable on the default path "/"
Assert.IsTrue(rootUrls.Any(x => x.IsUrl && x.Text == "/" && x.Culture == Cultures[0]));
Assert.IsTrue(rootUrls.Any(x => x.Url?.ToString() == "/" && x.Culture == Cultures[0] && x.Message == null));
//We dont expect non-default languages without a domain to be routable
Assert.IsTrue(rootUrls.Any(x => x.IsUrl == false && x.Culture == Cultures[2]));
Assert.IsTrue(rootUrls.Any(x => x.Url == null && x.Culture == Cultures[2]));
});
}

View File

@@ -98,7 +98,7 @@ public class HtmlImageSourceParserTests
It.IsAny<UrlMode>(),
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/1001/my-image.jpg"));
.Returns(UrlInfo.AsUrl("/media/1001/my-image.jpg", "Test Provider"));
var umbracoContextAccessor = new TestUmbracoContextAccessor();

View File

@@ -183,7 +183,7 @@ public class HtmlLocalLinkParserTests
It.IsAny<UrlMode>(),
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/my-test-url"));
.Returns(UrlInfo.AsUrl("/my-test-url", "Test Provider"));
var contentType = new PublishedContentType(
Guid.NewGuid(),
666,
@@ -213,7 +213,7 @@ public class HtmlLocalLinkParserTests
It.IsAny<UrlMode>(),
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/1001/my-image.jpg"));
.Returns(UrlInfo.AsUrl("/media/1001/my-image.jpg", "Test Provider"));
var umbracoContextAccessor = new TestUmbracoContextAccessor();
@@ -257,14 +257,14 @@ public class HtmlLocalLinkParserTests
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
.Returns(UrlInfo.AsUrl("/relative-url", "Test Provider"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("http://example.com/absolute-url"));
.Returns(UrlInfo.AsUrl("http://example.com/absolute-url", "Test Provider"));
var contentType = new PublishedContentType(
Guid.NewGuid(),
@@ -328,28 +328,28 @@ public class HtmlLocalLinkParserTests
UrlMode.Default,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
.Returns(UrlInfo.AsUrl("/relative-url", "Test Provider"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
.Returns(UrlInfo.AsUrl("/relative-url", "Test Provider"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("https://example.com/absolute-url"));
.Returns(UrlInfo.AsUrl("https://example.com/absolute-url", "Test Provider"));
contentUrlProvider
.Setup(x => x.GetUrl(
It.IsAny<IPublishedContent>(),
UrlMode.Auto,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/relative-url"));
.Returns(UrlInfo.AsUrl("/relative-url", "Test Provider"));
var contentType = new PublishedContentType(
Guid.NewGuid(),
@@ -371,28 +371,28 @@ public class HtmlLocalLinkParserTests
UrlMode.Default,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
.Returns(UrlInfo.AsUrl("/media/relative/image.jpg", "Test Provider"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Relative,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
.Returns(UrlInfo.AsUrl("/media/relative/image.jpg", "Test Provider"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Absolute,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("https://example.com/media/absolute/image.jpg"));
.Returns(UrlInfo.AsUrl("https://example.com/media/absolute/image.jpg", "Test Provider"));
mediaUrlProvider.Setup(x => x.GetMediaUrl(
It.IsAny<IPublishedContent>(),
It.IsAny<string>(),
UrlMode.Auto,
It.IsAny<string>(),
It.IsAny<Uri>()))
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
.Returns(UrlInfo.AsUrl("/media/relative/image.jpg", "Test Provider"));
var mediaType = new PublishedContentType(
Guid.NewGuid(),