using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Web.PublishedCache; // Facade namespace Umbraco.Web.Routing { /// /// Provides utilities to handle domains. /// public class DomainHelper { private readonly IDomainCache _domainCache; public DomainHelper(IDomainCache domainCache) { _domainCache = domainCache; } #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 DomainAndUri DomainForNode(int nodeId, Uri current) { // be safe if (nodeId <= 0) return null; // get the domains on that node var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); // none? if (domains.Length == 0) return null; // else filter var helper = Current.SiteDomainHelper; 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 bool NodeHasDomains(int nodeId) { return nodeId > 0 && _domainCache.GetAssigned(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 IEnumerable DomainsForNode(int nodeId, Uri current, bool excludeDefault = true) { // be safe if (nodeId <= 0) return null; // get the domains on that node var domains = _domainCache.GetAssigned(nodeId, false).ToArray(); // none? if (domains.Length == 0) return null; // get the domains and their uris var domainAndUris = DomainsForUri(domains, current).ToArray(); // filter var helper = Current.SiteDomainHelper; 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(IEnumerable 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 domainsAndUris = domains .Where(d => d.IsWildcard == false) //.Select(SanitizeForBackwardCompatibility) .Select(d => new DomainAndUri(d, current)) .OrderByDescending(d => d.Uri.ToString()) .ToArray(); if (domainsAndUris.Length == 0) 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, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com domainAndUri = domainsAndUris .FirstOrDefault(d => d.Uri.EndPathWithSlash().IsBaseOf(currentWithSlash.WithoutPort())); 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(IEnumerable domains, Uri current) { return domains .Where(d => d.IsWildcard == false) //.Select(SanitizeForBackwardCompatibility) .Select(d => new DomainAndUri(d, current)) .OrderByDescending(d => d.Uri.ToString()); } #endregion #region Utilities /// /// 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(IEnumerable 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(IEnumerable 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.ContentId == id && d.IsWildcard == false)) .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(IEnumerable 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.ContentId == 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.GetAbsolutePathDecoded().Length).EnsureStartsWith('/'); } #endregion } }