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