-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Description
Which Umbraco version are you using? (Please write the exact version, example: 10.1.0)
12.3.3
Bug summary
When you are building an Umbraco site, you are not limited to having Urls generated based on their position in the Umbraco Content Tree, it's possible to manipulate the generation of the Urls by using a custom UrlProvider in combination with an IContentFinder.
In some scenarios with large Umbraco sites you might want to 'hide' a particular node from the Url.
eg if your site had lots of campaign landing pages, all in the root of the site for super SEO ranking points you might want to introduce nodes in the backoffice content tree, to look like folders but which aren't part of the Url, but help editors organise their campaign mess. eg /biscuit-offers/new-custard-cream-sale would become /new-custard-cream-sale and you could achieve this with a custom UrlProvider + IContentFinder to route the page at the root when it's not at the root.
In this scenario or similar, if the editor visited the /biscuit-offers/ node in the backoffice, and switch to info panel they would get the url /biscuit-offers/ but this isn't an actual page! or you would get the message 'this page is published but it is not in the cache'
Anyway you could always tidy this scenario up wtih Umbraco's super flexibility by having something like this in your UrlProvider
if (content.ContentType.Alias == "hiddenFolderDocTypeAlias")
{
return UrlInfo.Message("A Hidden Folder has no direct Url", culture);
}
With the expectation that this message is shown to the editor in the links section instead of a Url!
But now in V12 latest this blows up!
Stack Trace
'''
at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind, UriCreationOptions& creationOptions)
at System.Uri..ctor(String uriString)
at Umbraco.Extensions.UriExtensions.MakeAbsolute(Uri uri, Uri baseUri)
at Umbraco.Extensions.UrlProviderExtensions.DetectCollisionAsync(ILogger logger, IContent content, String url, String culture, IUmbracoContext umbracoContext, IPublishedRouter publishedRouter, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility)
at Umbraco.Extensions.UrlProviderExtensions.GetContentUrlsByCultureAsync(IContent content, IEnumerable1 cultures, IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, IContentService contentService, ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, ILogger logger, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider) at Umbraco.Extensions.UrlProviderExtensions.GetContentUrlsAsync(IContent content, IPublishedRouter publishedRouter, IUmbracoContext umbracoContext, ILocalizationService localizationService, ILocalizedTextService textService, IContentService contentService, IVariationContextAccessor variationContextAccessor, ILogger1 logger, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider)
at Umbraco.Cms.Web.BackOffice.Mapping.ContentMapDefinition.GetUrls(IContent source)
at Umbraco.Cms.Web.BackOffice.Mapping.ContentMapDefinition.Map[TVariant](IContent source, ContentItemDisplay1 target, MapperContext context) at Umbraco.Cms.Core.Mapping.UmbracoMapper.<>c__DisplayClass11_02.b__1(Object source, Object target, MapperContext context)
at Umbraco.Cms.Core.Mapping.UmbracoMapper.Map[TTarget](Object source, Type sourceType, MapperContext context)
at Umbraco.Cms.Core.Mapping.UmbracoMapper.Map[TTarget](Object source, MapperContext context)
at Umbraco.Cms.Core.Mapping.UmbracoMapper.Map[TTarget](Object source, Action`1 f)
at Umbraco.Cms.Web.BackOffice.Controllers.ContentController.MapToDisplayWithSchedule(IContent content)
at Umbraco.Cms.Web.BackOffice.Controllers.ContentController.GetById(Int32 id)
at lambda_method899(Closure, Object, Object[])
at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncObjectResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Logged|12_1(ControllerActionInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.g__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
'''
Specifics
Digging into this further...
What UrlInfo.Message does (it's an extension method on UrlInfo - https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Core/Routing/UrlInfo.cs#L80) is create a UrlInfo response from the provider with the text set to be the custom message and with the all-important isUrl flag set to false - so nobody mistakes this as an actual Url and tries to do something 'Urlish' with it...
.... but ....
In the GetContentUrlsByCultureAsync method, for each culture it tries to examine each possible Url and detect any collisions.
| private static async Task<IEnumerable<UrlInfo>> GetContentUrlsByCultureAsync( |
The PublishedUrlProvider calls GetUrl, to find the UrlInfo objects for the node from the underlying registered UrlProviders... but only returns the 'text' property as a string... not the whole UrlInfo object
then this is passed to DetectCollsionAsync -
| Attempt<UrlInfo?> hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility); |
Attempt<UrlInfo?> hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility);
(which ironically returns a list of UrlInfos!)
These 'url strings' are used to see if there is a collision.
If a collision occurs, the collision message is added to the list of returned UrlInfos...
if no collision - a brand new UrlInfo class is constructed from the string url
But the problemtherefore is triggered inside the DetectCollisionAsync method, the first line of code converts the provided Url into a Uri
var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute);
and when the Text returned from UrlProvider is a custom message 'This is a hidden folder and has no Url' rather than an actual valid routable url, this blows up!
Ways to fix?
The PublishedUrlProvider could have a GetUrlInfos method that returns UrlInfo objects from the underlying UrlProviders, and then the IsUrl property could be used to determine whether or not to detect collisions, eg no need to if isUrl is false, it can just be added to the filtered list of UrlInfos... there won't be a clash from a message!
or
we could Try and parse the Url in DetectCollsionsAsync to see if it's a valid URI, or check for starting with / or something, and again allow anything that isn't a Url to be added as a message...
or have I totally misunderstood the situation!
Happy to have a go at fixing it if it needs to be fixed and if it's not a super breaking change that can't be made til Umbraco 25 etc...
Bizarre workaround
If you set your message to be /a-hidden-folder-has-no-url/ then this gets parsed successfully as a Uri! so you avoid the error, and if you add a further custom IContentFinder, that then routes this specific Url to page not found page... then the message appears as a Url! But you can tidy this up in the EditorSendingContentNotification
Steps to reproduce
Create a Custom UrlProvider for your site (have it inherit DefaultUrlProvider), replace the DefaultUrlProvider with your custom UrlProvider with UmbracoBuilder.
In your custom UrlProvider, check for a particular doctype, and return
UrlInfo.Message("this is not a url", culture);
Then visit that DocType in the backoffice and you'll get the error.
Expected result / actual result
Expected result is to see the Custom Message appear in the Urls section for a page based on the Doc Type you are sending the custom message to.
