From 8b00e72bf6d8379a257359d7033bebfb178cb6f1 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 31 Jan 2013 10:06:25 -0100 Subject: [PATCH] Web.Routing - refactor url providing (U4-1321...) + new IUrlProvider --- .../Routing/NiceUrlProviderTests.cs | 24 +- .../NiceUrlsProviderWithDomainsTests.cs | 62 ++-- .../Routing/UrlsWithNestedDomains.cs | 4 +- .../Routing/uQueryGetNodeIdByUrlTests.cs | 4 +- .../TestHelpers/BaseRoutingTest.cs | 4 +- .../Models/PublishedContentBase.cs | 4 +- .../Mvc/RedirectToUmbracoPageResult.cs | 4 +- src/Umbraco.Web/PublishedContentExtensions.cs | 4 +- src/Umbraco.Web/Routing/AliasUrlProvider.cs | 97 +++++ .../Routing/ContentFinderByIdPath.cs | 3 - src/Umbraco.Web/Routing/DefaultUrlProvider.cs | 313 ++++++++++++++++ src/Umbraco.Web/Routing/DomainAndUri.cs | 49 +++ src/Umbraco.Web/Routing/DomainHelper.cs | 140 ++++---- src/Umbraco.Web/Routing/IUrlProvider.cs | 43 +++ src/Umbraco.Web/Routing/NiceUrlProvider.cs | 334 ------------------ .../Routing/PublishedContentRequestEngine.cs | 4 +- src/Umbraco.Web/Routing/RoutingContext.cs | 10 +- src/Umbraco.Web/Routing/UrlProvider.cs | 121 +++++++ .../Routing/UrlProviderResolver.cs | 41 +++ .../Templates/TemplateUtilities.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 7 +- src/Umbraco.Web/UmbracoContext.cs | 6 +- src/Umbraco.Web/UmbracoHelper.cs | 8 +- src/Umbraco.Web/WebBootManager.cs | 17 +- .../umbraco/editContent.aspx.cs | 8 +- 25 files changed, 816 insertions(+), 499 deletions(-) create mode 100644 src/Umbraco.Web/Routing/AliasUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/DefaultUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/DomainAndUri.cs create mode 100644 src/Umbraco.Web/Routing/IUrlProvider.cs delete mode 100644 src/Umbraco.Web/Routing/NiceUrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/UrlProvider.cs create mode 100644 src/Umbraco.Web/Routing/UrlProviderResolver.cs diff --git a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs index e717a464f4..b56352afd9 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs @@ -58,14 +58,14 @@ namespace Umbraco.Tests.Routing foreach (var sample in samples) { - var result = routingContext.NiceUrlProvider.GetNiceUrl(sample.Key); + var result = routingContext.UrlProvider.GetUrl(sample.Key); Assert.AreEqual(sample.Value, result); } var randomSample = new KeyValuePair(1177, "/home/sub1/custom-sub-1"); for (int i = 0; i < 5; i++) { - var result = routingContext.NiceUrlProvider.GetNiceUrl(randomSample.Key); + var result = routingContext.UrlProvider.GetUrl(randomSample.Key); Assert.AreEqual(randomSample.Value, result); } @@ -106,7 +106,7 @@ namespace Umbraco.Tests.Routing SettingsForTests.HideTopLevelNodeFromPath = false; SettingsForTests.UseDomainPrefixes = false; - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId); + var result = routingContext.UrlProvider.GetUrl(nodeId); Assert.AreEqual(niceUrlMatch, result); } @@ -129,7 +129,7 @@ namespace Umbraco.Tests.Routing SettingsForTests.HideTopLevelNodeFromPath = true; SettingsForTests.UseDomainPrefixes = false; - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId); + var result = routingContext.UrlProvider.GetUrl(nodeId); Assert.AreEqual(niceUrlMatch, result); } @@ -142,14 +142,14 @@ namespace Umbraco.Tests.Routing SettingsForTests.HideTopLevelNodeFromPath = false; SettingsForTests.UseDomainPrefixes = false; - Assert.AreEqual("/home/sub1/custom-sub-1/", routingContext.NiceUrlProvider.GetNiceUrl(1177)); + Assert.AreEqual("/home/sub1/custom-sub-1/", routingContext.UrlProvider.GetUrl(1177)); SettingsForTests.UseDomainPrefixes = true; - Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", routingContext.NiceUrlProvider.GetNiceUrl(1177)); + Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", routingContext.UrlProvider.GetUrl(1177)); SettingsForTests.UseDomainPrefixes = false; - routingContext.NiceUrlProvider.EnforceAbsoluteUrls = true; - Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", routingContext.NiceUrlProvider.GetNiceUrl(1177)); + routingContext.UrlProvider.EnforceAbsoluteUrls = true; + Assert.AreEqual("http://example.com/home/sub1/custom-sub-1/", routingContext.UrlProvider.GetUrl(1177)); } [Test] @@ -161,12 +161,12 @@ namespace Umbraco.Tests.Routing SettingsForTests.HideTopLevelNodeFromPath = false; SettingsForTests.UseDomainPrefixes = false; - Assert.AreEqual("#", routingContext.NiceUrlProvider.GetNiceUrl(999999)); + Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); SettingsForTests.UseDomainPrefixes = true; - Assert.AreEqual("#", routingContext.NiceUrlProvider.GetNiceUrl(999999)); + Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); SettingsForTests.UseDomainPrefixes = false; - routingContext.NiceUrlProvider.EnforceAbsoluteUrls = true; - Assert.AreEqual("#", routingContext.NiceUrlProvider.GetNiceUrl(999999)); + routingContext.UrlProvider.EnforceAbsoluteUrls = true; + Assert.AreEqual("#", routingContext.UrlProvider.GetUrl(999999)); } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs b/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs index 909ce35bd1..330c6a3739 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Routing SetDomains1(); var currentUri = new Uri(currentUrl); - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId, currentUri, absolute); + var result = routingContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); Assert.AreEqual(expected, result); } @@ -231,7 +231,7 @@ namespace Umbraco.Tests.Routing SetDomains2(); var currentUri = new Uri(currentUrl); - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId, currentUri, absolute); + var result = routingContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); Assert.AreEqual(expected, result); } @@ -252,7 +252,7 @@ namespace Umbraco.Tests.Routing SetDomains3(); var currentUri = new Uri(currentUrl); - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId, currentUri, absolute); + var result = routingContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); Assert.AreEqual(expected, result); } @@ -279,7 +279,7 @@ namespace Umbraco.Tests.Routing SetDomains4(); var currentUri = new Uri(currentUrl); - var result = routingContext.NiceUrlProvider.GetNiceUrl(nodeId, currentUri, absolute); + var result = routingContext.UrlProvider.GetUrl(nodeId, currentUri, absolute); Assert.AreEqual(expected, result); } @@ -296,17 +296,17 @@ namespace Umbraco.Tests.Routing SetDomains4(); string ignore; - ignore = routingContext.NiceUrlProvider.GetNiceUrl(1001, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(10011, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(100111, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(10012, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(100121, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(10013, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(1002, new Uri("http://domain1.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(1001, new Uri("http://domain2.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(10011, new Uri("http://domain2.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(100111, new Uri("http://domain2.com"), false); - ignore = routingContext.NiceUrlProvider.GetNiceUrl(1002, new Uri("http://domain2.com"), false); + ignore = routingContext.UrlProvider.GetUrl(1001, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(10011, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(100111, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(10012, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(100121, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(10013, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(1002, new Uri("http://domain1.com"), false); + ignore = routingContext.UrlProvider.GetUrl(1001, new Uri("http://domain2.com"), false); + ignore = routingContext.UrlProvider.GetUrl(10011, new Uri("http://domain2.com"), false); + ignore = routingContext.UrlProvider.GetUrl(100111, new Uri("http://domain2.com"), false); + ignore = routingContext.UrlProvider.GetUrl(1002, new Uri("http://domain2.com"), false); var cachedRoutes = ((DefaultRoutesCache)routingContext.RoutesCache).GetCachedRoutes(); Assert.AreEqual(7, cachedRoutes.Count); @@ -323,15 +323,15 @@ namespace Umbraco.Tests.Routing CheckRoute(cachedRoutes, cachedIds, 1002, "/1002"); // use the cache - Assert.AreEqual("/", routingContext.NiceUrlProvider.GetNiceUrl(1001, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/en/", routingContext.NiceUrlProvider.GetNiceUrl(10011, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/en/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/fr/", routingContext.NiceUrlProvider.GetNiceUrl(10012, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/fr/1001-2-1/", routingContext.NiceUrlProvider.GetNiceUrl(100121, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/1001-3/", routingContext.NiceUrlProvider.GetNiceUrl(10013, new Uri("http://domain1.com"), false)); - Assert.AreEqual("/1002/", routingContext.NiceUrlProvider.GetNiceUrl(1002, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/", routingContext.UrlProvider.GetUrl(1001, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/en/", routingContext.UrlProvider.GetUrl(10011, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/en/1001-1-1/", routingContext.UrlProvider.GetUrl(100111, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/fr/", routingContext.UrlProvider.GetUrl(10012, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/fr/1001-2-1/", routingContext.UrlProvider.GetUrl(100121, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/1001-3/", routingContext.UrlProvider.GetUrl(10013, new Uri("http://domain1.com"), false)); + Assert.AreEqual("/1002/", routingContext.UrlProvider.GetUrl(1002, new Uri("http://domain1.com"), false)); - Assert.AreEqual("http://domain1.com/fr/1001-2-1/", routingContext.NiceUrlProvider.GetNiceUrl(100121, new Uri("http://domain2.com"), false)); + Assert.AreEqual("http://domain1.com/fr/1001-2-1/", routingContext.UrlProvider.GetUrl(100121, new Uri("http://domain2.com"), false)); } void CheckRoute(IDictionary routes, IDictionary ids, int id, string route) @@ -354,17 +354,17 @@ namespace Umbraco.Tests.Routing SetDomains4(); SettingsForTests.UseDomainPrefixes = false; - Assert.AreEqual("/en/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111)); - Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100311)); + Assert.AreEqual("/en/1001-1-1/", routingContext.UrlProvider.GetUrl(100111)); + Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.UrlProvider.GetUrl(100311)); SettingsForTests.UseDomainPrefixes = true; - Assert.AreEqual("http://domain1.com/en/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111)); - Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100311)); + Assert.AreEqual("http://domain1.com/en/1001-1-1/", routingContext.UrlProvider.GetUrl(100111)); + Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.UrlProvider.GetUrl(100311)); SettingsForTests.UseDomainPrefixes = false; - routingContext.NiceUrlProvider.EnforceAbsoluteUrls = true; - Assert.AreEqual("http://domain1.com/en/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111)); - Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100311)); + routingContext.UrlProvider.EnforceAbsoluteUrls = true; + Assert.AreEqual("http://domain1.com/en/1001-1-1/", routingContext.UrlProvider.GetUrl(100111)); + Assert.AreEqual("http://domain3.com/en/1003-1-1/", routingContext.UrlProvider.GetUrl(100311)); } [Test] @@ -378,7 +378,7 @@ namespace Umbraco.Tests.Routing InitializeLanguagesAndDomains(); SetDomains5(); - var result = routingContext.NiceUrlProvider.GetAllAbsoluteNiceUrls(100111); + var result = routingContext.UrlProvider.GetOtherUrls(100111); // will always get absolute urls // all of them diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index 6ac27d62b9..3d343b2a67 100644 --- a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs +++ b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Routing // get the nice url for 100111 routingContext = GetRoutingContext(url); - Assert.AreEqual("http://domain2.com/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111, true)); + Assert.AreEqual("http://domain2.com/1001-1-1/", routingContext.UrlProvider.GetUrl(100111, true)); // check that the proper route has been cached var cachedRoutes = ((DefaultRoutesCache)routingContext.RoutesCache).GetCachedRoutes(); @@ -59,7 +59,7 @@ namespace Umbraco.Tests.Routing //Assert.AreEqual("1001/1001-1/1001-1-1", cachedRoutes[100111]); // yes // what's the nice url now? - Assert.AreEqual("http://domain2.com/1001-1-1/", routingContext.NiceUrlProvider.GetNiceUrl(100111)); // good + Assert.AreEqual("http://domain2.com/1001-1-1/", routingContext.UrlProvider.GetUrl(100111)); // good //Assert.AreEqual("http://domain1.com/1001-1/1001-1-1", routingContext.NiceUrlProvider.GetNiceUrl(100111, true)); // bad } diff --git a/src/Umbraco.Tests/Routing/uQueryGetNodeIdByUrlTests.cs b/src/Umbraco.Tests/Routing/uQueryGetNodeIdByUrlTests.cs index 6193acc474..575f354429 100644 --- a/src/Umbraco.Tests/Routing/uQueryGetNodeIdByUrlTests.cs +++ b/src/Umbraco.Tests/Routing/uQueryGetNodeIdByUrlTests.cs @@ -34,13 +34,13 @@ namespace Umbraco.Tests.Routing var umbracoContext = GetUmbracoContext(url, t.Id); var contentStore = new DefaultPublishedContentStore(); - var niceUrls = new NiceUrlProvider(contentStore, umbracoContext); + var urlProvider = new UrlProvider(umbracoContext, contentStore, new IUrlProvider[] { new DefaultUrlProvider() }); var routingContext = new RoutingContext( umbracoContext, lookups, new FakeLastChanceFinder(), contentStore, - niceUrls, + urlProvider, GetRoutesCache()); //assign the routing context back to the umbraco context diff --git a/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs b/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs index b31bbeda9b..4968daed26 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseRoutingTest.cs @@ -28,13 +28,13 @@ namespace Umbraco.Tests.TestHelpers { var umbracoContext = GetUmbracoContext(url, templateId, routeData); var contentStore = new DefaultPublishedContentStore(); - var niceUrls = new NiceUrlProvider(contentStore, umbracoContext); + var urlProvider = new UrlProvider(umbracoContext, contentStore, new IUrlProvider[] { new DefaultUrlProvider() }); var routingContext = new RoutingContext( umbracoContext, Enumerable.Empty(), new FakeLastChanceFinder(), contentStore, - niceUrls, + urlProvider, GetRoutesCache()); //assign the routing context back to the umbraco context diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index bb45ac12c4..670cc0e608 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -63,9 +63,9 @@ namespace Umbraco.Web.Models case PublishedItemType.Content: if (UmbracoContext.Current == null) throw new InvalidOperationException("Cannot resolve a Url for a content item with a null UmbracoContext.Current reference"); - if (UmbracoContext.Current.NiceUrlProvider == null) + if (UmbracoContext.Current.UrlProvider == null) throw new InvalidOperationException("Cannot resolve a Url for a content item with a null UmbracoContext.Current.NiceUrlProvider reference"); - _url= UmbracoContext.Current.NiceUrlProvider.GetNiceUrl(this.Id); + _url= UmbracoContext.Current.UrlProvider.GetUrl(this.Id); break; case PublishedItemType.Media: var prop = GetProperty("umbracoFile"); diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs index 231549cbc6..427a24f6e6 100644 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -26,8 +26,8 @@ namespace Umbraco.Web.Mvc throw new InvalidOperationException("Cannot redirect, no entity was found for id " + _pageId); } - var result = _umbracoContext.RoutingContext.NiceUrlProvider.GetNiceUrl(PublishedContent.Id); - if (result != NiceUrlProvider.NullUrl) + var result = _umbracoContext.RoutingContext.UrlProvider.GetUrl(PublishedContent.Id); + if (result != "#") { _url = result; return _url; diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 3c76260a63..15ba821814 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1173,7 +1173,7 @@ namespace Umbraco.Web if (firstNode == null) return new DataTable(); //no children found - var urlProvider = UmbracoContext.Current.RoutingContext.NiceUrlProvider; + var urlProvider = UmbracoContext.Current.RoutingContext.UrlProvider; //use new utility class to create table so that we don't have to maintain code in many places, just one var dt = Umbraco.Core.DataTableExtensions.GenerateDataTable( @@ -1204,7 +1204,7 @@ namespace Umbraco.Web {"UpdateDate", n.UpdateDate}, {"CreatorName", n.CreatorName}, {"WriterName", n.WriterName}, - {"Url", urlProvider.GetNiceUrl(n.Id)} + {"Url", urlProvider.GetUrl(n.Id)} }; var userVals = new Dictionary(); foreach (var p in from IPublishedContentProperty p in n.Properties where p.Value != null select p) diff --git a/src/Umbraco.Web/Routing/AliasUrlProvider.cs b/src/Umbraco.Web/Routing/AliasUrlProvider.cs new file mode 100644 index 0000000000..5d935f3865 --- /dev/null +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides urls using the umbracoUrlAlias property. + /// + internal class AliasUrlProvider : IUrlProvider + { + /// + /// Gets the nice url of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on url indicated by current and settings, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it should return null. + /// + public string GetUrl(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current, bool absolute) + { + return null; // we have nothing to say + } + + const string UmbracoUrlAlias = "umbracoUrlAlias"; + + private bool FindByUrlAliasEnabled + { + get + { + var hasFinder = ContentFinderResolver.Current.ContainsType(); + var hasHandler = ContentFinderResolver.Current.ContainsType() + && NotFoundHandlerHelper.CustomHandlerTypes.Contains(typeof(global::umbraco.SearchForAlias)); + return hasFinder || hasHandler; + } + } + + /// + /// Gets the other urls of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + public IEnumerable GetOtherUrls(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current) + { + if (!FindByUrlAliasEnabled) + return Enumerable.Empty(); // we have nothing to say + + var node = contentCache.GetDocumentById(umbracoContext, id); + string umbracoUrlName = null; + if (node.HasProperty(UmbracoUrlAlias)) + umbracoUrlName = node.GetPropertyValue(UmbracoUrlAlias); + if (string.IsNullOrWhiteSpace(umbracoUrlName)) + return Enumerable.Empty(); + + /* + + // walk up from that node until we hit a node with a domain, + // or we reach the content root, collecting urls in the way + var n = node; + Uri domainUri = DomainUriAtNode(n.Id, current); + while (domainUri == null && n != null) // n is null at root + { + // get the url + var urlName = n.UrlName; + + // move to parent node + n = n.Parent; + domainUri = n == null ? null : DomainUriAtNode(n.Id, current); + } + + if (domainUri == null) + return new string[] { "/" + umbracoUrlName }; + else + // fuck - there may be MANY domains actually!! + return null; + * + */ + + // just for fun + return new[] { "/" + umbracoUrlName }; + } + } +} diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index 54d21d4477..7fb3e0401a 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs @@ -1,9 +1,6 @@ using System; -using System.Diagnostics; -using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using umbraco.interfaces; using Umbraco.Core; namespace Umbraco.Web.Routing diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs new file mode 100644 index 0000000000..b2a4ecb882 --- /dev/null +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; + +using Umbraco.Core; +using Umbraco.Core.Logging; +using umbraco.cms.businesslogic.web; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides urls. + /// + internal class DefaultUrlProvider : IUrlProvider + { + #region GetUrl + + /// + /// Gets the nice url of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on url indicated by current and settings, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it should return null. + /// + public virtual string GetUrl(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current, bool absolute) + { + Uri domainUri; + string path; + + if (!current.IsAbsoluteUri) +// ReSharper disable LocalizableElement + throw new ArgumentException("Current url must be absolute.", "current"); +// ReSharper restore LocalizableElement + + // do not read cache if previewing + var route = umbracoContext.InPreviewMode + ? null + : umbracoContext.RoutingContext.RoutesCache.GetRoute(id); + + if (!string.IsNullOrEmpty(route)) + { + // there was a route in the cache - extract domainUri and path + // route is / or / + int pos = route.IndexOf('/'); + path = pos == 0 ? route : route.Substring(pos); + domainUri = pos == 0 ? null : DomainUriAtNode(int.Parse(route.Substring(0, pos)), current); + } + else + { + // there was no route in the cache - create a route + var node = contentCache.GetDocumentById(umbracoContext, id); + if (node == null) + { + LogHelper.Warn( + "Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.", + () => id); + + return null; + } + + // walk up from that node until we hit a node with a domain, + // or we reach the content root, collecting urls in the way + var pathParts = new List(); + var n = node; + domainUri = DomainUriAtNode(n.Id, current); + while (domainUri == null && n != null) // n is null at root + { + // get the url + var urlName = n.UrlName; + pathParts.Add(urlName); + + // move to parent node + n = n.Parent; + domainUri = n == null ? null : DomainUriAtNode(n.Id, current); + } + + // no domain, respect HideTopLevelNodeFromPath for legacy purposes + if (domainUri == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) + ApplyHideTopLevelNodeFromPath(umbracoContext, contentCache, node, pathParts); + + // assemble the route + pathParts.Reverse(); + path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc + route = (n == null ? "" : n.Id.ToString()) + path; + + // do not store if previewing + if (!umbracoContext.InPreviewMode) + umbracoContext.RoutingContext.RoutesCache.Store(id, route); + } + + // assemble the url from domainUri (maybe null) and path + return AssembleUrl(domainUri, path, current, absolute).ToString(); + } + + #endregion + + #region GetOtherUrls + + /// + /// Gets the other urls of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current) + { + string path; + IEnumerable domainUris; + + // will not read cache if previewing! + var route = umbracoContext.InPreviewMode + ? null + : umbracoContext.RoutingContext.RoutesCache.GetRoute(id); + + if (!string.IsNullOrEmpty(route)) + { + // there was a route in the cache - extract domainUri and path + // route is / or / + int pos = route.IndexOf('/'); + path = pos == 0 ? route : route.Substring(pos); + domainUris = pos == 0 ? null : DomainUrisAtNode(int.Parse(route.Substring(0, pos)), current); + } + else + { + // there was no route in the cache - create a route + var node = contentCache.GetDocumentById(umbracoContext, id); + if (node == null) + { + LogHelper.Warn( + "Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.", + () => id); + + return null; + } + + // walk up from that node until we hit a node with domains, + // or we reach the content root, collecting urls in the way + var pathParts = new List(); + var n = node; + domainUris = DomainUrisAtNode(n.Id, current); + while (domainUris == null && n != null) // n is null at root + { + // get the url + var urlName = node.UrlName; + pathParts.Add(urlName); + + // move to parent node + n = n.Parent; + domainUris = n == null ? null : DomainUrisAtNode(n.Id, current); + } + + // no domain, respect HideTopLevelNodeFromPath for legacy purposes + if (domainUris == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) + ApplyHideTopLevelNodeFromPath(umbracoContext, contentCache, node, pathParts); + + // assemble the route + pathParts.Reverse(); + path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc + route = (n == null ? "" : n.Id.ToString()) + path; + + // do not store if previewing + if (!umbracoContext.InPreviewMode) + umbracoContext.RoutingContext.RoutesCache.Store(id, route); + } + + // assemble the alternate urls from domainUris (maybe empty) and path + return AssembleUrls(domainUris, path).Select(uri => uri.ToString()); + } + + #endregion + + #region Utilities + + Uri AssembleUrl(Uri domainUri, string path, Uri current, bool absolute) + { + Uri uri; + + if (domainUri == null) + { + // no domain was found : return an absolute or relative url + // ignore vdir at that point + if (!absolute || current == null) + uri = new Uri(path, UriKind.Relative); + else + uri = new Uri(current.GetLeftPart(UriPartial.Authority) + path); + } + else + { + // a domain was found : return an absolute or relative url + // ignore vdir at that point + if (!absolute && current != null && domainUri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) + uri = new Uri(CombinePaths(domainUri.AbsolutePath, path), UriKind.Relative); // relative + else + uri = new Uri(CombinePaths(domainUri.GetLeftPart(UriPartial.Path), path)); // absolute + } + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain urls too! + return UriUtility.UriFromUmbraco(uri); + } + + string CombinePaths(string path1, string path2) + { + string path = path1.TrimEnd('/') + path2; + return path == "/" ? path : path.TrimEnd('/'); + } + + // always build absolute urls unless we really cannot + IEnumerable AssembleUrls(IEnumerable domainUris, string path) + { + // no domain == no "other" url + if (domainUris == null) + return Enumerable.Empty(); + + // if no domain was found and then we have no "other" url + // else return absolute urls, ignoring vdir at that point + var uris = domainUris.Select(domainUri => new Uri(CombinePaths(domainUri.GetLeftPart(UriPartial.Path), path))); + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain urls too! + return uris.Select(UriUtility.UriFromUmbraco); + } + + Uri DomainUriAtNode(int nodeId, Uri current) + { + // be safe + if (nodeId <= 0) + return null; + + // get the domains on that node + var domains = Domain.GetDomainsById(nodeId); + + // filter those that match + var domainAndUri = DomainHelper.DomainMatch(domains, current, domainAndUris => MapDomain(current, domainAndUris)); + + return domainAndUri == null ? null : domainAndUri.Uri; + } + + /// + /// Filters a list of DomainAndUri to pick one that best matches the current request. + /// + /// The list of DomainAndUri to filter. + /// The Uri of the current request. + /// The selected DomainAndUri. + /// + /// If the filter is invoked then is _not_ empty and + /// is _not_ null, and could not be + /// matched with anything in . + /// The filter _must_ return something else an exception will be thrown. + /// + protected virtual DomainAndUri MapDomain(Uri current, DomainAndUri[] domainAndUris) + { + // all we can do at the moment + return domainAndUris.First(); + } + + Uri[] DomainUrisAtNode(int nodeId, Uri current) + { + // be safe + if (nodeId <= 0) + return null; + + var domainAndUris = DomainHelper.DomainMatches(Domain.GetDomainsById(nodeId), current).ToArray(); + + // if any, then filter them (and maybe return empty) else return null + return !domainAndUris.Any() ? null : MapDomains(current, domainAndUris).Select(d => d.Uri).ToArray(); + } + + protected virtual IEnumerable MapDomains(Uri current, DomainAndUri[] domainAndUris) + { + // all we can do at the moment + return domainAndUris; + } + + static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContentStore contentCache, Core.Models.IPublishedContent node, IList pathParts) + { + // in theory if hideTopLevelNodeFromPath is true, then there should be only once + // top-level node, or else domains should be assigned. but for backward compatibility + // we add this check - we look for the document matching "/" and if it's not us, then + // we do not hide the top level path + // it has to be taken care of in IPublishedContentStore.GetDocumentByRoute too so if + // "/foo" fails (looking for "/*/foo") we try also "/foo". + // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but + // that's the way it works pre-4.10 and we try to be backward compat for the time being + if (node.Parent == null) + { + var rootNode = contentCache.GetDocumentByRoute(umbracoContext, "/", true); + if (rootNode.Id == node.Id) // remove only if we're the default node + pathParts.RemoveAt(pathParts.Count - 1); + } + else + { + pathParts.RemoveAt(pathParts.Count - 1); + } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/DomainAndUri.cs b/src/Umbraco.Web/Routing/DomainAndUri.cs new file mode 100644 index 0000000000..5b94a233a3 --- /dev/null +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using umbraco.cms.businesslogic.web; + +namespace Umbraco.Web.Routing +{ + /// + /// Represents an Umbraco domain and its normalized uri. + /// + /// + /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. + /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. + /// + internal class DomainAndUri + { + /// + /// Initializes a new instance of the class with a Domain and a Uri. + /// + /// The Domain. + /// The Uri. + public DomainAndUri(Domain domain, Uri uri) + { + this.Domain = domain; + this.Uri = uri; + } + + /// + /// Gets or sets the Umbraco domain. + /// + public Domain Domain { get; internal set; } + + /// + /// Gets or sets the normalized uri of the domain. + /// + public Uri Uri { get; internal set; } + + /// + /// Gets a string that represents the instance. + /// + /// A string that represents the current instance. + public override string ToString() + { + return string.Format("{{ \"{0}\", \"{1}\" }}", Domain.Name, Uri); + } + } +} diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index ac34a934d2..8d7c9c9972 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; - using Umbraco.Core; using umbraco.cms.businesslogic.web; @@ -13,35 +11,6 @@ namespace Umbraco.Web.Routing /// internal class DomainHelper { - /// - /// Represents an Umbraco domain and its normalized uri. - /// - /// - /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. - /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. - /// - internal class DomainAndUri - { - /// - /// The Umbraco domain. - /// - public Domain Domain; - - /// - /// The normalized uri of the domain. - /// - public Uri Uri; - - /// - /// Gets a string that represents the instance. - /// - /// A string that represents the current instance. - public override string ToString() - { - return string.Format("{{ \"{0}\", \"{1}\" }}", Domain.Name, Uri); - } - } - private static bool IsWildcardDomain(Domain d) { // supporting null or whitespace for backward compatibility, @@ -67,50 +36,63 @@ namespace Umbraco.Web.Routing return d; } - /// - /// Finds the domain that best matches the current uri, into an enumeration of domains. - /// - /// The enumeration of Umbraco domains. - /// The uri of the current request, or null. - /// A value indicating whether to return the first domain of the list when no domain matches. - /// The domain and its normalized uri, that best matches the current uri, else the first domain (if defaultToFirst is true), else null. - public static DomainAndUri DomainMatch(IEnumerable domains, Uri current, bool defaultToFirst) - { - // sanitize the list to have proper uris for comparison (scheme, path end with /) - // we need to end with / because example.com/foo cannot match example.com/foobar - // we need to order so example.com/foo matches before example.com/ - var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; - var domainsAndUris = domains - .Where(d => !IsWildcardDomain(d)) - .Select(d => SanitizeForBackwardCompatibility(d)) - .Select(d => new { Domain = d, UriString = UriUtility.EndPathWithSlash(UriUtility.StartWithScheme(d.Name, scheme)) }) - .OrderByDescending(t => t.UriString) - .Select(t => new DomainAndUri { Domain = t.Domain, Uri = new Uri(t.UriString) }); + /// + /// Finds the domain that best matches the current uri, into an enumeration of domains. + /// + /// The enumeration of Umbraco domains. + /// The uri of the current request, or null. + /// A function to filter the list of domains, if more than one applies, or null. + /// The domain and its normalized uri, that best matches the current uri. + /// + /// If more than one domain matches, then the function is used to pick + /// the right one, unless it is null, in which case the method returns null. + /// The filter, if any, will be called only with a non-empty argument, and _must_ return something. + /// + public static DomainAndUri DomainMatch(Domain[] domains, Uri current, Func filter = null) + { + // sanitize the list to have proper uris for comparison (scheme, path end with /) + // we need to end with / because example.com/foo cannot match example.com/foobar + // we need to order so example.com/foo matches before example.com/ + var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; + var domainsAndUris = domains + .Where(d => !IsWildcardDomain(d)) + .Select(SanitizeForBackwardCompatibility) + .Select(d => new { Domain = d, UriString = UriUtility.EndPathWithSlash(UriUtility.StartWithScheme(d.Name, scheme)) }) + .OrderByDescending(t => t.UriString) + .Select(t => new DomainAndUri(t.Domain, new Uri(t.UriString))) + .ToArray(); - if (!domainsAndUris.Any()) - return null; + if (!domainsAndUris.Any()) + return null; - DomainAndUri domainAndUri; - if (current == null) - { - // take the first one by default - domainAndUri = domainsAndUris.First(); - } - else - { - // look for a domain that would be the base of the hint - // else take the first one by default - var hintWithSlash = current.EndPathWithSlash(); - domainAndUri = domainsAndUris - .FirstOrDefault(t => t.Uri.IsBaseOf(hintWithSlash)); - if (domainAndUri == null && defaultToFirst) - domainAndUri = domainsAndUris.First(); - } + DomainAndUri domainAndUri; + if (current == null) + { + // take the first one by default (is that OK?) + domainAndUri = domainsAndUris.First(); + } + else + { + // look for a domain that would be the base of the hint + // assume only one can match the hint (is that OK?) + var hintWithSlash = current.EndPathWithSlash(); + domainAndUri = domainsAndUris + .FirstOrDefault(t => t.Uri.IsBaseOf(hintWithSlash)); + // if none matches, then try to run the filter to sort them out + if (domainAndUri == null && filter != null) + { + domainAndUri = filter(domainsAndUris); + // if still nothing, pick the first one? + // no: move that constraint to the filter, but check + if (domainAndUri == null) + throw new InvalidOperationException("The filter returned null."); + } + } - if (domainAndUri != null) - domainAndUri.Uri = domainAndUri.Uri.TrimPathEndSlash(); - return domainAndUri; - } + if (domainAndUri != null) + domainAndUri.Uri = domainAndUri.Uri.TrimPathEndSlash(); + return domainAndUri; + } /// /// Gets an enumeration of matching an enumeration of Umbraco domains. @@ -118,15 +100,15 @@ namespace Umbraco.Web.Routing /// The enumeration of Umbraco domains. /// The uri of the current request, or null. /// The enumeration of matching the enumeration of Umbraco domains. - public static IEnumerable DomainMatches(IEnumerable domains, Uri current) + public static IEnumerable DomainMatches(Domain[] domains, Uri current) { - var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; + var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; var domainsAndUris = domains .Where(d => !IsWildcardDomain(d)) - .Select(d => SanitizeForBackwardCompatibility(d)) + .Select(SanitizeForBackwardCompatibility) .Select(d => new { Domain = d, UriString = UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(d.Name, scheme)) }) .OrderByDescending(t => t.UriString) - .Select(t => new DomainAndUri { Domain = t.Domain, Uri = new Uri(t.UriString) }); + .Select(t => new DomainAndUri(t.Domain, new Uri(t.UriString))); return domainsAndUris; } @@ -143,7 +125,7 @@ namespace Umbraco.Web.Routing return path.Split(',') .Reverse() - .Select(id => int.Parse(id)) + .Select(int.Parse) .TakeWhile(id => id != stopNodeId) .Any(id => domains.Any(d => d.RootNodeId == id && !IsWildcardDomain(d))); } @@ -151,11 +133,11 @@ namespace Umbraco.Web.Routing /// /// Gets the deepest wildcard in a node path. /// - /// The enumeration of Umbraco domains. + /// The Umbraco domains. /// The node path. /// The current domain root node identifier, or null. /// The deepest wildcard in the path, or null. - public static Domain LookForWildcardDomain(IEnumerable domains, string path, int? rootNodeId) + public static Domain LookForWildcardDomain(Domain[] domains, string path, int? rootNodeId) { // "When you perform comparisons with nullable types, if the value of one of the nullable // types is null and the other is not, all comparisons evaluate to false." diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs new file mode 100644 index 0000000000..86e3791824 --- /dev/null +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides urls. + /// + internal interface IUrlProvider + { + /// + /// Gets the nice url of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on url indicated by current and settings, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it should return null. + /// + string GetUrl(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current, bool absolute); + + /// + /// Gets the other urls of a published content. + /// + /// The Umbraco context. + /// The content cache. + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + IEnumerable GetOtherUrls(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current); + } +} diff --git a/src/Umbraco.Web/Routing/NiceUrlProvider.cs b/src/Umbraco.Web/Routing/NiceUrlProvider.cs deleted file mode 100644 index 59cfc83e8d..0000000000 --- a/src/Umbraco.Web/Routing/NiceUrlProvider.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -using Umbraco.Core; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Web.Routing; - -using umbraco; -using umbraco.cms.businesslogic.web; - -namespace Umbraco.Web.Routing -{ - /// - /// Provides nice urls for a nodes. - /// - internal class NiceUrlProvider - { - internal const string NullUrl = "#"; - - /// - /// Initializes a new instance of the class. - /// - /// The content store. - /// The Umbraco context. - public NiceUrlProvider(IPublishedContentStore publishedContentStore, UmbracoContext umbracoContext) - { - _umbracoContext = umbracoContext; - _publishedContentStore = publishedContentStore; - this.EnforceAbsoluteUrls = false; - } - - private readonly UmbracoContext _umbracoContext; - private readonly IPublishedContentStore _publishedContentStore; - - public bool EnforceAbsoluteUrls { get; set; } - - #region GetNiceUrl - - /// - /// Gets the nice url of a node. - /// - /// The node identifier. - /// The nice url for the node. - /// The url is absolute or relative depending on the current url, settings, and options. - public string GetNiceUrl(int nodeId) - { - var absolute = UmbracoSettings.UseDomainPrefixes || this.EnforceAbsoluteUrls; - return GetNiceUrl(nodeId, _umbracoContext.CleanedUmbracoUrl, absolute); - } - - /// - /// Gets the nice url of a node. - /// - /// The node identifier. - /// A value indicating whether the url should be absolute in any case. - /// The nice url for the node. - /// The url is absolute or relative depending on the current url, unless absolute is true, in which case the url is always absolute. - public string GetNiceUrl(int nodeId, bool absolute) - { - return GetNiceUrl(nodeId, _umbracoContext.CleanedUmbracoUrl, absolute); - } - - /// - /// Gets the nice url of a node. - /// - /// The node id. - /// The current absolute url. - /// A value indicating whether the url should be absolute in any case. - /// The nice url for the node. - /// The url is absolute or relative depending on url indicated by current, unless absolute is true, in which case the url is always absolute. - public string GetNiceUrl(int nodeId, Uri current, bool absolute) - { - Uri domainUri; - string path; - - if (!current.IsAbsoluteUri) - throw new ArgumentException("Current url must be absolute.", "current"); - - // do not read cache if previewing - var route = _umbracoContext.InPreviewMode - ? null - : _umbracoContext.RoutingContext.RoutesCache.GetRoute(nodeId); - - if (!string.IsNullOrEmpty(route)) - { - // there was a route in the cache - extract domainUri and path - // route is / or / - int pos = route.IndexOf('/'); - path = pos == 0 ? route : route.Substring(pos); - domainUri = pos == 0 ? null : DomainUriAtNode(int.Parse(route.Substring(0, pos)), current); - } - else - { - // there was no route in the cache - create a route - var node = _publishedContentStore.GetDocumentById(_umbracoContext, nodeId); - if (node == null) - { - LogHelper.Warn( - "Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.", - () => nodeId); - - return NullUrl; - } - - // walk up from that node until we hit a node with a domain, - // or we reach the content root, collecting urls in the way - var pathParts = new List(); - var n = node; - domainUri = DomainUriAtNode(n.Id, current); - while (domainUri == null && n != null) // n is null at root - { - // get the url - var urlName = n.UrlName; - pathParts.Add(urlName); - - // move to parent node - n = n.Parent; - domainUri = n == null ? null : DomainUriAtNode(n.Id, current); - } - - // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (domainUri == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(node, pathParts); - - // assemble the route - pathParts.Reverse(); - path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - route = (n == null ? "" : n.Id.ToString()) + path; - - // do not store if previewing - if (!_umbracoContext.InPreviewMode) - _umbracoContext.RoutingContext.RoutesCache.Store(nodeId, route); - } - - // assemble the url from domainUri (maybe null) and path - return AssembleUrl(domainUri, path, current, absolute).ToString(); - } - - #endregion - - #region GetAlternateNiceUrls - - public IEnumerable GetAllAbsoluteNiceUrls(int nodeId) - { - return GetAllAbsoluteNiceUrls(nodeId, _umbracoContext.CleanedUmbracoUrl); - } - - /// - /// Gets the nice urls of a node. - /// - /// The node id. - /// The current url. - /// An enumeration of all valid urls for the node. - /// The urls are absolute. A node can have more than one url if more than one domain is defined. - public IEnumerable GetAllAbsoluteNiceUrls(int nodeId, Uri current) - { - // this is for editContent.aspx which had its own, highly buggy, implementation of NiceUrl... - //TODO: finalize & test implementation then replace in editContent.aspx - - string path; - IEnumerable domainUris; - - // will not read cache if previewing! - var route = _umbracoContext.InPreviewMode - ? null - : _umbracoContext.RoutingContext.RoutesCache.GetRoute(nodeId); - - if (!string.IsNullOrEmpty(route)) - { - // there was a route in the cache - extract domainUri and path - // route is / or / - int pos = route.IndexOf('/'); - path = pos == 0 ? route : route.Substring(pos); - domainUris = pos == 0 ? new Uri[] { } : DomainUrisAtNode(int.Parse(route.Substring(0, pos)), current); - } - else - { - // there was no route in the cache - create a route - var node = _publishedContentStore.GetDocumentById(_umbracoContext, nodeId); - if (node == null) - { - LogHelper.Warn( - "Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.", - () => nodeId); - - return new string[] { NullUrl }; - } - - // walk up from that node until we hit a node with domains, - // or we reach the content root, collecting urls in the way - var pathParts = new List(); - var n = node; - domainUris = DomainUrisAtNode(n.Id, current); - while (!domainUris.Any() && n != null) // n is null at root - { - // get the url - var urlName = node.UrlName; - pathParts.Add(urlName); - - // move to parent node - n = n.Parent; - domainUris = n == null ? new Uri[] { } : DomainUrisAtNode(n.Id, current); - } - - // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (!domainUris.Any() && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(node, pathParts); - - // assemble the route - pathParts.Reverse(); - path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - route = (n == null ? "" : n.Id.ToString()) + path; - - // do not store if previewing - if (!_umbracoContext.InPreviewMode) - _umbracoContext.RoutingContext.RoutesCache.Store(nodeId, route); - } - - // assemble the alternate urls from domainUris (maybe empty) and path - return AssembleUrls(domainUris, path, current).Select(uri => uri.ToString()); - } - - #endregion - - #region Utilities - - Uri AssembleUrl(Uri domainUri, string path, Uri current, bool absolute) - { - Uri uri; - - if (domainUri == null) - { - // no domain was found : return an absolute or relative url - // ignore vdir at that point - if (!absolute || current == null) - uri = new Uri(path, UriKind.Relative); - else - uri = new Uri(current.GetLeftPart(UriPartial.Authority) + path); - } - else - { - // a domain was found : return an absolute or relative url - // ignore vdir at that point - if (!absolute && current != null && domainUri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) - uri = new Uri(CombinePaths(domainUri.AbsolutePath, path), UriKind.Relative); // relative - else - uri = new Uri(CombinePaths(domainUri.GetLeftPart(UriPartial.Path), path)); // absolute - } - - // UriFromUmbraco will handle vdir - // meaning it will add vdir into domain urls too! - return UriUtility.UriFromUmbraco(uri); - } - - string CombinePaths(string path1, string path2) - { - string path = path1.TrimEnd('/') + path2; - return path == "/" ? path : path.TrimEnd('/'); - } - - // always build absolute urls unless we really cannot - IEnumerable AssembleUrls(IEnumerable domainUris, string path, Uri current) - { - List uris = new List(); - if (!domainUris.Any()) - { - // no domain was found : return an absolute or relative url - // ignore vdir at that point - if (current == null) - uris.Add(new Uri(path, UriKind.Relative)); - else - uris.Add(new Uri(current.GetLeftPart(UriPartial.Authority) + path)); - } - else - { - // domains were found : return absolute urls - // ignore vdir at that point - uris.AddRange(domainUris.Select(domainUri => new Uri(CombinePaths(domainUri.GetLeftPart(UriPartial.Path), path)))); - } - - // UriFromUmbraco will handle vdir - // meaning it will add vdir into domain urls too! - return uris.Select(uri => UriUtility.UriFromUmbraco(uri)); - } - - Uri DomainUriAtNode(int nodeId, Uri current) - { - // be safe - if (nodeId <= 0) - return null; - - // apply filter on domains defined on that node - var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomainsById(nodeId), current, true); - return domainAndUri == null ? null : domainAndUri.Uri; - } - - IEnumerable DomainUrisAtNode(int nodeId, Uri current) - { - // be safe - if (nodeId <= 0) - return new Uri[] { }; - - var domainAndUris = DomainHelper.DomainMatches(Domain.GetDomainsById(nodeId), current); - return domainAndUris.Select(d => d.Uri); - } - - void ApplyHideTopLevelNodeFromPath(Core.Models.IPublishedContent node, List pathParts) - { - // in theory if hideTopLevelNodeFromPath is true, then there should be only once - // top-level node, or else domains should be assigned. but for backward compatibility - // we add this check - we look for the document matching "/" and if it's not us, then - // we do not hide the top level path - // it has to be taken care of in IPublishedContentStore.GetDocumentByRoute too so if - // "/foo" fails (looking for "/*/foo") we try also "/foo". - // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but - // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (node.Parent == null) - { - var rootNode = _publishedContentStore.GetDocumentByRoute(_umbracoContext, "/", true); - if (rootNode.Id == node.Id) // remove only if we're the default node - pathParts.RemoveAt(pathParts.Count - 1); - } - else - { - pathParts.RemoveAt(pathParts.Count - 1); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index af7336b578..feff852982 100644 --- a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs +++ b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs @@ -166,7 +166,7 @@ namespace Umbraco.Web.Routing LogHelper.Debug("{0}Uri=\"{1}\"", () => tracePrefix, () => _pcr.Uri); // try to find a domain matching the current request - var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains().ToArray(), _pcr.Uri, false); + var domainAndUri = DomainHelper.DomainMatch(Domain.GetDomains().ToArray(), _pcr.Uri); // handle domain if (domainAndUri != null) @@ -622,7 +622,7 @@ namespace Umbraco.Web.Routing var redirectId = _pcr.PublishedContent.GetPropertyValue("umbracoRedirect", -1); var redirectUrl = "#"; if (redirectId > 0) - redirectUrl = _routingContext.NiceUrlProvider.GetNiceUrl(redirectId); + redirectUrl = _routingContext.UrlProvider.GetUrl(redirectId); if (redirectUrl != "#") _pcr.SetRedirect(redirectUrl); } diff --git a/src/Umbraco.Web/Routing/RoutingContext.cs b/src/Umbraco.Web/Routing/RoutingContext.cs index 377e909f8e..8b5784ded7 100644 --- a/src/Umbraco.Web/Routing/RoutingContext.cs +++ b/src/Umbraco.Web/Routing/RoutingContext.cs @@ -16,20 +16,20 @@ namespace Umbraco.Web.Routing /// The document lookups resolver. /// /// The content store. - /// The nice urls resolver. + /// The nice urls provider. internal RoutingContext( UmbracoContext umbracoContext, IEnumerable contentFinders, IContentFinder contentLastChanceFinder, IPublishedContentStore publishedContentStore, - NiceUrlProvider niceUrlResolver, + UrlProvider urlProvider, IRoutesCache routesCache) { this.UmbracoContext = umbracoContext; this.PublishedContentFinders = contentFinders; this.PublishedContentLastChanceFinder = contentLastChanceFinder; this.PublishedContentStore = publishedContentStore; - this.NiceUrlProvider = niceUrlResolver; + this.UrlProvider = urlProvider; this.RoutesCache = routesCache; } @@ -54,9 +54,9 @@ namespace Umbraco.Web.Routing internal IPublishedContentStore PublishedContentStore { get; private set; } /// - /// Gets the nice urls provider. + /// Gets the urls provider. /// - internal NiceUrlProvider NiceUrlProvider { get; private set; } + internal UrlProvider UrlProvider { get; private set; } /// /// Gets the diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs new file mode 100644 index 0000000000..d393720df2 --- /dev/null +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides urls. + /// + internal class UrlProvider + { + /// + /// Initializes a new instance of the class with an Umbraco context, a content cache, and a list of url providers. + /// + /// The Umbraco context. + /// The content cache. + /// The list of url providers. + public UrlProvider(UmbracoContext umbracoContext, IPublishedContentStore contentCache, + IEnumerable urlProviders) + { + _umbracoContext = umbracoContext; + _contentCache = contentCache; + _urlProviders = urlProviders; + EnforceAbsoluteUrls = false; + } + + private readonly UmbracoContext _umbracoContext; + private readonly IPublishedContentStore _contentCache; + private readonly IEnumerable _urlProviders; + + /// + /// Gets or sets a value indicating whether the provider should enforce absolute urls. + /// + public bool EnforceAbsoluteUrls { get; set; } + + /// + /// Gets the url of a published content. + /// + /// The published content identifier. + /// The url for the published content. + /// + /// The url is absolute or relative depending on the current url, settings, and options. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(int id) + { + var absolute = UmbracoSettings.UseDomainPrefixes | EnforceAbsoluteUrls; + return GetUrl(id, _umbracoContext.CleanedUmbracoUrl, absolute); + } + + /// + /// Gets the nice url of a published content. + /// + /// The published content identifier. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on the current url and settings, unless absolute is true, + /// in which case the url is always absolute. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(int id, bool absolute) + { + absolute = absolute | EnforceAbsoluteUrls; + return GetUrl(id, _umbracoContext.CleanedUmbracoUrl, absolute); + } + + /// + /// Gets the nice url of a published content. + /// + /// The published content id. + /// The current absolute url. + /// A value indicating whether the url should be absolute in any case. + /// The url for the published content. + /// + /// The url is absolute or relative depending on url indicated by current and settings, unless + /// absolute is true, in which case the url is always absolute. + /// If the provider is unable to provide a url, it returns "#". + /// + public string GetUrl(int id, Uri current, bool absolute) + { + absolute = absolute | EnforceAbsoluteUrls; + var url = _urlProviders.Select(provider => provider.GetUrl(_umbracoContext, _contentCache, id, current, absolute)).FirstOrDefault(u => u != null); + return url ?? "#"; // legacy wants this + } + + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The other urls for the published content. + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// The results depend on the current url. + /// + public IEnumerable GetOtherUrls(int id) + { + return GetOtherUrls(id, _umbracoContext.CleanedUmbracoUrl); + } + + /// + /// Gets the other urls of a published content. + /// + /// The published content id. + /// The current absolute url. + /// The other urls for the published content. + /// + /// Other urls are those that GetUrl would not return in the current context, but would be valid + /// urls for the node in other contexts (different domain for current request, umbracoUrlAlias...). + /// + public IEnumerable GetOtherUrls(int id, Uri current) + { + // providers can return null or an empty list or a non-empty list, be prepared + var urls = _urlProviders.SelectMany(provider => provider.GetOtherUrls(_umbracoContext, _contentCache, id, current) ?? Enumerable.Empty()); + + return urls; + } + } +} diff --git a/src/Umbraco.Web/Routing/UrlProviderResolver.cs b/src/Umbraco.Web/Routing/UrlProviderResolver.cs new file mode 100644 index 0000000000..16de15bf77 --- /dev/null +++ b/src/Umbraco.Web/Routing/UrlProviderResolver.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Routing +{ + /// + /// Resolves IUrlProvider objects. + /// + internal sealed class UrlProviderResolver : ManyObjectsResolverBase + { + /// + /// Initializes a new instance of the class with an initial list of provider types. + /// + /// The list of provider types. + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal UrlProviderResolver(IEnumerable providerTypes) + : base(providerTypes) + { } + + /// + /// Initializes a new instance of the class with an initial list of provider types. + /// + /// The list of provider types. + /// The resolver is created by the WebBootManager and thus the constructor remains internal. + internal UrlProviderResolver(params Type[] providerTypes) + : base(providerTypes) + { } + + /// + /// Gets the providers. + /// + public IEnumerable Providers + { + get { return this.Values; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 94b36323f0..e25e75eb4a 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Templates return text; } - var niceUrlsProvider = UmbracoContext.Current.NiceUrlProvider; + var urlProvider = UmbracoContext.Current.UrlProvider; // Parse internal links MatchCollection tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); @@ -36,7 +36,7 @@ namespace Umbraco.Web.Templates if (tag.Groups.Count > 0) { string id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - string newLink = niceUrlsProvider.GetNiceUrl(int.Parse(id)); + string newLink = urlProvider.GetUrl(int.Parse(id)); text = text.Replace(tag.Value.ToString(), "href=\"" + newLink); } return text; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index bc793dc6c6..ae76a047d5 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -379,15 +379,21 @@ + + + + + + @@ -569,7 +575,6 @@ - diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 0e5ae7f1d5..db2c233b5c 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -270,13 +270,13 @@ namespace Umbraco.Web /// /// If the RoutingContext is null, this will throw an exception. /// - internal NiceUrlProvider NiceUrlProvider + internal UrlProvider UrlProvider { get { if (RoutingContext == null) - throw new InvalidOperationException("Cannot access the NiceUrlProvider when the UmbracoContext's RoutingContext is null"); - return RoutingContext.NiceUrlProvider; + throw new InvalidOperationException("Cannot access the UrlProvider when the UmbracoContext's RoutingContext is null"); + return RoutingContext.UrlProvider; } } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 1e1467b790..da51f57184 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -416,8 +416,8 @@ namespace Umbraco.Web /// String with a friendly url from a node public string NiceUrl(int nodeId) { - var niceUrlsProvider = UmbracoContext.Current.NiceUrlProvider; - return niceUrlsProvider.GetNiceUrl(nodeId); + var urlProvider = UmbracoContext.Current.UrlProvider; + return urlProvider.GetUrl(nodeId); } /// @@ -427,8 +427,8 @@ namespace Umbraco.Web /// String with a friendly url with full domain from a node public string NiceUrlWithDomain(int nodeId) { - var niceUrlsProvider = UmbracoContext.Current.NiceUrlProvider; - return niceUrlsProvider.GetNiceUrl(nodeId, true); + var urlProvider = UmbracoContext.Current.UrlProvider; + return urlProvider.GetUrl(nodeId, true); } #endregion diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 412298493a..1e9ecfba36 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -275,22 +275,25 @@ namespace Umbraco.Web PublishedMediaStoreResolver.Current = new PublishedMediaStoreResolver(new DefaultPublishedMediaStore()); FilteredControllerFactoriesResolver.Current = new FilteredControllerFactoriesResolver( - //add all known factories, devs can then modify this list on application startup either by binding to events - //or in their own global.asax + // add all known factories, devs can then modify this list on application + // startup either by binding to events or in their own global.asax new[] { typeof (RenderControllerFactory) }); + UrlProviderResolver.Current = new UrlProviderResolver( + typeof(AliasUrlProvider), + typeof(DefaultUrlProvider) + ); + // the legacy 404 will run from within ContentFinderByNotFoundHandlers below // so for the time being there is no last chance finder ContentLastChanceFinderResolver.Current = new ContentLastChanceFinderResolver(); ContentFinderResolver.Current = new ContentFinderResolver( - //add all known resolvers in the correct order, devs can then modify this list on application startup either by binding to events - //or in their own global.asax - new[] - { + // add all known resolvers in the correct order, devs can then modify this list + // on application startup either by binding to events or in their own global.asax typeof (ContentFinderByPageIdQuery), typeof (ContentFinderByNiceUrl), typeof (ContentFinderByIdPath), @@ -300,7 +303,7 @@ namespace Umbraco.Web //typeof (ContentFinderByProfile), //typeof (ContentFinderByUrlAlias), typeof (ContentFinderByNotFoundHandlers) - }); + ); RoutesCacheResolver.Current = new RoutesCacheResolver(new DefaultRoutesCache(_isForTesting == false)); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs index 8b99dc4a4e..3acf249765 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/editContent.aspx.cs @@ -390,8 +390,8 @@ namespace umbraco.cms.presentation return; } - var niceUrlProvider = Umbraco.Web.UmbracoContext.Current.RoutingContext.NiceUrlProvider; - var url = niceUrlProvider.GetNiceUrl(_document.Id); + var urlProvider = Umbraco.Web.UmbracoContext.Current.RoutingContext.UrlProvider; + var url = urlProvider.GetUrl(_document.Id); string niceUrlText = null; var altUrlsText = new System.Text.StringBuilder(); @@ -415,8 +415,8 @@ namespace umbraco.cms.presentation { niceUrlText = string.Format("{0}", url); - foreach (var altUrl in niceUrlProvider.GetAllAbsoluteNiceUrls(_document.Id).Where(u => u != url)) - altUrlsText.AppendFormat("{0}
", altUrl); + foreach (var otherUrl in urlProvider.GetOtherUrls(_document.Id)) + altUrlsText.AppendFormat("{0}
", otherUrl); } UpdateNiceUrlProperties(niceUrlText, altUrlsText.ToString());