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
}
}