using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; using Umbraco.Web.PublishedCache; // published snapshot namespace Umbraco.Web.Routing { /// /// Provides utilities to handle domains. /// public class DomainHelper { private readonly IDomainCache _domainCache; private readonly ISiteDomainHelper _siteDomainHelper; public DomainHelper(IDomainCache domainCache, ISiteDomainHelper siteDomainHelper) { _domainCache = domainCache; _siteDomainHelper = siteDomainHelper; } #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 culture, or null. /// The domain and its uri, if any, that best matches the specified uri and culture, else null. /// /// If at least a domain is set on the node then the method returns the domain that /// best matches the specified uri and culture, else it returns null. /// If culture is null, uses the default culture for the installation instead. Otherwise, /// will try with the specified culture, else return null. /// internal DomainAndUri DomainForNode(int nodeId, Uri current, string culture = null) { // 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 // it could be that none apply (due to culture) return SelectDomain(domains, current, culture, _domainCache.DefaultCulture, (cdomainAndUris, ccurrent, cculture, cdefaultCulture) => _siteDomainHelper.MapDomain(cdomainAndUris, ccurrent, cculture, cdefaultCulture)); } /// /// 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 = SelectDomains(domains, current).ToArray(); // filter return _siteDomainHelper.MapDomains(domainAndUris, current, excludeDefault, null, _domainCache.DefaultCulture).ToArray(); } #endregion #region Selects Domain(s) /// /// Selects the domain that best matches a specified uri and cultures, from a set of domains. /// /// The group of domains. /// An optional uri. /// An optional culture. /// An optional default culture. /// An optional function to filter the list of domains, if more than one applies. /// The domain and its normalized uri, that best matches the specified uri and cultures. /// /// fixme - must document and explain this all /// If is null, pick the first domain that matches , /// else the first that matches , else the first one (ordered by id), else null. /// If is not null, look for domains that would be a base uri of the current uri, /// If more than one domain matches, then the function is used to pick /// the right one, unless it is null, in which case the method returns null. /// The filter, if any, will be called only with a non-empty argument, and _must_ return something. /// internal static DomainAndUri SelectDomain(IEnumerable domains, Uri uri, string culture = null, string defaultCulture = null, Func, Uri, string, string, DomainAndUri> 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(d => new DomainAndUri(d, uri)) .OrderByDescending(d => d.Uri.ToString()) .ToList(); // nothing = no magic, return null if (domainsAndUris.Count == 0) return null; // sanitize cultures culture = culture.NullOrWhiteSpaceAsNull(); defaultCulture = defaultCulture.NullOrWhiteSpaceAsNull(); if (uri == null) { // no uri - will only rely on culture return GetByCulture(domainsAndUris, culture, defaultCulture); } // else we have a uri, // try to match that uri, else filter // if a culture is specified, then try to get domains for that culture // (else cultureDomains will be null) // do NOT specify a default culture, else it would pick those domains var cultureDomains = SelectByCulture(domainsAndUris, culture, defaultCulture: null); IReadOnlyCollection considerForBaseDomains = domainsAndUris; if (cultureDomains != null) { if (cultureDomains.Count == 1) // only 1, return return cultureDomains.First(); // else restrict to those domains, for base lookup considerForBaseDomains = cultureDomains; } // look for domains that would be the base of the uri var baseDomains = SelectByBase(considerForBaseDomains, uri); if (baseDomains.Count > 0) // found, return return baseDomains.First(); // if nothing works, then try to run the filter to select a domain // either restricting on cultureDomains, or on all domains if (filter != null) { var domainAndUri = filter(cultureDomains ?? domainsAndUris, uri, culture, defaultCulture); // 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; } return null; } private static bool IsBaseOf(DomainAndUri domain, Uri uri) => domain.Uri.EndPathWithSlash().IsBaseOf(uri); private static IReadOnlyCollection SelectByBase(IReadOnlyCollection domainsAndUris, Uri uri) { // look for domains that would be the base of the uri // ie current is www.example.com/foo/bar, look for domain www.example.com var currentWithSlash = uri.EndPathWithSlash(); var baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithSlash)).ToList(); // if none matches, try again without the port // ie current is www.example.com:1234/foo/bar, look for domain www.example.com var currentWithoutPort = currentWithSlash.WithoutPort(); if (baseDomains.Count == 0) baseDomains = domainsAndUris.Where(d => IsBaseOf(d, currentWithoutPort)).ToList(); return baseDomains; } private static IReadOnlyCollection SelectByCulture(IReadOnlyCollection domainsAndUris, string culture, string defaultCulture) { // we try our best to match cultures, but may end with a bogus domain if (culture != null) // try the supplied culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.Name.InvariantEquals(culture)).ToList(); if (cultureDomains.Count > 0) return cultureDomains; } if (defaultCulture != null) // try the defaultCulture culture { var cultureDomains = domainsAndUris.Where(x => x.Culture.Name.InvariantEquals(defaultCulture)).ToList(); if (cultureDomains.Count > 0) return cultureDomains; } return null; } private static DomainAndUri GetByCulture(IReadOnlyCollection domainsAndUris, string culture, string defaultCulture) { DomainAndUri domainAndUri; // we try our best to match cultures, but may end with a bogus domain if (culture != null) // try the supplied culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.Name.InvariantEquals(culture)); if (domainAndUri != null) return domainAndUri; } if (defaultCulture != null) // try the defaultCulture culture { domainAndUri = domainsAndUris.FirstOrDefault(x => x.Culture.Name.InvariantEquals(defaultCulture)); if (domainAndUri != null) return domainAndUri; } return domainsAndUris.First(); // what else? } /// /// Selects the domains that match a specified uri, from a set of domains. /// /// The domains. /// The uri, or null. /// The domains and their normalized uris, that match the specified uri. internal static IEnumerable SelectDomains(IEnumerable domains, Uri uri) { // fixme where are we matching ?!!? return domains .Where(d => d.IsWildcard == false) .Select(d => new DomainAndUri(d, uri)) .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 } }