diff --git a/src/Umbraco.Core/Routing/IPublishedRequest.cs b/src/Umbraco.Core/Routing/IPublishedRequest.cs index 3d6ab51276..57b38dbff8 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequest.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequest.cs @@ -6,7 +6,9 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Routing { - + /// + /// The result of Umbraco routing built with the + /// public interface IPublishedRequest { /// @@ -15,11 +17,6 @@ namespace Umbraco.Web.Routing /// The cleaned up Uri has no virtual directory, no trailing slash, no .aspx extension, etc. Uri Uri { get; } - /// - /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. - /// - bool IgnorePublishedContentCollisions { get; } - /// /// Gets a value indicating the requested content. /// @@ -73,8 +70,22 @@ namespace Umbraco.Web.Routing IReadOnlyDictionary Headers { get; } /// - /// Gets a value indicating if the no-cache value should be added to the Cache-Control header + /// Gets a value indicating whether the no-cache value should be added to the Cache-Control header /// bool SetNoCacheHeader { get; } + + /// + /// Gets a value indicating whether the Umbraco Backoffice should ignore a collision for this request. + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since + /// collission checking only occurs in the back office which is launched by + /// for which events do not execute. + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + bool IgnorePublishedContentCollisions { get; } } } diff --git a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs index 706315795e..f8e5837838 100644 --- a/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/IPublishedRequestBuilder.cs @@ -138,5 +138,19 @@ namespace Umbraco.Web.Routing /// Sets a dictionary of Headers to append to the Response object. /// IPublishedRequestBuilder SetHeaders(IReadOnlyDictionary headers); + + /// + /// Can be called to configure the result to ignore URL collisions + /// + /// + /// This is an uncommon API used for edge cases with complex routing and would be used + /// by developers to configure the request to disable collision checks in . + /// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers since + /// collission checking only occurs in the back office which is launched by + /// for which events do not execute. + /// More can be read about this setting here: https://github.com/umbraco/Umbraco-CMS/pull/2148, https://issues.umbraco.org/issue/U4-10345 + /// but it's still unclear how this was used. + /// + void IgnorePublishedContentCollisions(); } } diff --git a/src/Umbraco.Core/Routing/IPublishedRouter.cs b/src/Umbraco.Core/Routing/IPublishedRouter.cs index 6d974fcc44..b4c35c0e4d 100644 --- a/src/Umbraco.Core/Routing/IPublishedRouter.cs +++ b/src/Umbraco.Core/Routing/IPublishedRouter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; -using Umbraco.Core.Models; namespace Umbraco.Web.Routing { @@ -10,8 +8,6 @@ namespace Umbraco.Web.Routing /// public interface IPublishedRouter { - // TODO: consider this and UmbracoRouteValueTransformer - move some code around? - /// /// Creates a published request. /// @@ -20,18 +16,12 @@ namespace Umbraco.Web.Routing Task CreateRequestAsync(Uri uri); /// - /// Prepares a request for rendering. + /// Sends a through the routing pipeline and builds a result. /// /// The request. - /// A value indicating whether the request was successfully prepared and can be rendered. - Task RouteRequestAsync(IPublishedRequestBuilder request); - - /// - /// Tries to route a request. - /// - /// The request. - /// A value indicating whether the request can be routed to a document. - Task TryRouteRequestAsync(IPublishedRequestBuilder request); + /// The options. + /// The built instance. + Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options); /// /// Updates the request to "not found". diff --git a/src/Umbraco.Core/Routing/PublishedRequest.cs b/src/Umbraco.Core/Routing/PublishedRequest.cs index 7f1f8b63b5..bb1c28cab1 100644 --- a/src/Umbraco.Core/Routing/PublishedRequest.cs +++ b/src/Umbraco.Core/Routing/PublishedRequest.cs @@ -13,11 +13,9 @@ namespace Umbraco.Web.Routing /// /// Initializes a new instance of the class. /// - public PublishedRequest(Uri uri, IPublishedContent publishedContent, bool isInternalRedirect, ITemplate template, DomainAndUri domain, CultureInfo culture, string redirectUrl, int? responseStatusCode, IReadOnlyList cacheExtensions, IReadOnlyDictionary headers, bool cacheabilityNoCache) + public PublishedRequest(Uri uri, IPublishedContent publishedContent, bool isInternalRedirect, ITemplate template, DomainAndUri domain, CultureInfo culture, string redirectUrl, int? responseStatusCode, IReadOnlyList cacheExtensions, IReadOnlyDictionary headers, bool cacheabilityNoCache, bool ignorePublishedContentCollisions) { Uri = uri ?? throw new ArgumentNullException(nameof(uri)); - // TODO: What is this? - //IgnorePublishedContentCollisions = ignorePublishedContentCollisions; PublishedContent = publishedContent; IsInternalRedirect = isInternalRedirect; Template = template; @@ -28,6 +26,7 @@ namespace Umbraco.Web.Routing CacheExtensions = cacheExtensions; Headers = headers; SetNoCacheHeader = cacheabilityNoCache; + IgnorePublishedContentCollisions = ignorePublishedContentCollisions; } /// diff --git a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs index 77c3420399..4230e73a78 100644 --- a/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs +++ b/src/Umbraco.Core/Routing/PublishedRequestBuilder.cs @@ -18,6 +18,7 @@ namespace Umbraco.Web.Routing private string _redirectUrl; private HttpStatusCode? _responseStatus; private IPublishedContent _publishedContent; + private bool _ignorePublishedContentCollisions; /// /// Initializes a new instance of the class. @@ -70,7 +71,8 @@ namespace Umbraco.Web.Routing _responseStatus.HasValue ? (int?)_responseStatus : null, _cacheExtensions, _headers, - _cacheability); + _cacheability, + _ignorePublishedContentCollisions); /// public IPublishedRequestBuilder SetNoCacheHeader(bool cacheability) @@ -190,5 +192,8 @@ namespace Umbraco.Web.Routing Template = model; return true; } + + /// + public void IgnorePublishedContentCollisions() => _ignorePublishedContentCollisions = true; } } diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 70049660ca..c17eb3e2b7 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -100,26 +100,25 @@ namespace Umbraco.Web.Routing return publishedRequestBuilder; } - /// - public Task TryRouteRequestAsync(IPublishedRequestBuilder request) + private IPublishedRequest TryRouteRequest(IPublishedRequestBuilder request) { FindDomain(request); // TODO: This was ported from v8 but how could it possibly have a redirect here? if (request.IsRedirect()) { - return Task.FromResult(false); + return request.Build(); } // TODO: This was ported from v8 but how could it possibly have content here? if (request.HasPublishedContent()) { - return Task.FromResult(true); + return request.Build(); } FindPublishedContent(request); - return Task.FromResult(request.Build().Success()); + return request.Build(); } private void SetVariationContext(CultureInfo culture) @@ -138,11 +137,18 @@ namespace Umbraco.Web.Routing } /// - public async Task RouteRequestAsync(IPublishedRequestBuilder request) + public async Task RouteRequestAsync(IPublishedRequestBuilder request, RouteRequestOptions options) { + // outbound routing performs different/simpler logic + if (options.RouteDirection == RouteDirection.Outbound) + { + return TryRouteRequest(request); + } + // find domain FindDomain(request); + // TODO: This was ported from v8 but how could it possibly have a redirect here? // if request has been flagged to redirect then return // whoever called us is in charge of actually redirecting if (request.IsRedirect()) @@ -156,7 +162,8 @@ namespace Umbraco.Web.Routing // find the published content if it's not assigned. This could be manually assigned with a custom route handler, or // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. - if (request.PublishedContent == null) + // TODO: This might very well change when we look into porting custom routes in netcore like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. + if (!request.HasPublishedContent()) { // find the document & template FindPublishedContentAndTemplate(request); diff --git a/src/Umbraco.Core/Routing/RouteDirection.cs b/src/Umbraco.Core/Routing/RouteDirection.cs new file mode 100644 index 0000000000..1d811b06e3 --- /dev/null +++ b/src/Umbraco.Core/Routing/RouteDirection.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Web.Routing +{ + /// + /// The direction of a route + /// + public enum RouteDirection + { + /// + /// An inbound route used to map a URL to a content item + /// + Inbound = 1, + + /// + /// An outbound route used to generate a URL for a content item + /// + Outbound = 2 + } +} diff --git a/src/Umbraco.Core/Routing/RouteRequestOptions.cs b/src/Umbraco.Core/Routing/RouteRequestOptions.cs new file mode 100644 index 0000000000..91a343e2f0 --- /dev/null +++ b/src/Umbraco.Core/Routing/RouteRequestOptions.cs @@ -0,0 +1,29 @@ +using System; + +namespace Umbraco.Web.Routing +{ + /// + /// Options for routing an Umbraco request + /// + public struct RouteRequestOptions : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + public RouteRequestOptions(RouteDirection direction) => RouteDirection = direction; + + /// + /// Gets the + /// + public RouteDirection RouteDirection { get; } + + /// + public override bool Equals(object obj) => obj is RouteRequestOptions options && Equals(options); + + /// + public bool Equals(RouteRequestOptions other) => RouteDirection == other.RouteDirection; + + /// + public override int GetHashCode() => 15391035 + RouteDirection.GetHashCode(); + } +} diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index e7095feb2b..698b9ab526 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -204,20 +204,19 @@ namespace Umbraco.Web.Routing } uri = uriUtility.UriToUmbraco(uri); - IPublishedRequestBuilder pcr = await publishedRouter.CreateRequestAsync(uri); - var routeResult = await publishedRouter.TryRouteRequestAsync(pcr); + IPublishedRequestBuilder builder = await publishedRouter.CreateRequestAsync(uri); + IPublishedRequest pcr = await publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound)); - if (pcr.PublishedContent == null) + if (!pcr.HasPublishedContent()) { var urlInfo = UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture); return Attempt.Succeed(urlInfo); } - // TODO: What is this? - //if (pcr.IgnorePublishedContentCollisions) - //{ - // return false; - //} + if (pcr.IgnorePublishedContentCollisions) + { + return Attempt.Fail(); + } if (pcr.PublishedContent.Id != content.Id) { diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 9cb920f434..de6cb72edb 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -11,6 +11,7 @@ using Umbraco.Extensions; using Umbraco.Web.Common.Routing; using Umbraco.Web.Routing; using Umbraco.Web.Website.Controllers; +using RouteDirection = Umbraco.Web.Routing.RouteDirection; namespace Umbraco.Web.Website.Routing { @@ -112,7 +113,7 @@ namespace Umbraco.Web.Website.Routing // an immutable object. The only way to make this better would be to have a RouteRequest // as part of UmbracoContext but then it will require a PublishedRouter dependency so not sure that's worth it. // Maybe could be a one-time Set method instead? - IPublishedRequest publishedRequest = umbracoContext.PublishedRequest = await _publishedRouter.RouteRequestAsync(requestBuilder); + IPublishedRequest publishedRequest = umbracoContext.PublishedRequest = await _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(RouteDirection.Inbound)); return publishedRequest; } diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index aef8b4bc3e..f45371707a 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Security; using Umbraco.Web.Composing; using Umbraco.Web.Routing; +using RouteDirection = Umbraco.Web.Routing.RouteDirection; namespace Umbraco.Web { @@ -136,7 +137,7 @@ namespace Umbraco.Web // instantiate, prepare and process the published content request // important to use CleanedUmbracoUrl - lowercase path-only version of the current URL var requestBuilder = _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl).Result; - var request = umbracoContext.PublishedRequest = _publishedRouter.RouteRequestAsync(requestBuilder).Result; + var request = umbracoContext.PublishedRequest = _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(RouteDirection.Inbound)).Result; // NOTE: This has been ported to netcore // HandleHttpResponseStatus returns a value indicating that the request should