using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Umbraco.Core; using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Routing { /// /// Provides utilities to handle domains. /// public class DomainHelper { #region Temp. abstract Umbraco's API /// /// 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 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(); } #endregion #region Domain for Node /// /// 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; // get the domains on that node var domains = GetNodeDomains(nodeId, false); // none? if (!domains.Any()) return null; // else filter var helper = SiteDomainHelperResolver.Current.Helper; var domainAndUri = DomainForUri(domains, current, domainAndUris => helper.MapDomain(current, domainAndUris)); if (domainAndUri == null) throw new Exception("DomainForUri returned null."); return domainAndUri; } /// /// Gets a value indicating whether a specified node has domains. /// /// The node identifier. /// True if the node has domains, else false. internal static bool NodeHasDomains(int nodeId) { return nodeId > 0 && GetNodeDomains(nodeId, false).Any(); } /// /// 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; // get the domains on that node var domains = GetNodeDomains(nodeId, false); // none? if (!domains.Any()) return null; // 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 current url // ie current is www.example.com/foo/bar, look for domain www.example.com var currentWithSlash = current.EndPathWithSlash(); domainAndUri = domainsAndUris .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash)); if (domainAndUri != null) return domainAndUri; // if none matches, then try to run the filter to pick a domain if (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. public static string PathRelativeToDomain(Uri domainUri, string path) { return path.Substring(domainUri.AbsolutePath.Length).EnsureStartsWith('/'); } #endregion } }