diff --git a/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs b/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs index b3a0e466f7..0256e2b10f 100644 --- a/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs +++ b/src/Umbraco.Tests/Routing/DomainsAndCulturesTests.cs @@ -25,7 +25,7 @@ namespace Umbraco.Tests.Routing void InitializeLanguagesAndDomains() { - var domains = Domain.GetDomains(); + var domains = Domain.GetDomains(true); // we want wildcards too here foreach (var d in domains) d.Delete(); diff --git a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs index e717a464f4..73d42a1d7d 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlProviderTests.cs @@ -26,6 +26,10 @@ namespace Umbraco.Tests.Routing true); SettingsForTests.SettingsFilePath = Core.IO.IOHelper.MapPath(Core.IO.SystemDirectories.Config + Path.DirectorySeparatorChar, false); + + SiteDomainHelperResolver.Reset(); + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); + FreezeResolution(); } internal override IRoutesCache GetRoutesCache() @@ -58,14 +62,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 +110,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 +133,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 +146,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 +165,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..e899f930c9 100644 --- a/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/NiceUrlsProviderWithDomainsTests.cs @@ -20,6 +20,10 @@ namespace Umbraco.Tests.Routing // ensure we can create them although the content is not in the database TestHelper.DropForeignKeys("umbracoDomains"); + + SiteDomainHelperResolver.Reset(); + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); + FreezeResolution(); } internal override IRoutesCache GetRoutesCache() @@ -202,7 +206,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 +235,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 +256,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 +283,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 +300,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 +327,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,23 +358,23 @@ 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] public void Get_Nice_Url_Alternate() { - var routingContext = GetRoutingContext("http://domain1.com/test", 1111); + var routingContext = GetRoutingContext("http://domain1.com/en/test", 1111); SettingsForTests.UseDirectoryUrls = true; SettingsForTests.HideTopLevelNodeFromPath = false; @@ -378,13 +382,12 @@ namespace Umbraco.Tests.Routing InitializeLanguagesAndDomains(); SetDomains5(); - var result = routingContext.NiceUrlProvider.GetAllAbsoluteNiceUrls(100111); + var url = routingContext.UrlProvider.GetUrl(100111, true); + Assert.AreEqual("http://domain1.com/en/1001-1-1/", url); + + var result = routingContext.UrlProvider.GetOtherUrls(100111).ToArray(); - // will always get absolute urls - // all of them - // including the local one - duplicate?! - then must manually exclude? - Assert.AreEqual(3, result.Count()); - Assert.IsTrue(result.Contains("http://domain1.com/en/1001-1-1/")); + Assert.AreEqual(2, result.Count()); Assert.IsTrue(result.Contains("http://domain1a.com/en/1001-1-1/")); Assert.IsTrue(result.Contains("http://domain1b.com/en/1001-1-1/")); } diff --git a/src/Umbraco.Tests/Routing/SiteDomainHelperTests.cs b/src/Umbraco.Tests/Routing/SiteDomainHelperTests.cs new file mode 100644 index 0000000000..9a046925ef --- /dev/null +++ b/src/Umbraco.Tests/Routing/SiteDomainHelperTests.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Routing; +using umbraco.cms.businesslogic.web; +using System.Reflection; + +namespace Umbraco.Tests.Routing +{ + [TestFixture] + public class SiteDomainHelperTests + { + [SetUp] + public void SetUp() + { + SiteDomainHelper.Clear(); // assuming this works! + } + + [Test] + public void AddSites() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + + var sites = SiteDomainHelper.Sites; + + Assert.AreEqual(2, sites.Count); + + Assert.Contains("site1", sites.Keys); + Assert.Contains("site2", sites.Keys); + + var domains = sites["site1"]; + Assert.AreEqual(3, domains.Count()); + Assert.Contains("domain1.com", domains); + Assert.Contains("domain1.net", domains); + Assert.Contains("domain1.org", domains); + + domains = sites["site2"]; + Assert.AreEqual(3, domains.Count()); + Assert.Contains("domain2.com", domains); + Assert.Contains("domain2.net", domains); + Assert.Contains("domain2.org", domains); + } + + [TestCase("foo")] // that one is suspect + [TestCase("domain.com")] + [TestCase("domain.com/")] + [TestCase("domain.com:12")] + [TestCase("domain.com:12/")] + [TestCase("http://www.domain.com")] + [TestCase("http://www.domain.com:12")] + [TestCase("http://www.domain.com:12/")] + [TestCase("https://foo.www.domain.com")] + [TestCase("https://foo.www.domain.com:5478/")] + public void AddValidSite(string domain) + { + SiteDomainHelper.AddSite("site1", domain); + } + + [TestCase("domain.com/foo")] + [TestCase("http:/domain.com")] + [TestCase("*")] + public void AddInvalidSite(string domain) + { + Assert.Throws(() => SiteDomainHelper.AddSite("site1", domain)); + } + + [Test] + public void AddRemoveSites() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + + var sites = SiteDomainHelper.Sites; + + SiteDomainHelper.RemoveSite("site1"); + SiteDomainHelper.RemoveSite("site3"); + + Assert.AreEqual(1, sites.Count); + + Assert.Contains("site2", sites.Keys); + } + + [Test] + public void AddSiteAgain() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site1", "domain2.com", "domain1.net"); + + var sites = SiteDomainHelper.Sites; + + Assert.AreEqual(1, sites.Count); + + Assert.Contains("site1", sites.Keys); + + var domains = sites["site1"]; + Assert.AreEqual(2, domains.Count()); + Assert.Contains("domain2.com", domains); + Assert.Contains("domain1.net", domains); + } + + [Test] + public void BindSitesOnce() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + SiteDomainHelper.AddSite("site3", "domain3.com", "domain3.net", "domain3.org"); + SiteDomainHelper.AddSite("site4", "domain4.com", "domain4.net", "domain4.org"); + + SiteDomainHelper.BindSites("site1", "site2"); + + var bindings = SiteDomainHelper.Bindings; + + Assert.AreEqual(2, bindings.Count); + Assert.Contains("site1", bindings.Keys); + Assert.Contains("site2", bindings.Keys); + + var others = bindings["site1"]; + Assert.AreEqual(1, others.Count); + Assert.Contains("site2", others); + + others = bindings["site2"]; + Assert.AreEqual(1, others.Count); + Assert.Contains("site1", others); + } + + [Test] + public void BindMoreSites() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + SiteDomainHelper.AddSite("site3", "domain3.com", "domain3.net", "domain3.org"); + SiteDomainHelper.AddSite("site4", "domain4.com", "domain4.net", "domain4.org"); + + SiteDomainHelper.BindSites("site1", "site2"); + SiteDomainHelper.BindSites("site1", "site3"); + + var bindings = SiteDomainHelper.Bindings; + + Assert.AreEqual(3, bindings.Count); + Assert.Contains("site1", bindings.Keys); + Assert.Contains("site2", bindings.Keys); + Assert.Contains("site3", bindings.Keys); + + var others = bindings["site1"]; + Assert.AreEqual(2, others.Count); + Assert.Contains("site2", others); + Assert.Contains("site3", others); + + others = bindings["site2"]; + Assert.AreEqual(2, others.Count); + Assert.Contains("site1", others); + Assert.Contains("site3", others); + + others = bindings["site3"]; + Assert.AreEqual(2, others.Count); + Assert.Contains("site1", others); + Assert.Contains("site2", others); + } + + [Test] + public void MapDomain() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + SiteDomainHelper.AddSite("site3", "domain3.com", "domain3.net", "domain3.org"); + SiteDomainHelper.AddSite("site4", "domain4.com", "domain4.net", "domain4.org"); + + //SiteDomainHelper.BindSites("site1", "site3"); + //SiteDomainHelper.BindSites("site2", "site4"); + + // map methods are not static because we can override them + var helper = new SiteDomainHelper(); + + // current is a site1 uri, domains contain current + // so we'll get current + // + var current = new Uri("http://domain1.com/foo/bar"); + var output = helper.MapDomain(current, new[] + { + new DomainAndUri(new MockDomain("domain1.com"), Uri.UriSchemeHttp), + new DomainAndUri(new MockDomain("domain2.com"), Uri.UriSchemeHttp), + }).Uri.ToString(); + Assert.AreEqual("http://domain1.com/", output); + + // current is a site1 uri, domains do not contain current + // so we'll get the corresponding site1 domain + // + current = new Uri("http://domain1.com/foo/bar"); + output = helper.MapDomain(current, new[] + { + new DomainAndUri(new MockDomain("domain1.net"), Uri.UriSchemeHttp), + new DomainAndUri(new MockDomain("domain2.net"), Uri.UriSchemeHttp) + }).Uri.ToString(); + Assert.AreEqual("http://domain1.net/", output); + + // current is a site1 uri, domains do not contain current + // so we'll get the corresponding site1 domain + // order does not matter + // + current = new Uri("http://domain1.com/foo/bar"); + output = helper.MapDomain(current, new[] + { + new DomainAndUri(new MockDomain("domain2.net"), Uri.UriSchemeHttp), + new DomainAndUri(new MockDomain("domain1.net"), Uri.UriSchemeHttp) + }).Uri.ToString(); + Assert.AreEqual("http://domain1.net/", output); + } + + [Test] + public void MapDomains() + { + SiteDomainHelper.AddSite("site1", "domain1.com", "domain1.net", "domain1.org"); + SiteDomainHelper.AddSite("site2", "domain2.com", "domain2.net", "domain2.org"); + SiteDomainHelper.AddSite("site3", "domain3.com", "domain3.net", "domain3.org"); + SiteDomainHelper.AddSite("site4", "domain4.com", "domain4.net", "domain4.org"); + + // map methods are not static because we can override them + var helper = new SiteDomainHelper(); + + // the rule is: + // - exclude the current domain + // - exclude what MapDomain would return + // - return all domains from same site, or bound sites + + // current is a site1 uri, domains contains current + // + var current = new Uri("http://domain1.com/foo/bar"); + var output = helper.MapDomains(current, new[] + { + new DomainAndUri(new MockDomain("domain1.com"), Uri.UriSchemeHttp), // no: current + what MapDomain would pick + new DomainAndUri(new MockDomain("domain2.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain3.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain4.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain1.org"), Uri.UriSchemeHttp), // yes: same site (though bogus setup) + }, true).ToArray(); + + Assert.AreEqual(1, output.Count()); + Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + + // current is a site1 uri, domains does not contain current + // + current = new Uri("http://domain1.com/foo/bar"); + output = helper.MapDomains(current, new[] + { + new DomainAndUri(new MockDomain("domain1.net"), Uri.UriSchemeHttp), // no: what MapDomain would pick + new DomainAndUri(new MockDomain("domain2.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain3.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain4.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain1.org"), Uri.UriSchemeHttp), // yes: same site (though bogus setup) + }, true).ToArray(); + + Assert.AreEqual(1, output.Count()); + Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + + SiteDomainHelper.BindSites("site1", "site3"); + SiteDomainHelper.BindSites("site2", "site4"); + + // current is a site1 uri, domains contains current + // + current = new Uri("http://domain1.com/foo/bar"); + output = helper.MapDomains(current, new[] + { + new DomainAndUri(new MockDomain("domain1.com"), Uri.UriSchemeHttp), // no: current + what MapDomain would pick + new DomainAndUri(new MockDomain("domain2.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain3.com"), Uri.UriSchemeHttp), // yes: bound site + new DomainAndUri(new MockDomain("domain3.org"), Uri.UriSchemeHttp), // yes: bound site + new DomainAndUri(new MockDomain("domain4.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain1.org"), Uri.UriSchemeHttp), // yes: same site (though bogus setup) + }, true).ToArray(); + + Assert.AreEqual(3, output.Count()); + Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.Contains("http://domain3.com/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.Contains("http://domain3.org/", output.Select(d => d.Uri.ToString()).ToArray()); + + // current is a site1 uri, domains does not contain current + // + current = new Uri("http://domain1.com/foo/bar"); + output = helper.MapDomains(current, new[] + { + new DomainAndUri(new MockDomain("domain1.net"), Uri.UriSchemeHttp), // no: what MapDomain would pick + new DomainAndUri(new MockDomain("domain2.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain3.com"), Uri.UriSchemeHttp), // yes: bound site + new DomainAndUri(new MockDomain("domain3.org"), Uri.UriSchemeHttp), // yes: bound site + new DomainAndUri(new MockDomain("domain4.com"), Uri.UriSchemeHttp), // no: not same site + new DomainAndUri(new MockDomain("domain1.org"), Uri.UriSchemeHttp), // yes: same site (though bogus setup) + }, true).ToArray(); + + Assert.AreEqual(3, output.Count()); + Assert.Contains("http://domain1.org/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.Contains("http://domain3.com/", output.Select(d => d.Uri.ToString()).ToArray()); + Assert.Contains("http://domain3.org/", output.Select(d => d.Uri.ToString()).ToArray()); + } + + class MockDomain : Domain + { + private static readonly FieldInfo NameField = typeof (Domain).GetField("_name", BindingFlags.Instance | BindingFlags.NonPublic); + + public MockDomain(string name) + { + NameField.SetValue(this, name); + } + } + } +} diff --git a/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs b/src/Umbraco.Tests/Routing/UrlsWithNestedDomains.cs index 6ac27d62b9..5deee2f194 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 } @@ -69,6 +69,10 @@ namespace Umbraco.Tests.Routing // ensure we can create them although the content is not in the database TestHelper.DropForeignKeys("umbracoDomains"); + + SiteDomainHelperResolver.Reset(); + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); + FreezeResolution(); } internal override IRoutesCache GetRoutesCache() 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.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 361afa6e92..98fd3385d6 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -287,6 +287,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 914a3f3671..9d581157fa 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -571,6 +571,7 @@ + @@ -610,6 +611,7 @@ + @@ -773,6 +775,8 @@ + + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 1e52ff4b30..90db9665b1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -5,7 +5,7 @@ http://umbraco.org - Manage hostnames + Culture and Hostnames Audit Trail Browse Node Copy @@ -36,19 +36,31 @@ Update + Permission denied. Add new Domain - Invalid hostname + remove + Invalid node. + Invalid domain format. + Domain has already been assigned. Domain + Language New domain '%0%' has been created Domain '%0%' is deleted Domain '%0%' has already been assigned - - https://www.example.com/, example.com/en, etc. Use * to match
- any domain and just set the culture.]]> +
One-level paths in domains are supported, eg. "example.com/en". However, they + they should be avoided. Better use the culture setting above.]]>
Domain '%0%' has been updated Edit Current Domains + Inherit + Culture + + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]> +
+ Domains Viewing for diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx new file mode 100644 index 0000000000..2fd4e3bdaa --- /dev/null +++ b/src/Umbraco.Web.UI/umbraco/dialogs/AssignDomain2.aspx @@ -0,0 +1,77 @@ +<%@ Page Language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" Codebehind="AssignDomain2.aspx.cs" AutoEventWireup="True" Inherits="umbraco.dialogs.AssignDomain2" %> +<%@ Import Namespace="Umbraco.Web" %> +<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> +<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> + + + + + + + + + + + + +
+
+ + + +
<%=umbraco.ui.Text("assignDomain", "setLanguageHelp") %> +
+
+ + + + + + + + + + + + + + + + + +
<%=umbraco.ui.Text("assignDomain", "domain") %><%=umbraco.ui.Text("assignDomain", "language") %> +
<%=umbraco.ui.Text("assignDomain", "remove") %>
+ + + + + +
<%=umbraco.ui.Text("assignDomain", "domainHelp") %>
+
+
+ +

+ + + <%=umbraco.ui.Text("general", "or")%> + + <%=umbraco.ui.Text("general", "cancel")%> +

+ +
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/JQuery/jquery.validate.min.js b/src/Umbraco.Web.UI/umbraco_client/Application/JQuery/jquery.validate.min.js new file mode 100644 index 0000000000..85afad5380 --- /dev/null +++ b/src/Umbraco.Web.UI/umbraco_client/Application/JQuery/jquery.validate.min.js @@ -0,0 +1,4 @@ +/*! jQuery Validation Plugin - v1.10.0 - 9/7/2012 +* https://github.com/jzaefferer/jquery-validation +* Copyright (c) 2012 Jörn Zaefferer; Licensed MIT, GPL */ +(function(a){a.extend(a.fn,{validate:function(b){if(!this.length){b&&b.debug&&window.console&&console.warn("nothing selected, can't validate, returning nothing");return}var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.validateDelegate(":submit","click",function(b){c.settings.submitHandler&&(c.submitButton=b.target),a(b.target).hasClass("cancel")&&(c.cancelSubmit=!0)}),this.submit(function(b){function d(){var d;return c.settings.submitHandler?(c.submitButton&&(d=a("").attr("name",c.submitButton.name).val(c.submitButton.value).appendTo(c.currentForm)),c.settings.submitHandler.call(c,c.currentForm,b),c.submitButton&&d.remove(),!1):!0}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){if(a(this[0]).is("form"))return this.validate().form();var b=!0,c=a(this[0].form).validate();return this.each(function(){b&=c.element(this)}),b},removeAttrs:function(b){var c={},d=this;return a.each(b.split(/\s/),function(a,b){c[b]=d.attr(b),d.removeAttr(b)}),c},rules:function(b,c){var d=this[0];if(b){var e=a.data(d.form,"validator").settings,f=e.rules,g=a.validator.staticRules(d);switch(b){case"add":a.extend(g,a.validator.normalizeRule(c)),f[d.name]=g,c.messages&&(e.messages[d.name]=a.extend(e.messages[d.name],c.messages));break;case"remove":if(!c)return delete f[d.name],g;var h={};return a.each(c.split(/\s/),function(a,b){h[b]=g[b],delete g[b]}),h}}var i=a.validator.normalizeRules(a.extend({},a.validator.metadataRules(d),a.validator.classRules(d),a.validator.attributeRules(d),a.validator.staticRules(d)),d);if(i.required){var j=i.required;delete i.required,i=a.extend({required:j},i)}return i}}),a.extend(a.expr[":"],{blank:function(b){return!a.trim(""+b.value)},filled:function(b){return!!a.trim(""+b.value)},unchecked:function(a){return!a.checked}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return arguments.length===1?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),c)}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",validClass:"valid",errorElement:"label",focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a,b){this.lastActive=a,this.settings.focusCleanup&&!this.blockFocusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.addWrapper(this.errorsFor(a)).hide())},onfocusout:function(a,b){!this.checkable(a)&&(a.name in this.submitted||!this.optional(a))&&this.element(a)},onkeyup:function(a,b){if(b.which===9&&this.elementValue(a)==="")return;(a.name in this.submitted||a===this.lastActive)&&this.element(a)},onclick:function(a,b){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){b.type==="radio"?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){b.type==="radio"?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:!1,prototype:{init:function(){function d(b){var c=a.data(this[0].form,"validator"),d="on"+b.type.replace(/^validate/,"");c.settings[d]&&c.settings[d].call(c,this[0],b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var b=this.groups={};a.each(this.settings.groups,function(c,d){a.each(d.split(/\s/),function(a,d){b[d]=c})});var c=this.settings.rules;a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).validateDelegate(":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'] ,[type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'] ","focusin focusout keyup",d).validateDelegate("[type='radio'], [type='checkbox'], select, option","click",d),this.settings.invalidHandler&&a(this.currentForm).bind("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){b=this.validationTargetFor(this.clean(b)),this.lastElement=b,this.prepareElement(b),this.currentElements=a(b);var c=this.check(b)!==!1;return c?delete this.invalid[b.name]:this.invalid[b.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),c},showErrors:function(b){if(b){a.extend(this.errorMap,b),this.errorList=[];for(var c in b)this.errorList.push({message:b[c],element:this.findByName(c)[0]});this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.submitted={},this.lastElement=null,this.prepareForm(),this.hideErrors(),this.elements().removeClass(this.settings.errorClass).removeData("previousValue")},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b=0;for(var c in a)b++;return b},hideErrors:function(){this.addWrapper(this.toHide).hide()},valid:function(){return this.size()===0},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&a.grep(this.errorList,function(a){return a.element.name===b.name}).length===1&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, [disabled]").not(this.settings.ignore).filter(function(){return!this.name&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.name in c||!b.objectLength(a(this).rules())?!1:(c[this.name]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.replace(" ",".");return a(this.settings.errorElement+"."+b,this.errorContext)},reset:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([]),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c=a(b).attr("type"),d=a(b).val();return c==="radio"||c==="checkbox"?a('input[name="'+a(b).attr("name")+'"]:checked').val():typeof d=="string"?d.replace(/\r/g,""):d},check:function(b){b=this.validationTargetFor(this.clean(b));var c=a(b).rules(),d=!1,e=this.elementValue(b),f;for(var g in c){var h={method:g,parameters:c[g]};try{f=a.validator.methods[g].call(this,e,b,h.parameters);if(f==="dependency-mismatch"){d=!0;continue}d=!1;if(f==="pending"){this.toHide=this.toHide.not(this.errorsFor(b));return}if(!f)return this.formatAndAdd(b,h),!1}catch(i){throw this.settings.debug&&window.console&&console.log("exception occured when checking element "+b.id+", check the '"+h.method+"' method",i),i}}if(d)return;return this.objectLength(c)&&this.successList.push(b),!0},customMetaMessage:function(b,c){if(!a.metadata)return;var d=this.settings.meta?a(b).metadata()[this.settings.meta]:a(b).metadata();return d&&d.messages&&d.messages[c]},customDataMessage:function(b,c){return a(b).data("msg-"+c.toLowerCase())||b.attributes&&a(b).attr("data-msg-"+c.toLowerCase())},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+"")},formatAndAdd:function(b,c){var d=this.defaultMessage(b,c.method),e=/\$?\{(\d+)\}/g;typeof d=="function"?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),this.errorList.push({message:d,element:b}),this.errorMap[b.name]=d,this.submitted[b.name]=d},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b;for(a=0;this.errorList[a];a++){var c=this.errorList[a];this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message)}this.errorList.length&&(this.toShow=this.toShow.add(this.containers));if(this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d=this.errorsFor(b);d.length?(d.removeClass(this.settings.validClass).addClass(this.settings.errorClass),d.attr("generated")&&d.html(c)):(d=a("<"+this.settings.errorElement+"/>").attr({"for":this.idOrName(b),generated:!0}).addClass(this.settings.errorClass).html(c||""),this.settings.wrapper&&(d=d.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.append(d).length||(this.settings.errorPlacement?this.settings.errorPlacement(d,a(b)):d.insertAfter(b))),!c&&this.settings.success&&(d.text(""),typeof this.settings.success=="string"?d.addClass(this.settings.success):this.settings.success(d,b)),this.toShow=this.toShow.add(d)},errorsFor:function(b){var c=this.idOrName(b);return this.errors().filter(function(){return a(this).attr("for")===c})},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(a){return this.checkable(a)&&(a=this.findByName(a.name).not(this.settings.ignore)[0]),a},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find('[name="'+b+'"]')},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):!0},dependTypes:{"boolean":function(a,b){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(a){this.pending[a.name]||(this.pendingRequest++,this.pending[a.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],c&&this.pendingRequest===0&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.formSubmitted=!1):!c&&this.pendingRequest===0&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b){return a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,"remote")})}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},attributeRules:function(b){var c={},d=a(b);for(var e in a.validator.methods){var f;e==="required"?(f=d.get(0).getAttribute(e),f===""&&(f=!0),f=!!f):f=d.attr(e),f?c[e]=f:d[0].getAttribute("type")===e&&(c[e]=!0)}return c.maxlength&&/-1|2147483647|524288/.test(c.maxlength)&&delete c.maxlength,c},metadataRules:function(b){if(!a.metadata)return{};var c=a.data(b.form,"validator").settings.meta;return c?a(b).metadata()[c]:a(b).metadata()},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1){delete b[d];return}if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=e.param!==undefined?e.param:!0:delete b[d]}}),a.each(b,function(d,e){b[d]=a.isFunction(e)?e(c):e}),a.each(["minlength","maxlength","min","max"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){b[this]&&(b[this]=[Number(b[this][0]),Number(b[this][1])])}),a.validator.autoCreateRanges&&(b.min&&b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),b.minlength&&b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b.messages&&delete b.messages,b},normalizeRule:function(b){if(typeof b=="string"){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=d!==undefined?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if(c.nodeName.toLowerCase()==="select"){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:a.trim(b).length>0},remote:function(b,c,d){if(this.optional(c))return"dependency-mismatch";var e=this.previousValue(c);this.settings.messages[c.name]||(this.settings.messages[c.name]={}),e.originalMessage=this.settings.messages[c.name].remote,this.settings.messages[c.name].remote=e.message,d=typeof d=="string"&&{url:d}||d;if(this.pending[c.name])return"pending";if(e.old===b)return e.valid;e.old=b;var f=this;this.startRequest(c);var g={};return g[c.name]=b,a.ajax(a.extend(!0,{url:d,mode:"abort",port:"validate"+c.name,dataType:"json",data:g,success:function(d){f.settings.messages[c.name].remote=e.originalMessage;var g=d===!0||d==="true";if(g){var h=f.formSubmitted;f.prepareElement(c),f.formSubmitted=h,f.successList.push(c),delete f.invalid[c.name],f.showErrors()}else{var i={},j=d||f.defaultMessage(c,"remote");i[c.name]=e.message=a.isFunction(j)?j(b):j,f.invalid[c.name]=!0,f.showErrors(i)}e.valid=g,f.stopRequest(c,g)}},d)),"pending"},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(a.trim(b),c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(a.trim(b),c);return this.optional(c)||e<=d},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(a.trim(b),c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},email:function(a,b){return this.optional(b)||/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(a)},url:function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a))},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(a)},number:function(a,b){return this.optional(b)||/^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 \-]+/.test(a))return!1;var c=0,d=0,e=!1;a=a.replace(/\D/g,"");for(var f=a.length-1;f>=0;f--){var g=a.charAt(f);d=parseInt(g,10),e&&(d*=2)>9&&(d-=9),c+=d,e=!e}return c%10===0},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.unbind(".validate-equalTo").bind("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()}}}),a.format=a.validator.format})(jQuery),function(a){var b={};if(a.ajaxPrefilter)a.ajaxPrefilter(function(a,c,d){var e=a.port;a.mode==="abort"&&(b[e]&&b[e].abort(),b[e]=d)});else{var c=a.ajax;a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return e==="abort"?(b[f]&&b[f].abort(),b[f]=c.apply(this,arguments)):c.apply(this,arguments)}}}(jQuery),function(a){!jQuery.event.special.focusin&&!jQuery.event.special.focusout&&document.addEventListener&&a.each({focus:"focusin",blur:"focusout"},function(b,c){function d(b){return b=a.event.fix(b),b.type=c,a.event.handle.call(this,b)}a.event.special[c]={setup:function(){this.addEventListener(b,d,!0)},teardown:function(){this.removeEventListener(b,d,!0)},handler:function(b){var d=arguments;return d[0]=a.event.fix(b),d[0].type=c,a.event.handle.apply(this,d)}}}),a.extend(a.fn,{validateDelegate:function(b,c,d){return this.bind(c,function(c){var e=a(c.target);if(e.is(b))return d.apply(e,arguments)})}})}(jQuery) \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js index 933ba3b5b0..6aabdbbfe7 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js +++ b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js @@ -257,7 +257,7 @@ Umbraco.Application.Actions = function () { /// if (UmbClientMgr.mainTree().getActionNode().nodeId != '-1' && UmbClientMgr.mainTree().getActionNode().nodeType != '') { - UmbClientMgr.openModalWindow("dialogs/assignDomain.aspx?id=" + UmbClientMgr.mainTree().getActionNode().nodeId, uiKeys['actions_assignDomain'], true, 500, 420); + UmbClientMgr.openModalWindow("dialogs/assignDomain2.aspx?id=" + UmbClientMgr.mainTree().getActionNode().nodeId, uiKeys['actions_assignDomain'], true, 500, 620); } }, diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.css b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.css new file mode 100644 index 0000000000..e9ec81c77c --- /dev/null +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.css @@ -0,0 +1,58 @@ +/* Custom styles for AssignDomain2.aspx dialog */ + +button { + font-size: 11px; + color: #333333; + font-family: Trebuchet MS, Lucida Grande, verdana, arial; +} + +table.addDomain { + width: 100%; + margin-top: 8px; +} + +table.domains { + width: 100%; +} + +table.addDomain td.help { + padding-left:48px; + padding-top:4px; +} + +button { + white-space: nowrap; +} + +#komask { + background: #ffffff; + opacity: .6; + z-index: 99; + display: none; + position: absolute; +} + +input.domain { + width: 296px; +} + +input.domain.error { + padding: 0; + margin: 0; +} + +select.language { + width: 100px; +} + +label.error { + padding: 0 0 6px 0; + margin: 0; + background: none; + border:none; +} + +a.remove { + color: #ff0000; + padding-left: 8px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js new file mode 100644 index 0000000000..e1e7b14c36 --- /dev/null +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/AssignDomain2.js @@ -0,0 +1,130 @@ +Umbraco.Sys.registerNamespace("Umbraco.Dialogs"); + +(function ($) { + + // register AssignDomain dialog + Umbraco.Dialogs.AssignDomain2 = base2.Base.extend({ + + _opts: null, + + _isRepeated: function (element) { + var inputs = $('#form1 input.domain'); + var elementName = element.attr('name'); + var repeated = false; + inputs.each(function() { + var input = $(this); + if (input.attr('name') != elementName && input.val() == element.val()) + repeated = true; + }); + return repeated; + }, + + // constructor + constructor: function (opts) { + // merge options with default + this._opts = $.extend({ + invalidDomain: 'Invalid domain.', + duplicateDomain: 'Domain has already been assigned.' + }, opts); + }, + + // public methods/variables + + languages: null, + language: null, + domains: null, + + addDomain: function () { + this.domains.push({ + Name: "", + Lang: "" + }); + }, + + init: function () { + var self = this; + + self.domains = ko.observableArray(self._opts.domains); + self.languages = self._opts.languages; + self.language = self._opts.language; + self.removeDomain = function() { self.domains.remove(this); }; + + ko.applyBindings(self); + + $.validator.addMethod("domain", function (value, element, param) { + var re = /^(http[s]?:\/\/)?([-\w]+(\.[-\w]+)*)(:\d+)?(\/[-\w]*)?$/gi; + return this.optional(element) || re.test(value); + }, self._opts.invalidDomain); + + $.validator.addMethod("duplicate", function (value, element, param) { + return $(element).nextAll('input').val() == 0 && !self._isRepeated($(element)); + }, self._opts.duplicateDomain); + + $.validator.addClassRules({ + domain: { domain: true }, + duplicate: { duplicate: true } + }); + + $('#form1').validate({ + debug: true, + focusCleanup: true, + onkeyup: false + }); + + $('#form1 input.domain').live('focus', function(event) { + if (event.type != 'focusin') return; + $(this).nextAll('input').val(0); + }); + + // force validation *now* + $('#form1').valid(); + + $('#btnSave').click(function () { + if (!$('#form1').valid()) + return false; + + var mask = $('#komask'); + var masked = mask.next(); + mask.height(masked.height()); + mask.width(masked.width()); + mask.show(); + + var data = { nodeId: self._opts.nodeId, language: self.language ? self.language : 0, domains: self.domains }; + $.post(self._opts.restServiceLocation + 'SaveLanguageAndDomains', ko.toJSON(data), function (json) { + mask.hide(); + + if (json.Valid) { + UmbClientMgr.closeModalWindow(); + } + else { + var inputs = $('#form1 input.domain'); + inputs.each(function() { $(this).nextAll('input').val(0); }); + for (var i = 0; i < json.Domains.length; i++) { + var d = json.Domains[i]; + if (d.Duplicate == 1) + inputs.each(function() { + var input = $(this); + if (input.val() == d.Name) + input.nextAll('input').val(1); + }); + } + $('#form1').valid(); + } + }) + .fail(function (xhr, textStatus, errorThrown) { + mask.css('opacity', 1).css('color', "#ff0000").html(xhr.responseText); + }); + return false; + }); + } + + }); + + // set defaults for jQuery ajax calls + $.ajaxSetup({ + dataType: 'json', + cache: false, + contentType: 'application/json; charset=utf-8' + }); + +})(jQuery); \ No newline at end of file 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..6821e3c1fc --- /dev/null +++ b/src/Umbraco.Web/Routing/AliasUrlProvider.cs @@ -0,0 +1,113 @@ +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 + { + // note - at the moment we seem to accept pretty much anything as an alias + // without any form of validation ... could even prob. kill the XPath ... + // ok, this is somewhat experimental and is NOT enabled by default + + #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 string GetUrl(UmbracoContext umbracoContext, IPublishedContentStore contentCache, int id, Uri current, bool absolute) + { + return null; // we have nothing to say + } + + #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 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(); + + var n = node; + var domainUris = DomainHelper.DomainsForNode(n.Id, current, false); + while (domainUris == null && n != null) // n is null at root + { + // move to parent node + n = n.Parent; + domainUris = n == null ? null : DomainHelper.DomainsForNode(n.Id, current, false); + } + + var path = "/" + umbracoUrlName; + + if (domainUris == null) + { + var uri = new Uri(path, UriKind.Relative); + return new[] { UriUtility.UriFromUmbraco(uri).ToString() }; + } + + return domainUris + .Select(domainUri => new Uri(CombinePaths(domainUri.Uri.GetLeftPart(UriPartial.Path), path))) + .Select(uri => UriUtility.UriFromUmbraco(uri).ToString()); + } + + #endregion + + #region Utilities + + 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; + } + } + + string CombinePaths(string path1, string path2) + { + string path = path1.TrimEnd('/') + path2; + return path == "/" ? path : path.TrimEnd('/'); + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs b/src/Umbraco.Web/Routing/ContentFinderByIdPath.cs index 54d21d4477..8a7e5f4b31 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 @@ -26,10 +23,10 @@ namespace Umbraco.Web.Routing IPublishedContent node = null; var path = docRequest.Uri.GetAbsolutePathDecoded(); - int nodeId = -1; + var nodeId = -1; if (path != "/") // no id if "/" { - string noSlashPath = path.Substring(1); + var noSlashPath = path.Substring(1); if (!Int32.TryParse(noSlashPath, out nodeId)) nodeId = -1; diff --git a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs index 660c88e979..8dc538ee15 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using Umbraco.Core; -using Umbraco.Core.Logging; +using Umbraco.Core.Logging; using Umbraco.Core.Models; namespace Umbraco.Web.Routing @@ -37,10 +31,9 @@ namespace Umbraco.Web.Routing pcr.RoutingContext.UmbracoContext, id); - if (content == null) - LogHelper.Debug("Could not find content with that id."); - else - LogHelper.Debug("Found corresponding content."); + LogHelper.Debug(content == null + ? "Could not find content with that id." + : "Found corresponding content."); } else { diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs index a25d079062..67ab79005f 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrl.cs @@ -1,8 +1,5 @@ -using System.Diagnostics; -using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using umbraco.interfaces; using Umbraco.Core; namespace Umbraco.Web.Routing @@ -97,7 +94,8 @@ namespace Umbraco.Web.Routing docreq.PublishedContent = node; LogHelper.Debug("Query matches, id={0}", () => docreq.PublishedContent.Id); - var iscanon = _doDomainLookup && !DomainHelper.ExistsDomainInPath(docreq.Domain, node.Path); + var rootNodeId = docreq.Domain == null ? (int?) null : docreq.Domain.RootNodeId; + var iscanon = _doDomainLookup && !DomainHelper.ExistsDomainInPath(DomainHelper.GetAllDomains(false), node.Path, rootNodeId); if (!iscanon) LogHelper.Debug("Non canonical url"); diff --git a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs index 98ca45bf0f..5647426066 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNiceUrlAndTemplate.cs @@ -1,10 +1,6 @@ -using System.Diagnostics; -using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Models; -using umbraco.cms.businesslogic.template; using Umbraco.Core; -using Template = umbraco.cms.businesslogic.template.Template; namespace Umbraco.Web.Routing { @@ -15,7 +11,7 @@ namespace Umbraco.Web.Routing /// Handles /foo/bar/template where /foo/bar is the nice url of a document, and template a template alias. /// If successful, then the template of the document request is also assigned. /// - internal class ContentFinderByNiceUrlAndTemplate : ContentFinderByNiceUrl, IContentFinder + internal class ContentFinderByNiceUrlAndTemplate : ContentFinderByNiceUrl { /// /// Tries to find and assign an Umbraco document to a PublishedContentRequest. diff --git a/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandler.cs b/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandler.cs index df17288766..2d120dfd9c 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandler.cs @@ -1,13 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -using Umbraco.Core; using Umbraco.Core.Logging; -using Umbraco.Core.Models; - -using umbraco; using umbraco.interfaces; namespace Umbraco.Web.Routing @@ -16,7 +8,7 @@ namespace Umbraco.Web.Routing /// Provides an implementation of that runs a legacy NotFoundHandler. /// /// Provided for backward compatibility. - internal class ContentFinderByNotFoundHandler : IContentFinder + internal class ContentFinderByNotFoundHandler : IContentFinder { /// /// Tries to find and assign an Umbraco document to a PublishedContentRequest. @@ -25,36 +17,33 @@ namespace Umbraco.Web.Routing /// A value indicating whether an Umbraco document was found and assigned. public bool TryFindDocument(PublishedContentRequest pcr) { - var type = typeof(Thandler); + var type = typeof(THandler); var handler = GetHandler(type); if (handler == null) return false; var url = NotFoundHandlerHelper.GetLegacyUrlForNotFoundHandlers(); - LogHelper.Debug>("Running for legacy url='{0}'.", () => url); + LogHelper.Debug>("Running for legacy url='{0}'.", () => url); if (handler.Execute(url) && handler.redirectID > 0) { - LogHelper.Debug>("Handler '{0}' returned id={1}.", () => type.FullName, () => handler.redirectID); + LogHelper.Debug>("Handler '{0}' returned id={1}.", () => type.FullName, () => handler.redirectID); var content = pcr.RoutingContext.PublishedContentStore.GetDocumentById( pcr.RoutingContext.UmbracoContext, handler.redirectID); - if (content == null) - LogHelper.Debug>("Could not find content with that id."); - else - LogHelper.Debug>("Found corresponding content."); + LogHelper.Debug>(content == null + ? "Could not find content with that id." + : "Found corresponding content."); - pcr.PublishedContent = content; + pcr.PublishedContent = content; return content != null; } - else - { - LogHelper.Debug>("Handler '{0}' returned nothing.", () => type.FullName); - return false; - } + + LogHelper.Debug>("Handler '{0}' returned nothing.", () => type.FullName); + return false; } INotFoundHandler GetHandler(Type type) @@ -65,7 +54,7 @@ namespace Umbraco.Web.Routing } catch (Exception e) { - LogHelper.Error>(string.Format("Error instanciating handler {0}, ignoring.", type.FullName), e); + LogHelper.Error>(string.Format("Error instanciating handler {0}, ignoring.", type.FullName), e); return null; } } diff --git a/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandlers.cs b/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandlers.cs index c88f72acd6..5826b6676b 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandlers.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByNotFoundHandlers.cs @@ -1,12 +1,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Reflection; -using System.Web; -using System.Xml; using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using umbraco.IO; using umbraco.interfaces; namespace Umbraco.Web.Routing @@ -35,8 +29,6 @@ namespace Umbraco.Web.Routing #region Copied over and adapted from presentation.requestHandler - //FIXME: this is temporary and should be obsoleted - void HandlePageNotFound(PublishedContentRequest docRequest) { var url = NotFoundHandlerHelper.GetLegacyUrlForNotFoundHandlers(); @@ -45,8 +37,9 @@ namespace Umbraco.Web.Routing foreach (var handler in GetNotFoundHandlers()) { IContentFinder finder = null; + var handlerName = handler.GetType().FullName; - LogHelper.Debug("Handler '{0}'.", () => handler.GetType().FullName); + LogHelper.Debug("Handler '{0}'.", () => handlerName); // replace with our own implementation if (handler is global::umbraco.SearchForAlias) @@ -60,14 +53,15 @@ namespace Umbraco.Web.Routing if (finder != null) { - LogHelper.Debug("Replace handler '{0}' by new finder '{1}'.", () => handler.GetType().FullName, () => finder.GetType().FullName); + var finderName = finder.GetType().FullName; + LogHelper.Debug("Replace handler '{0}' by new finder '{1}'.", () => handlerName, () => finderName); if (finder.TryFindDocument(docRequest)) { // do NOT set docRequest.PublishedContent again here as // it would clear any template that the finder might have set - LogHelper.Debug("Finder '{0}' found node with id={1}.", () => finder.GetType().FullName, () => docRequest.PublishedContent.Id); + LogHelper.Debug("Finder '{0}' found node with id={1}.", () => finderName, () => docRequest.PublishedContent.Id); if (docRequest.Is404) - LogHelper.Debug("Finder '{0}' set status to 404.", () => finder.GetType().FullName); + LogHelper.Debug("Finder '{0}' set status to 404.", () => finderName); // if we found a document, break, don't look at more handler -- we're done break; @@ -80,22 +74,23 @@ namespace Umbraco.Web.Routing // else it's a legacy handler, run if (handler.Execute(url) && handler.redirectID > 0) - { + { + var redirectId = handler.redirectID; docRequest.PublishedContent = docRequest.RoutingContext.PublishedContentStore.GetDocumentById( docRequest.RoutingContext.UmbracoContext, - handler.redirectID); + redirectId); if (!docRequest.HasPublishedContent) { - LogHelper.Debug("Handler '{0}' found node with id={1} which is not valid.", () => handler.GetType().FullName, () => handler.redirectID); + LogHelper.Debug("Handler '{0}' found node with id={1} which is not valid.", () => handlerName, () => redirectId); break; } - LogHelper.Debug("Handler '{0}' found valid node with id={1}.", () => handler.GetType().FullName, () => handler.redirectID); + LogHelper.Debug("Handler '{0}' found valid node with id={1}.", () => handlerName, () => redirectId); if (docRequest.RoutingContext.UmbracoContext.HttpContext.Response.StatusCode == 404) { - LogHelper.Debug("Handler '{0}' set status code to 404.", () => handler.GetType().FullName); + LogHelper.Debug("Handler '{0}' set status code to 404.", () => handlerName); docRequest.Is404 = true; } diff --git a/src/Umbraco.Web/Routing/ContentFinderByProfile.cs b/src/Umbraco.Web/Routing/ContentFinderByProfile.cs index e842e5939e..138cd0b6f9 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByProfile.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByProfile.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; -using System.Xml; using Umbraco.Core.Logging; using Umbraco.Core.Models; using umbraco; diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs index cc0a3e6a3a..5a2d470e52 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAlias.cs @@ -1,8 +1,5 @@ -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/ContentFinderResolver.cs b/src/Umbraco.Web/Routing/ContentFinderResolver.cs index c6ee7b2559..e2962be817 100644 --- a/src/Umbraco.Web/Routing/ContentFinderResolver.cs +++ b/src/Umbraco.Web/Routing/ContentFinderResolver.cs @@ -1,10 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; - -using Umbraco.Core; using Umbraco.Core.ObjectResolution; 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..dd0f8c73e2 --- /dev/null +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -0,0 +1,262 @@ +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) + { + DomainAndUri 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 : DomainHelper.DomainForNode(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 = DomainHelper.DomainForNode(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 : DomainHelper.DomainForNode(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 : DomainHelper.DomainsForNode(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 = DomainHelper.DomainsForNode(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 : DomainHelper.DomainsForNode(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(DomainAndUri 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.Uri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) + uri = new Uri(CombinePaths(domainUri.Uri.AbsolutePath, path), UriKind.Relative); // relative + else + uri = new Uri(CombinePaths(domainUri.Uri.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.Uri.GetLeftPart(UriPartial.Path), path))); + + // UriFromUmbraco will handle vdir + // meaning it will add vdir into domain urls too! + return uris.Select(UriUtility.UriFromUmbraco); + } + + 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..db7eecf5af --- /dev/null +++ b/src/Umbraco.Web/Routing/DomainAndUri.cs @@ -0,0 +1,45 @@ +using System; +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 scheme. + /// + /// The domain. + /// The uri scheme. + public DomainAndUri(Domain domain, string scheme) + { + Domain = domain; + Uri = new Uri(UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(domain.Name, scheme))); + } + + /// + /// Gets or sets the Umbraco domain. + /// + public Domain Domain { get; private set; } + + /// + /// Gets or sets the normalized uri of the domain. + /// + public Uri Uri { get; private 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..ccb1cfc159 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -1,185 +1,272 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; - +using System.Threading; using Umbraco.Core; using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Routing { - /// - /// Provides utilities to handle domains. - /// + /// + /// Provides utilities to handle domains. + /// 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; + #region Temp. abstract Umbraco's API - /// - /// The normalized uri of the domain. - /// - public Uri Uri; + /// + /// Gets all domains defined in the system. + /// + /// A value indicating whether to include wildcard domains. + /// All domains defined in the system. + /// This is to temporarily abstract Umbraco's API. + internal static Domain[] GetAllDomains(bool includeWildcards) + { + return Domain.GetDomains(includeWildcards).ToArray(); + } - /// - /// 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); - } - } + /// + /// Gets all domains defined in the system at a specified node. + /// + /// The node identifier. + /// A value indicating whether to include wildcard domains. + /// All domains defined in the system at the specified node. + /// This is to temporarily abstract Umbraco's API. + internal static Domain[] GetNodeDomains(int nodeId, bool includeWildcards) + { + return Domain.GetDomains(includeWildcards).Where(d => d.RootNodeId == nodeId).ToArray(); + } - private static bool IsWildcardDomain(Domain d) - { - // supporting null or whitespace for backward compatibility, - // although we should not allow ppl to create them anymore - return string.IsNullOrWhiteSpace(d.Name) || d.Name.StartsWith("*"); - } + #endregion - private static Domain SanitizeForBackwardCompatibility(Domain d) - { - // this is a _really_ nasty one that should be removed in 6.x - // some people were using hostnames such as "/en" which happened to work pre-4.10 - // but make _no_ sense at all... and 4.10 throws on them, so here we just try - // to find a way so 4.11 does not throw. - // but, really. - // no. - var context = System.Web.HttpContext.Current; - if (context != null && d.Name.StartsWith("/")) - { - // turn /en into http://whatever.com/en so it becomes a parseable uri - var authority = context.Request.Url.GetLeftPart(UriPartial.Authority); - d.Name = authority + d.Name; - } - return d; - } + #region Domain for Node - /// - /// 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 for the specified node, if any, that best matches a specified uri. + /// + /// The node identifier. + /// The uri, or null. + /// The domain and its uri, if any, that best matches the specified uri, else null. + /// If at least a domain is set on the node then the method returns the domain that + /// best matches the specified uri, else it returns null. + internal static DomainAndUri DomainForNode(int nodeId, Uri current) + { + // be safe + if (nodeId <= 0) + return null; - if (!domainsAndUris.Any()) - return null; + // get the domains on that node + var domains = GetNodeDomains(nodeId, false); - 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(); - } + // none? + if (!domains.Any()) + return null; - if (domainAndUri != null) - domainAndUri.Uri = domainAndUri.Uri.TrimPathEndSlash(); - return domainAndUri; - } + // else filter + var helper = SiteDomainHelperResolver.Current.Helper; + var domainAndUri = DomainForUri(domains, current, domainAndUris => helper.MapDomain(current, domainAndUris)); - /// - /// Gets an enumeration of matching an enumeration of Umbraco domains. - /// - /// 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) - { - 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.TrimPathEndSlash(UriUtility.StartWithScheme(d.Name, scheme)) }) - .OrderByDescending(t => t.UriString) - .Select(t => new DomainAndUri { Domain = t.Domain, Uri = new Uri(t.UriString) }); - return domainsAndUris; - } + if (domainAndUri == null) + throw new Exception("DomainForUri returned null."); - /// - /// Gets a value indicating whether there is another domain defined down in the path to a node under the current domain's root node. - /// - /// The current domain. - /// The path to a node under the current domain's root node. - /// A value indicating if there is another domain defined down in the path. - public static bool ExistsDomainInPath(Domain current, string path) - { - var domains = Domain.GetDomains(); - var stopNodeId = current == null ? -1 : current.RootNodeId; + return domainAndUri; + } - return path.Split(',') - .Reverse() - .Select(id => int.Parse(id)) - .TakeWhile(id => id != stopNodeId) - .Any(id => domains.Any(d => d.RootNodeId == id && !IsWildcardDomain(d))); - } + /// + /// Find the domains for the specified node, if any, that match a specified uri. + /// + /// The node identifier. + /// The uri, or null. + /// A value indicating whether to exclude the current/default domain. True by default. + /// The domains and their uris, that match the specified uri, else null. + /// If at least a domain is set on the node then the method returns the domains that + /// best match the specified uri, else it returns null. + internal static IEnumerable DomainsForNode(int nodeId, Uri current, bool excludeDefault = true) + { + // be safe + if (nodeId <= 0) + return null; - /// - /// Gets the deepest wildcard in a node path. - /// - /// The enumeration of 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) - { - // "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." + // get the domains on that node + var domains = GetNodeDomains(nodeId, false); - return path - .Split(',') - .Select(int.Parse) - .Skip(1) - .Reverse() - .TakeWhile(id => !rootNodeId.HasValue || id != rootNodeId) - .Select(nodeId => domains.FirstOrDefault(d => d.RootNodeId == nodeId && IsWildcardDomain(d))) - .FirstOrDefault(domain => domain != null); - } + // none? + if (!domains.Any()) + return null; - /// - /// Returns the part of a path relative to the uri of a domain. - /// - /// The normalized uri of the domain. - /// The full path of the uri. - /// The path part relative to the uri of the domain. - /// Eg the relative part of /foo/bar/nil to domain example.com/foo is /bar/nil. - public static string PathRelativeToDomain(Uri domainUri, string path) - { - return path.Substring(domainUri.AbsolutePath.Length).EnsureStartsWith('/'); - } - } + // get the domains and their uris + var domainAndUris = DomainsForUri(domains, current).ToArray(); + + // filter + var helper = SiteDomainHelperResolver.Current.Helper; + return helper.MapDomains(current, domainAndUris, excludeDefault).ToArray(); + } + + #endregion + + #region Domain for Uri + + /// + /// Finds the domain that best matches a specified uri, into a group of domains. + /// + /// The group of domains. + /// The uri, 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 specified 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. + /// + internal static DomainAndUri DomainForUri(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 => !d.IsWildcard) + .Select(SanitizeForBackwardCompatibility) + .Select(d => new DomainAndUri(d, scheme)) + .OrderByDescending(d => d.Uri.ToString()) + .ToArray(); + + if (!domainsAndUris.Any()) + return null; + + DomainAndUri domainAndUri; + if (current == null) + { + // take the first one by default (what else can we do?) + domainAndUri = domainsAndUris.First(); // .First() protected by .Any() above + } + else + { + // look for the first domain that would be the base of the hint + var hintWithSlash = current.EndPathWithSlash(); + domainAndUri = domainsAndUris + .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash)); + // if none matches, then try to run the filter to pick a domain + 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."); + } + } + + return domainAndUri; + } + + /// + /// Gets the domains that match a specified uri, into a group of domains. + /// + /// The group of domains. + /// The uri, or null. + /// The domains and their normalized uris, that match the specified uri. + internal static IEnumerable DomainsForUri(Domain[] domains, Uri current) + { + var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; + return domains + .Where(d => !d.IsWildcard) + .Select(SanitizeForBackwardCompatibility) + .Select(d => new DomainAndUri(d, scheme)) + .OrderByDescending(d => d.Uri.ToString()); + } + + #endregion + + #region Utilities + + /// + /// Sanitize a Domain. + /// + /// The Domain to sanitize. + /// The sanitized domain. + /// This is a _really_ nasty one that should be removed at some point. Some people were + /// using hostnames such as "/en" which happened to work pre-4.10 but really make no sense at + /// all... and 4.10 throws on them, so here we just try to find a way so 4.11 does not throw. + /// But really... no. + private static Domain SanitizeForBackwardCompatibility(Domain domain) + { + var context = System.Web.HttpContext.Current; + if (context != null && domain.Name.StartsWith("/")) + { + // turn "/en" into "http://whatever.com/en" so it becomes a parseable uri + var authority = context.Request.Url.GetLeftPart(UriPartial.Authority); + domain.Name = authority + domain.Name; + } + return domain; + } + + /// + /// Gets a value indicating whether there is another domain defined down in the path to a node under the current domain's root node. + /// + /// The domains. + /// The path to a node under the current domain's root node eg '-1,1234,5678'. + /// The current domain root node identifier, or null. + /// A value indicating if there is another domain defined down in the path. + /// Looks _under_ rootNodeId but not _at_ rootNodeId. + internal static bool ExistsDomainInPath(Domain[] domains, string path, int? rootNodeId) + { + return FindDomainInPath(domains, path, rootNodeId) != null; + } + + /// + /// Gets the deepest non-wildcard Domain, if any, from a group of Domains, in a node path. + /// + /// The domains. + /// The node path eg '-1,1234,5678'. + /// The current domain root node identifier, or null. + /// The deepest non-wildcard Domain in the path, or null. + /// Looks _under_ rootNodeId but not _at_ rootNodeId. + internal static Domain FindDomainInPath(Domain[] domains, string path, int? rootNodeId) + { + var stopNodeId = rootNodeId ?? -1; + + return path.Split(',') + .Reverse() + .Select(int.Parse) + .TakeWhile(id => id != stopNodeId) + .Select(id => domains.FirstOrDefault(d => d.RootNodeId == id && !d.IsWildcard)) + .SkipWhile(domain => domain == null) + .FirstOrDefault(); + } + + /// + /// Gets the deepest wildcard Domain, if any, from a group of Domains, in a node path. + /// + /// The domains. + /// The node path eg '-1,1234,5678'. + /// The current domain root node identifier, or null. + /// The deepest wildcard Domain in the path, or null. + /// Looks _under_ rootNodeId but not _at_ rootNodeId. + internal static Domain FindWildcardDomainInPath(Domain[] domains, string path, int? rootNodeId) + { + var stopNodeId = rootNodeId ?? -1; + + return path.Split(',') + .Reverse() + .Select(int.Parse) + .TakeWhile(id => id != stopNodeId) + .Select(id => domains.FirstOrDefault(d => d.RootNodeId == id && d.IsWildcard)) + .FirstOrDefault(domain => domain != null); + } + + /// + /// Returns the part of a path relative to the uri of a domain. + /// + /// The normalized uri of the domain. + /// The full path of the uri. + /// The path part relative to the uri of the domain. + /// Eg the relative part of /foo/bar/nil to domain example.com/foo is /bar/nil. + internal static string PathRelativeToDomain(Uri domainUri, string path) + { + return path.Substring(domainUri.AbsolutePath.Length).EnsureStartsWith('/'); + } + + #endregion + } } diff --git a/src/Umbraco.Web/Routing/IContentFinder.cs b/src/Umbraco.Web/Routing/IContentFinder.cs index d9d70c5edd..4eef138549 100644 --- a/src/Umbraco.Web/Routing/IContentFinder.cs +++ b/src/Umbraco.Web/Routing/IContentFinder.cs @@ -5,12 +5,12 @@ namespace Umbraco.Web.Routing /// internal interface IContentFinder { - /// - /// Tries to find and assign an Umbraco document to a PublishedContentRequest. - /// - /// The PublishedContentRequest. - /// A value indicating whether an Umbraco document was found and assigned. - /// Optionally, can also assign the template or anything else on the document request, although that is not required. - bool TryFindDocument(PublishedContentRequest contentRequest); + /// + /// Tries to find and assign an Umbraco document to a PublishedContentRequest. + /// + /// The PublishedContentRequest. + /// A value indicating whether an Umbraco document was found and assigned. + /// Optionally, can also assign the template or anything else on the document request, although that is not required. + bool TryFindDocument(PublishedContentRequest contentRequest); } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/ISiteDomainHelper.cs b/src/Umbraco.Web/Routing/ISiteDomainHelper.cs new file mode 100644 index 0000000000..b290c9b1c1 --- /dev/null +++ b/src/Umbraco.Web/Routing/ISiteDomainHelper.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides utilities to handle site domains. + /// + internal interface ISiteDomainHelper + { + /// + /// Filters a list of DomainAndUri to pick one that best matches the current request. + /// + /// The Uri of the current request. + /// The list of DomainAndUri to filter. + /// 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. + /// + DomainAndUri MapDomain(Uri current, DomainAndUri[] domainAndUris); + + /// + /// Filters a list of DomainAndUri to pick those that best matches the current request. + /// + /// The Uri of the current request. + /// The list of DomainAndUri to filter. + /// A value indicating whether to exclude the current/default domain. + /// The selected DomainAndUri items. + /// The filter must return something, even empty, else an exception will be thrown. + IEnumerable MapDomains(Uri current, DomainAndUri[] domainAndUris, bool excludeDefault); + } +} diff --git a/src/Umbraco.Web/Routing/IUrlProvider.cs b/src/Umbraco.Web/Routing/IUrlProvider.cs new file mode 100644 index 0000000000..2a14b56cbb --- /dev/null +++ b/src/Umbraco.Web/Routing/IUrlProvider.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +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/NotFoundHandlerHelper.cs b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs index 46c6cef456..4287b215d3 100644 --- a/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs +++ b/src/Umbraco.Web/Routing/NotFoundHandlerHelper.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Web; using System.Xml; using System.Reflection; - -using Umbraco.Core; using Umbraco.Core.Logging; namespace Umbraco.Web.Routing @@ -47,7 +43,7 @@ namespace Umbraco.Web.Routing // auth. Paul Sterling confirmed in jan. 2013 that we can get rid of it. // code from requestHandler.cleanUrl - string root = Umbraco.Core.IO.SystemDirectories.Root.ToLower(); + string root = Core.IO.SystemDirectories.Root.ToLower(); if (!string.IsNullOrEmpty(root) && tmp.StartsWith(root)) tmp = tmp.Substring(root.Length); tmp = tmp.TrimEnd('/'); @@ -81,7 +77,7 @@ namespace Umbraco.Web.Routing return tmp; } - static IEnumerable _customHandlerTypes = null; + static IEnumerable _customHandlerTypes; static void InitializeNotFoundHandlers() { @@ -93,7 +89,7 @@ namespace Umbraco.Web.Routing var customHandlerTypes = new List(); var customHandlers = new XmlDocument(); - customHandlers.Load(Umbraco.Core.IO.IOHelper.MapPath(Umbraco.Core.IO.SystemFiles.NotFoundhandlersConfig)); + customHandlers.Load(Core.IO.IOHelper.MapPath(Core.IO.SystemFiles.NotFoundhandlersConfig)); foreach (XmlNode n in customHandlers.DocumentElement.SelectNodes("notFound")) { diff --git a/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs b/src/Umbraco.Web/Routing/PublishedContentRequestEngine.cs index af7336b578..4e16f57b40 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.DomainForUri(DomainHelper.GetAllDomains(false), _pcr.Uri); // handle domain if (domainAndUri != null) @@ -216,7 +216,7 @@ namespace Umbraco.Web.Routing var nodePath = _pcr.PublishedContent.Path; LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => nodePath); var rootNodeId = _pcr.HasDomain ? _pcr.Domain.RootNodeId : (int?)null; - var domain = DomainHelper.LookForWildcardDomain(Domain.GetDomains().ToArray(), nodePath, rootNodeId); + var domain = DomainHelper.FindWildcardDomainInPath(DomainHelper.GetAllDomains(true), nodePath, rootNodeId); if (domain != null) { @@ -270,24 +270,22 @@ namespace Umbraco.Web.Routing // recurse var subdir = directory.GetDirectories(alias.Substring(0, pos)).FirstOrDefault(); alias = alias.Substring(pos + 1); - return subdir == null ? false : FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); - } - else - { - // look here - return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); + return subdir != null && FindTemplateRenderingEngineInDirectory(subdir, alias, extensions); } + + // look here + return directory.GetFiles().Any(f => extensions.Any(e => f.Name.InvariantEquals(alias + e))); } #endregion #region Document and template - /// - /// Finds the Umbraco document (if any) matching the request, and updates the PublishedContentRequest accordingly. - /// - /// A value indicating whether a document and template were found. - private bool FindPublishedContentAndTemplate() + /// + /// Finds the Umbraco document (if any) matching the request, and updates the PublishedContentRequest accordingly. + /// + /// A value indicating whether a document and template were found. + private void FindPublishedContentAndTemplate() { const string tracePrefix = "FindPublishedContentAndTemplate: "; LogHelper.Debug("{0}Path=\"{1}\"", () => tracePrefix, () => _pcr.Uri.AbsolutePath); @@ -299,7 +297,7 @@ namespace Umbraco.Web.Routing // whoever called us is in charge of actually redirecting // -- do not process anything any further -- if (_pcr.IsRedirect) - return true; + return; // not handling umbracoRedirect here but after LookupDocument2 // so internal redirect, 404, etc has precedence over redirect @@ -315,14 +313,13 @@ namespace Umbraco.Web.Routing // handle wildcard domains HandleWildcardDomains(); - - return _pcr.HasPublishedContent && _pcr.HasTemplate; } - /// - /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. - /// - internal void FindPublishedContent() + /// + /// Tries to find the document matching the request, by running the IPublishedContentFinder instances. + /// + /// There is no finder collection. + internal void FindPublishedContent() { const string tracePrefix = "FindPublishedContent: "; @@ -334,10 +331,12 @@ namespace Umbraco.Web.Routing () => string.Format("{0}Begin finders", tracePrefix), () => string.Format("{0}End finders, {1}", tracePrefix, (_pcr.HasPublishedContent ? "a document was found" : "no document was found")))) { - _routingContext.PublishedContentFinders.Any(lookup => lookup.TryFindDocument(_pcr)); + if (_routingContext.PublishedContentFinders == null) + throw new InvalidOperationException("There is no finder collection."); + _routingContext.PublishedContentFinders.Any(finder => finder.TryFindDocument(_pcr)); } - // indicate that the published content (if any) we have at the moment is the + // indicate that the published content (if any) we have at the moment is the // one that was found by the standard finders before anything else took place. _pcr.SetIsInitialPublishedContent(); } @@ -622,7 +621,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/RoutesCacheResolver.cs b/src/Umbraco.Web/Routing/RoutesCacheResolver.cs index a472f34783..f4b98fafb6 100644 --- a/src/Umbraco.Web/Routing/RoutesCacheResolver.cs +++ b/src/Umbraco.Web/Routing/RoutesCacheResolver.cs @@ -32,7 +32,7 @@ namespace Umbraco.Web.Routing /// public IRoutesCache RoutesCache { - get { return this.Value; } + get { return Value; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/RoutingContext.cs b/src/Umbraco.Web/Routing/RoutingContext.cs index 377e909f8e..af6eff62b5 100644 --- a/src/Umbraco.Web/Routing/RoutingContext.cs +++ b/src/Umbraco.Web/Routing/RoutingContext.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace Umbraco.Web.Routing { @@ -9,28 +8,29 @@ namespace Umbraco.Web.Routing /// public class RoutingContext { - /// - /// Initializes a new instance of the class. - /// - /// - /// The document lookups resolver. - /// - /// The content store. - /// The nice urls resolver. - internal RoutingContext( + /// + /// Initializes a new instance of the class. + /// + /// + /// The document lookups resolver. + /// + /// The content store. + /// The nice urls provider. + /// The routes cache. + 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.RoutesCache = routesCache; + UmbracoContext = umbracoContext; + PublishedContentFinders = contentFinders; + PublishedContentLastChanceFinder = contentLastChanceFinder; + PublishedContentStore = publishedContentStore; + UrlProvider = urlProvider; + 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/SiteDomainHelper.cs b/src/Umbraco.Web/Routing/SiteDomainHelper.cs new file mode 100644 index 0000000000..ea035fa061 --- /dev/null +++ b/src/Umbraco.Web/Routing/SiteDomainHelper.cs @@ -0,0 +1,336 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Text.RegularExpressions; +using Umbraco.Core; +using umbraco.cms.businesslogic.web; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides utilities to handle site domains. + /// + internal class SiteDomainHelper : ISiteDomainHelper + { + #region Configure + + private static readonly ReaderWriterLockSlim ConfigLock = new ReaderWriterLockSlim(); + private static Dictionary _sites; + private static Dictionary> _bindings; + private static Dictionary> _qualifiedSites; + + // these are for unit tests *only* + internal static Dictionary Sites { get { return _sites; } } + internal static Dictionary> Bindings { get { return _bindings; } } + + // these are for validation + //private const string DomainValidationSource = @"^(\*|((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/[-\w]*)?))$"; + private const string DomainValidationSource = @"^(((?i:http[s]?://)?([-\w]+(\.[-\w]+)*)(:\d+)?(/)?))$"; + private static readonly Regex DomainValidation = new Regex(DomainValidationSource, RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Returns a disposable object that represents safe write access to config. + /// + /// Should be used in a using(SiteDomainHelper.ConfigWriteLock) { ... } mode. + protected static IDisposable ConfigWriteLock + { + get { return new WriteLock(ConfigLock); } + } + + /// + /// Returns a disposable object that represents safe read access to config. + /// + /// Should be used in a using(SiteDomainHelper.ConfigWriteLock) { ... } mode. + protected static IDisposable ConfigReadLock + { + get { return new ReadLock(ConfigLock); } + } + + /// + /// Clears the entire configuration. + /// + public static void Clear() + { + using (ConfigWriteLock) + { + _sites = null; + _bindings = null; + _qualifiedSites = null; + } + } + + private static IEnumerable ValidateDomains(IEnumerable domains) + { + // must use authority format w/optional scheme and port, but no path + // any domain should appear only once + return domains.Select(domain => + { + if (!DomainValidation.IsMatch(domain)) + throw new ArgumentOutOfRangeException("domains", string.Format("Invalid domain: \"{0}\"", domain)); + return domain; + }); + } + + /// + /// Adds a site. + /// + /// A key uniquely identifying the site. + /// The site domains. + /// At the moment there is no public way to remove a site. Clear and reconfigure. + public static void AddSite(string key, IEnumerable domains) + { + using (ConfigWriteLock) + { + _sites = _sites ?? new Dictionary(); + _sites[key] = ValidateDomains(domains).ToArray(); + _qualifiedSites = null; + } + } + + /// + /// Adds a site. + /// + /// A key uniquely identifying the site. + /// The site domains. + /// At the moment there is no public way to remove a site. Clear and reconfigure. + public static void AddSite(string key, params string[] domains) + { + using (ConfigWriteLock) + { + _sites = _sites ?? new Dictionary(); + _sites[key] = ValidateDomains(domains).ToArray(); + _qualifiedSites = null; + } + } + + /// + /// Removes a site. + /// + /// A key uniquely identifying the site. + internal static void RemoveSite(string key) + { + using (ConfigWriteLock) + { + if (_sites != null && _sites.ContainsKey(key)) + { + _sites.Remove(key); + if (_sites.Count == 0) + _sites = null; + + if (_bindings != null && _bindings.ContainsKey(key)) + { + foreach (var b in _bindings[key]) + { + _bindings[b].Remove(key); + if (_bindings[b].Count == 0) + _bindings.Remove(b); + } + _bindings.Remove(key); + if (_bindings.Count > 0) + _bindings = null; + } + + _qualifiedSites = null; + } + } + } + + /// + /// Binds some sites. + /// + /// The keys uniquely identifying the sites to bind. + /// + /// At the moment there is no public way to unbind sites. Clear and reconfigure. + /// If site1 is bound to site2 and site2 is bound to site3 then site1 is bound to site3. + /// + public static void BindSites(params string[] keys) + { + using (ConfigWriteLock) + { + foreach (var key in keys.Where(key => !_sites.ContainsKey(key))) + throw new ArgumentException(string.Format("Not an existing site key: {0}", key), "keys"); + + _bindings = _bindings ?? new Dictionary>(); + + var allkeys = _bindings + .Where(kvp => keys.Contains(kvp.Key)) + .SelectMany(kvp => kvp.Value) + .Union(keys) + .ToArray(); + + foreach (var key in allkeys) + { + if (!_bindings.ContainsKey(key)) + _bindings[key] = new List(); + var xkey = key; + var addKeys = allkeys.Where(k => k != xkey).Except(_bindings[key]); + _bindings[key].AddRange(addKeys); + } + } + } + + #endregion + + #region Map domains + + /// + /// Filters a list of DomainAndUri to pick one that best matches the current request. + /// + /// The Uri of the current request. + /// The list of DomainAndUri to filter. + /// 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. + /// + public virtual DomainAndUri MapDomain(Uri current, DomainAndUri[] domainAndUris) + { + var currentAuthority = current.GetLeftPart(UriPartial.Authority); + var qualifiedSites = GetQualifiedSites(current); + + return MapDomain(domainAndUris, qualifiedSites, currentAuthority); + } + + /// + /// Filters a list of DomainAndUri to pick those that best matches the current request. + /// + /// The Uri of the current request. + /// The list of DomainAndUri to filter. + /// A value indicating whether to exclude the current/default domain. + /// The selected DomainAndUri items. + /// The filter must return something, even empty, else an exception will be thrown. + public virtual IEnumerable MapDomains(Uri current, DomainAndUri[] domainAndUris, bool excludeDefault) + { + var currentAuthority = current.GetLeftPart(UriPartial.Authority); + KeyValuePair[] candidateSites = null; + IEnumerable ret = domainAndUris; + + using (ConfigReadLock) // so nothing changes between GetQualifiedSites and access to bindings + { + var qualifiedSites = GetQualifiedSitesInsideLock(current); + + if (excludeDefault) + { + // exclude the current one (avoid producing the absolute equivalent of what GetUrl returns) + var hintWithSlash = current.EndPathWithSlash(); + var hinted = domainAndUris.FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(hintWithSlash)); + if (hinted != null) + ret = ret.Where(d => d != hinted); + + // exclude the default one (avoid producing a possible duplicate of what GetUrl returns) + // only if the default one cannot be the current one ie if hinted is not null + if (hinted == null && domainAndUris.Any()) + { + // it is illegal to call MapDomain if domainAndUris is empty + // also, domainAndUris should NOT contain current, hence the test on hinted + var mainDomain = MapDomain(domainAndUris, qualifiedSites, currentAuthority); // what GetUrl would get + ret = ret.Where(d => d != mainDomain); + } + } + + // we do our best, but can't do the impossible + if (qualifiedSites == null) + return ret; + + // find a site that contains the current authority + var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority)); + + // if current belongs to a site, pick every element from domainAndUris that also belong + // to that site -- or to any site bound to that site + + if (!currentSite.Equals(default(KeyValuePair))) + { + candidateSites = new[] { currentSite }; + if (_bindings != null && _bindings.ContainsKey(currentSite.Key)) + { + var boundSites = qualifiedSites.Where(site => _bindings[currentSite.Key].Contains(site.Key)); + candidateSites = candidateSites.Union(boundSites).ToArray(); + + // .ToArray ensures it is evaluated before the configuration lock is exited + } + } + } + + // if we are able to filter, then filter, else return the whole lot + return candidateSites == null ? ret : ret.Where(d => + { + var authority = d.Uri.GetLeftPart(UriPartial.Authority); + return candidateSites.Any(site => site.Value.Contains(authority)); + }); + } + + private static Dictionary GetQualifiedSites(Uri current) + { + using (ConfigReadLock) + { + return GetQualifiedSitesInsideLock(current); + } + } + + private static Dictionary GetQualifiedSitesInsideLock(Uri current) + { + // we do our best, but can't do the impossible + if (_sites == null) + return null; + + // cached? + if (_qualifiedSites != null && _qualifiedSites.ContainsKey(current.Scheme)) + return _qualifiedSites[current.Scheme]; + + _qualifiedSites = _qualifiedSites ?? new Dictionary>(); + + // convert sites into authority sites based upon current scheme + // because some domains in the sites might not have a scheme -- and cache + return _qualifiedSites[current.Scheme] = _sites + .ToDictionary( + kvp => kvp.Key, + kvp => kvp.Value.Select(d => new Uri(UriUtility.StartWithScheme(d, current.Scheme)).GetLeftPart(UriPartial.Authority)).ToArray() + ); + + // .ToDictionary will evaluate and create the dictionary immediately + // the new value is .ToArray so it will also be evaluated immediately + // therefore it is safe to return and exit the configuration lock + } + + private static DomainAndUri MapDomain(DomainAndUri[] domainAndUris, Dictionary qualifiedSites, string currentAuthority) + { + if (domainAndUris == null) + throw new ArgumentNullException("domainAndUris"); + if (!domainAndUris.Any()) + throw new ArgumentException("Cannot be empty.", "domainAndUris"); + + // we do our best, but can't do the impossible + if (qualifiedSites == null) + return domainAndUris.First(); + + // find a site that contains the current authority + var currentSite = qualifiedSites.FirstOrDefault(site => site.Value.Contains(currentAuthority)); + + // if current belongs to a site - try to pick the first element + // from domainAndUris that also belongs to that site + var ret = currentSite.Equals(default(KeyValuePair)) + ? null + : domainAndUris.FirstOrDefault(d => currentSite.Value.Contains(d.Uri.GetLeftPart(UriPartial.Authority))); + + // no match means that either current does not belong to a site, or the site it belongs to + // does not contain any of domainAndUris. Yet we have to return something. here, it becomes + // a bit arbitrary. + + // look through sites in order and pick the first domainAndUri that belongs to a site + ret = ret ?? qualifiedSites + .Where(site => site.Key != currentSite.Key) + .Select(site => domainAndUris.FirstOrDefault(domainAndUri => site.Value.Contains(domainAndUri.Uri.GetLeftPart(UriPartial.Authority)))) + .FirstOrDefault(domainAndUri => domainAndUri != null); + + // random, really + ret = ret ?? domainAndUris.First(); + + return ret; + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/SiteDomainHelperResolver.cs b/src/Umbraco.Web/Routing/SiteDomainHelperResolver.cs new file mode 100644 index 0000000000..9cb101b6b1 --- /dev/null +++ b/src/Umbraco.Web/Routing/SiteDomainHelperResolver.cs @@ -0,0 +1,38 @@ +using System; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Routing +{ + /// + /// Resolves the implementation. + /// + internal sealed class SiteDomainHelperResolver : SingleObjectResolverBase + { + + /// + /// Initializes a new instance of the class with an implementation. + /// + /// The implementation. + internal SiteDomainHelperResolver(ISiteDomainHelper helper) + : base(helper) + { } + + + /// + /// Can be used by developers at runtime to set their IDomainHelper at app startup + /// + /// + public void SetHelper(ISiteDomainHelper helper) + { + Value = helper; + } + + /// + /// Gets or sets the implementation. + /// + public ISiteDomainHelper Helper + { + get { return Value; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/UrlProvider.cs b/src/Umbraco.Web/Routing/UrlProvider.cs new file mode 100644 index 0000000000..6b9b5e9778 --- /dev/null +++ b/src/Umbraco.Web/Routing/UrlProvider.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides urls. + /// + internal class UrlProvider + { + #region Ctor and configuration + + /// + /// 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; } + + #endregion + + #region GetUrl + + /// + /// 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 + } + + #endregion + + #region GetOtherUrls + + /// + /// 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; + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/UrlProviderResolver.cs b/src/Umbraco.Web/Routing/UrlProviderResolver.cs new file mode 100644 index 0000000000..bdbcf96c55 --- /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 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..3ca5d86f87 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -326,6 +326,13 @@ + + AssignDomain2.aspx + ASPXCodeBehind + + + AssignDomain2.aspx + @@ -379,15 +386,24 @@ + + + + + + + + + @@ -569,7 +585,6 @@ - @@ -1763,6 +1778,7 @@ + @@ -1786,6 +1802,9 @@ + + ASPXCodeBehind + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 0e5ae7f1d5..ea6ea718bf 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -84,7 +84,11 @@ namespace Umbraco.Web var umbracoContext = new UmbracoContext(httpContext, applicationContext); // create the nice urls provider - var niceUrls = new NiceUrlProvider(PublishedContentStoreResolver.Current.PublishedContentStore, umbracoContext); + // there's one per request because there are some behavior parameters that can be changed + var urlProvider = new UrlProvider( + umbracoContext, + PublishedContentStoreResolver.Current.PublishedContentStore, + UrlProviderResolver.Current.Providers); // create the RoutingContext, and assign var routingContext = new RoutingContext( @@ -92,7 +96,7 @@ namespace Umbraco.Web ContentFinderResolver.Current.Finders, ContentLastChanceFinderResolver.Current.Finder, PublishedContentStoreResolver.Current.PublishedContentStore, - niceUrls, + urlProvider, RoutesCacheResolver.Current.RoutesCache); //assign the routing context back @@ -270,13 +274,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/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 662bd9110e..dca6a898ce 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -1,14 +1,8 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; -using System.Threading; using System.Web; -using System.Web.Compilation; -using System.Web.Mvc; using System.Web.Routing; -using System.Web.UI; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Web.Routing; @@ -205,7 +199,7 @@ namespace Umbraco.Web { var uri = context.OriginalRequestUrl; - var reason = EnsureRoutableOutcome.IsRoutable;; + var reason = EnsureRoutableOutcome.IsRoutable; // ensure this is a document request if (!EnsureDocumentRequest(httpContext, uri)) @@ -244,9 +238,9 @@ namespace Umbraco.Web // handle directory-urls used for asmx // legacy - what's the point really? - if (maybeDoc && GlobalSettings.UseDirectoryUrls) + if (/*maybeDoc &&*/ GlobalSettings.UseDirectoryUrls) { - int asmxPos = lpath.IndexOf(".asmx/"); + int asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase); if (asmxPos >= 0) { // use uri.AbsolutePath, not path, 'cos path has been lowercased @@ -320,41 +314,36 @@ namespace Umbraco.Web // ensures Umbraco has at least one published node // if not, rewrites to splash and return false // if yes, return true - bool EnsureHasContent(UmbracoContext context, HttpContextBase httpContext) + private static bool EnsureHasContent(UmbracoContext context, HttpContextBase httpContext) { var store = context.RoutingContext.PublishedContentStore; - if (!store.HasContent(context)) - { - LogHelper.Warn("Umbraco has no content"); + if (store.HasContent(context)) + return true; - httpContext.Response.StatusCode = 503; + LogHelper.Warn("Umbraco has no content"); - var noContentUrl = "~/config/splashes/noNodes.aspx"; - httpContext.RewritePath(UriUtility.ToAbsolute(noContentUrl)); + httpContext.Response.StatusCode = 503; - return false; - } - else - { - return true; - } + const string noContentUrl = "~/config/splashes/noNodes.aspx"; + httpContext.RewritePath(UriUtility.ToAbsolute(noContentUrl)); + + return false; } // ensures Umbraco is configured // if not, redirect to install and return false // if yes, return true - bool EnsureIsConfigured(HttpContextBase httpContext, Uri uri) - { - if (!ApplicationContext.Current.IsConfigured) - { - LogHelper.Warn("Umbraco is not configured"); + private static bool EnsureIsConfigured(HttpContextBase httpContext, Uri uri) + { + if (ApplicationContext.Current.IsConfigured) + return true; - string installPath = UriUtility.ToAbsolute(SystemDirectories.Install); - string installUrl = string.Format("{0}/default.aspx?redir=true&url={1}", installPath, HttpUtility.UrlEncode(uri.ToString())); - httpContext.Response.Redirect(installUrl, true); - return false; - } - return true; + LogHelper.Warn("Umbraco is not configured"); + + var installPath = UriUtility.ToAbsolute(Core.IO.SystemDirectories.Install); + var installUrl = string.Format("{0}/default.aspx?redir=true&url={1}", installPath, HttpUtility.UrlEncode(uri.ToString())); + httpContext.Response.Redirect(installUrl, true); + return false; } #endregion @@ -433,7 +422,7 @@ namespace Umbraco.Web BeginRequest(new HttpContextWrapper(httpContext)); }; - app.PostResolveRequestCache += (sender, e) => + app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; ProcessRequest(new HttpContextWrapper(httpContext)); diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 412298493a..48a836bc7c 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), // not enabled by default + 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,9 @@ namespace Umbraco.Web //typeof (ContentFinderByProfile), //typeof (ContentFinderByUrlAlias), typeof (ContentFinderByNotFoundHandlers) - }); + ); + + SiteDomainHelperResolver.Current = new SiteDomainHelperResolver(new SiteDomainHelper()); RoutesCacheResolver.Current = new RoutesCacheResolver(new DefaultRoutesCache(_isForTesting == false)); diff --git a/src/Umbraco.Web/WebServices/DomainsApiController.cs b/src/Umbraco.Web/WebServices/DomainsApiController.cs new file mode 100644 index 0000000000..fd53e7ad95 --- /dev/null +++ b/src/Umbraco.Web/WebServices/DomainsApiController.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using Umbraco.Core; +using Umbraco.Web.WebApi; +//using umbraco.cms.businesslogic.language; +using umbraco.cms.businesslogic.web; + +namespace Umbraco.Web.WebServices +{ + /// + /// A REST controller used for managing domains. + /// + /// Nothing to do with Active Directory. + public class DomainsApiController : UmbracoAuthorizedApiController + { + [HttpPost] + // can't pass multiple complex args in json post request... + public PostBackModel SaveLanguageAndDomains(PostBackModel model) + { + var node = ApplicationContext.Current.Services.ContentService.GetById(model.NodeId); + + if (node == null) + throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest) + { + Content = new StringContent(string.Format("There is no content node with id {0}.", model.NodeId)), + ReasonPhrase = "Node Not Found." + }); + + if (!UmbracoUser.GetPermissions(node.Path).Contains(global::umbraco.BusinessLogic.Actions.ActionAssignDomain.Instance.Letter)) + throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.Unauthorized) + { + Content = new StringContent("You do not have permission to assign domains on that node."), + ReasonPhrase = "Permission Denied." + }); + + model.Valid = true; + var domains = Routing.DomainHelper.GetNodeDomains(model.NodeId, true); + var languages = global::umbraco.cms.businesslogic.language.Language.GetAllAsList().ToArray(); + var language = model.Language > 0 ? languages.FirstOrDefault(l => l.id == model.Language) : null; + + // process wildcard + + if (language != null) + { + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + wildcard.Language = language; + else // yet there is a race condition here... + Domain.MakeNew("*" + model.NodeId, model.NodeId, model.Language); + } + else + { + var wildcard = domains.FirstOrDefault(d => d.IsWildcard); + if (wildcard != null) + wildcard.Delete(); + } + + // process domains + + foreach (var domain in domains.Where(d => model.Domains.All(m => !m.Name.Equals(d.Name, StringComparison.OrdinalIgnoreCase)))) + domain.Delete(); + + var names = new List(); + + foreach (var domainModel in model.Domains.Where(m => !string.IsNullOrWhiteSpace(m.Name))) + { + language = languages.FirstOrDefault(l => l.id == domainModel.Lang); + if (language == null) + continue; + var name = domainModel.Name.ToLowerInvariant(); + if (names.Contains(name)) + { + domainModel.Duplicate = true; + continue; + } + names.Add(name); + var domain = domains.FirstOrDefault(d => d.Name.Equals(domainModel.Name, StringComparison.OrdinalIgnoreCase)); + if (domain != null) + domain.Language = language; + else if (Domain.Exists(domainModel.Name)) + domainModel.Duplicate = true; + else // yet there is a race condition here... + Domain.MakeNew(name, model.NodeId, domainModel.Lang); + } + + model.Valid = model.Domains.All(m => !m.Duplicate); + + return model; + } + + #region Models + + public class PostBackModel + { + public bool Valid { get; set; } + public int NodeId { get; set; } + public int Language { get; set; } + public DomainModel[] Domains { get; set; } + } + + public class DomainModel + { + public DomainModel(string name, int lang) + { + Name = name; + Lang = lang; + } + + public string Name { get; private set; } + public int Lang { get; private set; } + public bool Duplicate { get; set; } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx new file mode 100644 index 0000000000..2fd4e3bdaa --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx @@ -0,0 +1,77 @@ +<%@ Page Language="c#" MasterPageFile="../masterpages/umbracoDialog.Master" Codebehind="AssignDomain2.aspx.cs" AutoEventWireup="True" Inherits="umbraco.dialogs.AssignDomain2" %> +<%@ Import Namespace="Umbraco.Web" %> +<%@ Register TagPrefix="umb" Namespace="ClientDependency.Core.Controls" Assembly="ClientDependency.Core" %> +<%@ Register TagPrefix="cc1" Namespace="umbraco.uicontrols" Assembly="controls" %> + + + + + + + + + + + + +
+
+ + + +
<%=umbraco.ui.Text("assignDomain", "setLanguageHelp") %> +
+
+ + + + + + + + + + + + + + + + + +
<%=umbraco.ui.Text("assignDomain", "domain") %><%=umbraco.ui.Text("assignDomain", "language") %> +
<%=umbraco.ui.Text("assignDomain", "remove") %>
+ + + + + +
<%=umbraco.ui.Text("assignDomain", "domainHelp") %>
+
+
+ +

+ + + <%=umbraco.ui.Text("general", "or")%> + + <%=umbraco.ui.Text("general", "cancel")%> +

+ +
+
\ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs new file mode 100644 index 0000000000..600078a78d --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.UI.Pages; +using Umbraco.Web; +using Umbraco.Web.Routing; +using Umbraco.Web.WebServices; + + +namespace umbraco.dialogs +{ + public partial class AssignDomain2 : UmbracoEnsuredPage + { + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + var nodeId = GetNodeId(); + var node = ApplicationContext.Current.Services.ContentService.GetById(nodeId); + + if (node == null) + { + feedback.Text = ui.Text("assignDomain", "invalidNode"); + pane_language.Visible = false; + pane_domains.Visible = false; + phSave.Visible = false; + return; + } + + if (!UmbracoUser.GetPermissions(node.Path).Contains(BusinessLogic.Actions.ActionAssignDomain.Instance.Letter)) + { + feedback.Text = ui.Text("assignDomain", "permissionDenied"); + pane_language.Visible = false; + pane_domains.Visible = false; + phSave.Visible = false; + return; + } + + pane_language.Text = ui.Text("assignDomain", "setLanguage"); + pane_domains.Text = ui.Text("assignDomain", "setDomains"); + prop_language.Text = ui.Text("assignDomain", "language"); + + var nodeDomains = DomainHelper.GetNodeDomains(nodeId, true); + var wildcard = nodeDomains.FirstOrDefault(d => d.IsWildcard); + + var sb = new StringBuilder(); + sb.Append("languages: ["); + var i = 0; + foreach (var language in ApplicationContext.Current.Services.LocalizationService.GetAllLanguages()) + sb.AppendFormat("{0}{{ \"Id\": {1}, \"Code\": \"{2}\" }}", (i++ == 0 ? "" : ","), language.Id, language.IsoCode); + sb.Append("]\r\n"); + + sb.AppendFormat(",language: {0}", wildcard == null ? "undefined" : wildcard.Language.id.ToString()); + + sb.Append(",domains: ["); + i = 0; + foreach (var domain in nodeDomains.Where(d => !d.IsWildcard)) + sb.AppendFormat("{0}{{ \"Name\": \"{1}\", \"Lang\": \"{2}\" }}", (i++ == 0 ? "" :","), domain.Name, domain.Language.id); + sb.Append("]\r\n"); + + data.Text = sb.ToString(); + } + + protected int GetNodeId() + { + int nodeId; + if (!int.TryParse(Request.QueryString["id"], out nodeId)) + nodeId = -1; + return nodeId; + } + + protected string GetRestServicePath() + { + const string action = "ListDomains"; + var path = Url.GetUmbracoApiService(action); + return path.TrimEnd(action).EnsureEndsWith('/'); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.designer.cs new file mode 100644 index 0000000000..ba6d9aa58c --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/AssignDomain2.aspx.designer.cs @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace umbraco.dialogs { + + + public partial class AssignDomain2 { + + /// + /// data control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.Literal data; + + /// + /// feedback control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Feedback feedback; + + /// + /// pane_language control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pane_language; + + /// + /// prop_language control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.PropertyPanel prop_language; + + /// + /// pane_domains control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::umbraco.uicontrols.Pane pane_domains; + + /// + /// phSave control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.PlaceHolder phSave; + } +} 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()); diff --git a/src/umbraco.cms/businesslogic/web/Domain.cs b/src/umbraco.cms/businesslogic/web/Domain.cs index 858bf65b99..db59dc3d1e 100644 --- a/src/umbraco.cms/businesslogic/web/Domain.cs +++ b/src/umbraco.cms/businesslogic/web/Domain.cs @@ -147,7 +147,12 @@ namespace umbraco.cms.businesslogic.web internal static List GetDomains() { - return Cache.GetCacheItem>("UmbracoDomainList", getDomainsSyncLock, TimeSpan.FromMinutes(30), + return GetDomains(false); + } + + internal static List GetDomains(bool includeWildcards) + { + var domains = Cache.GetCacheItem>("UmbracoDomainList", getDomainsSyncLock, TimeSpan.FromMinutes(30), delegate { List result = new List(); @@ -170,6 +175,11 @@ namespace umbraco.cms.businesslogic.web } return result; }); + + if (!includeWildcards) + domains = domains.Where(d => !d.IsWildcard).ToList(); + + return domains; } public static Domain GetDomain(string DomainName) @@ -259,5 +269,23 @@ namespace umbraco.cms.businesslogic.web if (AfterDelete != null) AfterDelete(this, e); } + + #region Pipeline Refactoring + + // NOTE: the wildcard name thing should be managed by the Domain class + // internally but that would break too much backward compatibility, so + // we don't do it now. Will do it when the Domain class migrates to the + // new Core.Models API. + + /// + /// Gets a value indicating whether the domain is a wildcard domain. + /// + /// A value indicating whether the domain is a wildcard domain. + public bool IsWildcard + { + get { return string.IsNullOrWhiteSpace(Name) || Name.StartsWith("*"); } + } + + #endregion } } \ No newline at end of file