using System; using System.Collections.Generic; using System.Linq; using System.Text; using Umbraco.Core; using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Routing { /// /// 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; /// /// The normalized uri of the domain. /// public Uri Uri; /// /// Gets a string that represents the instance. /// /// A string that represents the current instance. public override string ToString() { return string.Format("{{ \"{0}\", \"{1}\" }}", Domain.Name, Uri); } } private static bool IsWildcardDomain(Domain d) { // supporting null or whitespace for backward compatibility, // although we should not allow ppl to create them anymore return string.IsNullOrWhiteSpace(d.Name) || d.Name.StartsWith("*"); } 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; } /// /// 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) }); if (!domainsAndUris.Any()) return null; DomainAndUri domainAndUri; if (current == null) { // take the first one by default domainAndUri = domainsAndUris.First(); } else { // look for a domain that would be the base of the hint // else take the first one by default var hintWithSlash = current.EndPathWithSlash(); domainAndUri = domainsAndUris .FirstOrDefault(t => t.Uri.IsBaseOf(hintWithSlash)); if (domainAndUri == null && defaultToFirst) domainAndUri = domainsAndUris.First(); } if (domainAndUri != null) domainAndUri.Uri = domainAndUri.Uri.TrimPathEndSlash(); return domainAndUri; } /// /// 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; } /// /// 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 path.Split(',') .Reverse() .Select(id => int.Parse(id)) .TakeWhile(id => id != stopNodeId) .Any(id => domains.Any(d => d.RootNodeId == id && !IsWildcardDomain(d))); } /// /// 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." 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); } /// /// 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('/'); } } }