diff --git a/src/Umbraco.Core/Resolving/ManyWeightedResolved.cs b/src/Umbraco.Core/Resolving/ManyWeightedResolved.cs new file mode 100644 index 0000000000..c8d56b8c67 --- /dev/null +++ b/src/Umbraco.Core/Resolving/ManyWeightedResolved.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Resolving +{ + public class ManyWeightedResolved + { + List _resolved = new List(); + Dictionary _weights = new Dictionary(); + + public ManyWeightedResolved() + { + Resolution.Frozen += (sender, e) => + { + _resolved = _resolved.OrderBy(r => _weights[r.GetType()]).ToList(); + _weights = null; + }; + } + + public ManyWeightedResolved(IEnumerable resolved) + : this() + { + _resolved.AddRange(resolved); + foreach (var type in _resolved.Select(r => r.GetType())) + _weights.Add(type, ResolutionWeightAttribute.ReadWeight(type)); + } + + public IEnumerable Values + { + get { return _resolved; } + } + + #region Manage collection + + public void Add(TResolved value) + { + Resolution.EnsureNotFrozen(); + + var type = value.GetType(); + EnsureNotExists(type); + _resolved.Add(value); + _weights[type] = ResolutionWeightAttribute.ReadWeight(type); + } + + public void Add(TResolved value, int weight) + { + Resolution.EnsureNotFrozen(); + + var type = value.GetType(); + EnsureNotExists(type); + _resolved.Add(value); + _weights[type] = weight; + } + + public void AddRange(IEnumerable values) + { + Resolution.EnsureNotFrozen(); + + foreach (var value in values) + { + var type = value.GetType(); + EnsureNotExists(type); + _resolved.Add(value); + _weights[type] = ResolutionWeightAttribute.ReadWeight(type); + } + } + + //public void SetWeight(TResolved value, int weight) + //{ + // Resolution.EnsureNotFrozen(); + + // var type = value.GetType(); + // EnsureExists(type); + // _weights[type] = weight; + //} + + public void SetWeight(int weight) + { + Resolution.EnsureNotFrozen(); + + var type = typeof(TResolving); + EnsureExists(type); + _weights[type] = weight; + } + + //public int GetWeight(TResolved value) + //{ + // var type = value.GetType(); + // EnsureExists(type); + // return _weights[value.GetType()]; + //} + + public int GetWeight() + { + var type = typeof(TResolving); + EnsureExists(type); + return _weights[type]; + } + + //public void Remove(TResolved value) + //{ + // Resolution.EnsureNotFrozen(); + + // var type = value.GetType(); + // var remove = _resolved.SingleOrDefault(r => r.GetType() == type); + // if (remove != null) + // { + // _resolved.Remove(remove); + // _weights.Remove(remove.GetType()); + // } + //} + + public void Remove() + { + Resolution.EnsureNotFrozen(); + + var type = typeof(TResolving); + var remove = _resolved.SingleOrDefault(r => r.GetType() == type); + if (remove != null) + { + _resolved.Remove(remove); + _weights.Remove(remove.GetType()); + } + } + + public void Clear() + { + Resolution.EnsureNotFrozen(); + + _resolved = new List(); + _weights = new Dictionary(); + } + + #endregion + + #region Utilities + + public bool Exists(Type type) + { + return _resolved.Any(r => r.GetType() == type); + } + + void EnsureExists(Type type) + { + if (!Exists(type)) + throw new InvalidOperationException("There is not value of that type in the collection."); + } + + void EnsureNotExists(Type type) + { + if (Exists(type)) + throw new InvalidOperationException("A value of that type already exists in the collection."); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Resolving/ManyWeightedResolverBase.cs b/src/Umbraco.Core/Resolving/ManyWeightedResolverBase.cs new file mode 100644 index 0000000000..fb68db0d47 --- /dev/null +++ b/src/Umbraco.Core/Resolving/ManyWeightedResolverBase.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Resolving +{ + public abstract class ManyWeightedResolverBase : ResolverBase + { + ManyWeightedResolved _resolved; + + protected ManyWeightedResolverBase() + { + _resolved = new ManyWeightedResolved(); + } + + protected ManyWeightedResolverBase(IEnumerable values) + { + _resolved = new ManyWeightedResolved(values); + } + + protected IEnumerable Values + { + get { return _resolved.Values; } + } + + #region Manage collection + + public void Add(TResolved value) + { + _resolved.Add(value); + } + + public void Add(TResolved value, int weight) + { + _resolved.Add(value, weight); + } + + public void AddRange(IEnumerable values) + { + _resolved.AddRange(values); + } + + public void SetWeight(int weight) + { + _resolved.SetWeight(weight); + } + + public int GetWeight() + { + return _resolved.GetWeight(); + } + + public void Remove() + { + _resolved.Remove(); + } + + public void Clear() + { + _resolved.Clear(); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Resolving/Resolution.cs b/src/Umbraco.Core/Resolving/Resolution.cs new file mode 100644 index 0000000000..96349d645b --- /dev/null +++ b/src/Umbraco.Core/Resolving/Resolution.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Resolving +{ + // notes: nothing in Resolving is thread-safe because everything should happen when the app is starting + + public class Resolution + { + public static event EventHandler Freezing; + public static event EventHandler Frozen; + + public static bool IsFrozen { get; private set; } + + public static void EnsureNotFrozen() + { + if (Resolution.IsFrozen) + throw new InvalidOperationException("Resolution is frozen. It is not possible to modify resolvers once resolution is frozen."); + } + + public static void Freeze() + { + if (Resolution.IsFrozen) + throw new InvalidOperationException("Resolution is frozen. It is not possible to freeze it again."); + if (Freezing != null) + Freezing(null, null); + IsFrozen = true; + if (Frozen != null) + Frozen(null, null); + } + } +} diff --git a/src/Umbraco.Core/Resolving/ResolutionWeightAttribute.cs b/src/Umbraco.Core/Resolving/ResolutionWeightAttribute.cs new file mode 100644 index 0000000000..c31e0a6b66 --- /dev/null +++ b/src/Umbraco.Core/Resolving/ResolutionWeightAttribute.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core.Resolving +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple=false, Inherited=false)] + public class ResolutionWeightAttribute : Attribute + { + public const int DefaultWeight = 100; + + public ResolutionWeightAttribute(int weight) + { + this.Weight = weight; + } + + public int Weight { get; private set; } + + public static int ReadWeight(Type type) + { + var attr = type.GetCustomAttribute(false); + return attr != null ? attr.Weight : DefaultWeight; + } + } +} diff --git a/src/Umbraco.Core/Resolving/ResolverBase.cs b/src/Umbraco.Core/Resolving/ResolverBase.cs new file mode 100644 index 0000000000..023eaecdc7 --- /dev/null +++ b/src/Umbraco.Core/Resolving/ResolverBase.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; + +namespace Umbraco.Core.Resolving +{ + public abstract class ResolverBase + { + static TResolver _resolver; + static readonly ReaderWriterLockSlim ResolversLock = new ReaderWriterLockSlim(); + + public static TResolver Current + { + get + { + using (new ReadLock(ResolversLock)) + { + if (_resolver == null) + throw new InvalidOperationException("Current has not been initialized. You must initialize Current before trying to read it."); + return _resolver; + } + } + + set + { + using (new WriteLock(ResolversLock)) + { + if (value == null) + throw new ArgumentNullException("value"); + if (_resolver != null) + throw new InvalidOperationException("Current has already been initialized. It is not possible to re-initialize Current once it has been initialized."); + _resolver = value; + } + } + } + } +} diff --git a/src/Umbraco.Core/Resolving/SingleResolved.cs b/src/Umbraco.Core/Resolving/SingleResolved.cs new file mode 100644 index 0000000000..065ad30898 --- /dev/null +++ b/src/Umbraco.Core/Resolving/SingleResolved.cs @@ -0,0 +1,48 @@ +using System; + +namespace Umbraco.Core.Resolving +{ + public class SingleResolved + { + TResolved _resolved; + bool _canBeNull; + + public SingleResolved() + : this(false) + { } + + public SingleResolved(TResolved value) + : this(false) + { + _resolved = value; + } + + public SingleResolved(bool canBeNull) + { + _canBeNull = canBeNull; + } + + public SingleResolved(TResolved value, bool canBeNull) + { + _resolved = value; + _canBeNull = canBeNull; + } + + public TResolved Value + { + get + { + return _resolved; + } + + set + { + Resolution.EnsureNotFrozen(); + + if (!_canBeNull && value == null) + throw new ArgumentNullException("value"); + _resolved = value; + } + } + } +} diff --git a/src/Umbraco.Core/Resolving/SingleResolverBase.cs b/src/Umbraco.Core/Resolving/SingleResolverBase.cs new file mode 100644 index 0000000000..7ca86b1cb3 --- /dev/null +++ b/src/Umbraco.Core/Resolving/SingleResolverBase.cs @@ -0,0 +1,48 @@ +using System; + +namespace Umbraco.Core.Resolving +{ + public abstract class SingleResolverBase : ResolverBase + { + TResolved _resolved; + bool _canBeNull; + + protected SingleResolverBase() + : this(false) + { } + + protected SingleResolverBase(TResolved value) + : this(false) + { + _resolved = value; + } + + protected SingleResolverBase(bool canBeNull) + { + _canBeNull = canBeNull; + } + + protected SingleResolverBase(TResolved value, bool canBeNull) + { + _resolved = value; + _canBeNull = canBeNull; + } + + protected TResolved Value + { + get + { + return _resolved; + } + + set + { + Resolution.EnsureNotFrozen(); + + if (!_canBeNull && value == null) + throw new ArgumentNullException("value"); + _resolved = value; + } + } + } +} diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index c4e81b2d31..39e95572dc 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d4aea239e8..b5f9b02f61 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -48,6 +48,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web/NiceUrlResolver.cs b/src/Umbraco.Web/NiceUrlResolver.cs deleted file mode 100644 index dec04e8a52..0000000000 --- a/src/Umbraco.Web/NiceUrlResolver.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -using Umbraco.Core; -using Umbraco.Web.Routing; - -using umbraco; -using umbraco.IO; -using umbraco.cms.businesslogic.web; - -namespace Umbraco.Web -{ - /// - /// Resolves NiceUrls for a given node id - /// - internal class NiceUrlResolver - { - public NiceUrlResolver(ContentStore contentStore, UmbracoContext umbracoContext) - { - _umbracoContext = umbracoContext; - _contentStore = contentStore; - } - - private readonly UmbracoContext _umbracoContext; - private readonly ContentStore _contentStore; - - // note: this could be a parameter... - const string UrlNameProperty = "@urlName"; - - #region GetNiceUrl - - public string GetNiceUrl(int nodeId) - { - return GetNiceUrl(nodeId, _umbracoContext.UmbracoUrl, false); - } - - public string GetNiceUrl(int nodeId, Uri current, bool absolute) - { - string path; - Uri domainUri; - - // will not read cache if previewing! - var route = _umbracoContext.InPreviewMode - ? null - : _umbracoContext.RoutesCache.GetRoute(nodeId); - - if (route != null) - { - // route is / eg "-1/", "-1/foo", "123/", "123/foo/bar"... - int pos = route.IndexOf('/'); - path = route.Substring(pos); - int id = int.Parse(route.Substring(0, pos)); // will be -1 or 1234 - domainUri = id > 0 ? DomainUriAtNode(id, current) : null; - } - else - { - var node = _contentStore.GetNodeById(nodeId); - if (node == null) - return "#"; // legacy wrote to the log here... - - var pathParts = new List(); - int id = nodeId; - domainUri = DomainUriAtNode(id, current); - while (domainUri == null && id > 0) - { - pathParts.Add(_contentStore.GetNodeProperty(node, UrlNameProperty)); - node = _contentStore.GetNodeParent(node); - id = int.Parse(_contentStore.GetNodeProperty(node, "@id")); // will be -1 or 1234 - domainUri = id > 0 ? DomainUriAtNode(id, current) : null; - } - - // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (domainUri == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) - pathParts.RemoveAt(pathParts.Count - 1); - - pathParts.Reverse(); - path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - route = id.ToString() + path; - - if (!_umbracoContext.InPreviewMode) - _umbracoContext.RoutesCache.Store(nodeId, route); - } - - return AssembleUrl(domainUri, path, current, absolute).ToString(); - } - - Uri AssembleUrl(Uri domain, string path, Uri current, bool absolute) - { - Uri uri; - - if (domain == null) - { - // no domain was found : return a relative url, add vdir if any - uri = new Uri(global::umbraco.IO.SystemDirectories.Root + path, UriKind.Relative); - } - else - { - // a domain was found : return an absolute or relative url - // cannot handle vdir, has to be in domain uri - if (!absolute && current != null && domain.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) - uri = new Uri(domain.AbsolutePath.TrimEnd('/') + path, UriKind.Relative); // relative - else - uri = new Uri(domain.GetLeftPart(UriPartial.Path).TrimEnd('/') + path); // absolute - } - - return UriFromUmbraco(uri); - } - - Uri DomainUriAtNode(int nodeId, Uri current) - { - // be safe - if (nodeId <= 0) - return null; - - // apply filter on domains defined on that node - var domainAndUri = Domains.ApplicableDomains(Domain.GetDomainsById(nodeId), current, true); - return domainAndUri == null ? null : domainAndUri.Uri; - } - - #endregion - - #region Map public urls to/from umbraco urls - - // fixme - what about vdir? - // path = path.Substring(UriUtility.AppVirtualPathPrefix.Length); // remove virtual directory - - public static Uri UriFromUmbraco(Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - if (path == "/") - return uri; - - if (!global::umbraco.GlobalSettings.UseDirectoryUrls) - path += ".aspx"; - else if (global::umbraco.UmbracoSettings.AddTrailingSlash) - path += "/"; - - return uri.Rewrite(path); - } - - public static Uri UriToUmbraco(Uri uri) - { - var path = uri.GetSafeAbsolutePath(); - - path = path.ToLower(); - if (path != "/") - path = path.TrimEnd('/'); - if (path.EndsWith(".aspx")) - path = path.Substring(0, path.Length - ".aspx".Length); - - return uri.Rewrite(path); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PluginResolverExtensions.cs b/src/Umbraco.Web/PluginResolverExtensions.cs index 6be48eff08..d480e3ec91 100644 --- a/src/Umbraco.Web/PluginResolverExtensions.cs +++ b/src/Umbraco.Web/PluginResolverExtensions.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web public static class PluginResolverExtensions { - private static volatile IEnumerable _lookups; + private static volatile IEnumerable _lookups; private static readonly object Locker = new object(); /// @@ -21,7 +21,7 @@ namespace Umbraco.Web /// /// /// - internal static IEnumerable ResolveLookups(this PluginResolver plugins) + internal static IEnumerable ResolveLookups(this PluginResolver plugins) { if (_lookups == null) { @@ -29,14 +29,13 @@ namespace Umbraco.Web { if (_lookups == null) { - var typeFinder = new TypeFinder2(); - var lookupTypes = typeFinder.FindClassesOfType(); - var lookups = new List(); + var lookupTypes = TypeFinder.FindClassesOfType(); + var lookups = new List(); foreach (var l in lookupTypes) { try { - var typeInstance = Activator.CreateInstance(l) as IRequestDocumentResolver; + var typeInstance = Activator.CreateInstance(l) as IDocumentLookup; lookups.Add(typeInstance); } catch (Exception ex) diff --git a/src/Umbraco.Web/Routing/DefaultRoutesCache.cs b/src/Umbraco.Web/Routing/DefaultRoutesCache.cs index b4daefaea2..55a2c08e75 100644 --- a/src/Umbraco.Web/Routing/DefaultRoutesCache.cs +++ b/src/Umbraco.Web/Routing/DefaultRoutesCache.cs @@ -6,7 +6,7 @@ using Umbraco.Core; namespace Umbraco.Web.Routing { /// - /// The default implementation of IRoutesCache + /// Provides a default implementation of . /// internal class DefaultRoutesCache : IRoutesCache { @@ -14,23 +14,31 @@ namespace Umbraco.Web.Routing private Dictionary _routes; private Dictionary _nodeIds; + /// + /// Initializes a new instance of the class. + /// public DefaultRoutesCache() { Clear(); - // here we should register handlers to clear the cache when content changes - // at the moment this is done by library, which clears everything when content changed - // - // but really, we should do some partial refreshes! - // otherwise, we could even cache 404 errors... - // - // these are the two events used by library in legacy code... - // is it enough? + //FIXME: // + // here we must register handlers to clear the cache when content changes + // this was done by presentation.library, which cleared everything when content changed + // but really, we should do some partial refreshes + + // these are the two events that were used by presentation.library + // are they enough? + global::umbraco.content.AfterRefreshContent += (sender, e) => Clear(); global::umbraco.content.AfterUpdateDocumentCache += (sender, e) => Clear(); } + /// + /// Stores a route for a node. + /// + /// The node identified. + /// The route. public void Store(int nodeId, string route) { using (new WriteLock(_lock)) @@ -40,6 +48,11 @@ namespace Umbraco.Web.Routing } } + /// + /// Gets a route for a node. + /// + /// The node identifier. + /// The route for the node, else null. public string GetRoute(int nodeId) { lock (new ReadLock(_lock)) @@ -48,6 +61,11 @@ namespace Umbraco.Web.Routing } } + /// + /// Gets a node for a route. + /// + /// The route. + /// The node identified for the route, else zero. public int GetNodeId(string route) { using (new ReadLock(_lock)) @@ -56,6 +74,10 @@ namespace Umbraco.Web.Routing } } + /// + /// Clears the route for a node. + /// + /// The node identifier. public void ClearNode(int nodeId) { using (var lck = new UpgradeableReadLock(_lock)) @@ -69,6 +91,9 @@ namespace Umbraco.Web.Routing } } + /// + /// Clears all routes. + /// public void Clear() { using (new WriteLock(_lock)) diff --git a/src/Umbraco.Web/Routing/DocumentRequest.cs b/src/Umbraco.Web/Routing/DocumentRequest.cs index 99b42b7263..bc25fc0fbc 100644 --- a/src/Umbraco.Web/Routing/DocumentRequest.cs +++ b/src/Umbraco.Web/Routing/DocumentRequest.cs @@ -140,22 +140,22 @@ namespace Umbraco.Web.Routing #endregion - #region Resolve + #region Lookup /// /// Determines the site root (if any) matching the http request. /// /// A value indicating whether a domain was found. - public bool ResolveDomain() + public bool LookupDomain() { - const string tracePrefix = "ResolveDomain: "; + const string tracePrefix = "LookupDomain: "; // note - we are not handling schemes nor ports here. Trace.TraceInformation("{0}Uri=\"{1}\"", tracePrefix, this.Uri); // try to find a domain matching the current request - var domainAndUri = Domains.ApplicableDomains(Domain.GetDomains(), RoutingContext.UmbracoContext.UmbracoUrl, false); + var domainAndUri = Domains.DomainMatch(Domain.GetDomains(), RoutingContext.UmbracoContext.UmbracoUrl, false); // handle domain if (domainAndUri != null) @@ -193,17 +193,17 @@ namespace Umbraco.Web.Routing /// Determines the Umbraco document (if any) matching the http request. /// /// A value indicating whether a document and template nave been found. - public bool ResolveDocument() + public bool LookupDocument() { - const string tracePrefix = "ResolveDocument: "; + const string tracePrefix = "LookupDocument: "; Trace.TraceInformation("{0}Path=\"{1}\"", tracePrefix, this.Uri.AbsolutePath); // look for the document // the first successful resolver, if any, will set this.Node, and may also set this.Template // some lookups may implement caching Trace.TraceInformation("{0}Begin resolvers", tracePrefix); - var resolvers = RoutingContext.RouteLookups.GetLookups(); - resolvers.Any(resolver => resolver.TrySetDocument(this)); + var lookups = RoutingContext.DocumentLookupsResolver.DocumentLookups; + lookups.Any(lookup => lookup.TrySetDocument(this)); Trace.TraceInformation("{0}End resolvers, {1}", tracePrefix, (this.HasNode ? "a document was found" : "no document was found")); // fixme - not handling umbracoRedirect @@ -211,7 +211,7 @@ namespace Umbraco.Web.Routing // so after ResolveDocument2() => docreq.IsRedirect => handled by the module! // handle not-found, redirects, access, template - ResolveDocument2(); + LookupDocument2(); // handle umbracoRedirect (moved from umbraco.page) FollowRedirect(); @@ -224,9 +224,9 @@ namespace Umbraco.Web.Routing /// Performs the document resolution second pass. /// /// The second pass consists in handling "not found", internal redirects, access validation, and template. - void ResolveDocument2() + void LookupDocument2() { - const string tracePrefix = "ResolveDocument2: "; + const string tracePrefix = "LookupDocument2: "; // handle "not found", follow internal redirects, validate access, template // because these might loop, we have to have some sort of infinite loop detection @@ -240,10 +240,11 @@ namespace Umbraco.Web.Routing if (!this.HasNode) { this.Is404 = true; - Trace.TraceInformation("{0}No document, try notFound lookup", tracePrefix); + Trace.TraceInformation("{0}No document, try last chance lookup", tracePrefix); // if it fails then give up, there isn't much more that we can do - if (RoutingContext.LookupNotFound == null || !RoutingContext.LookupNotFound.TrySetDocument(this)) + var lastChance = RoutingContext.DocumentLookupsResolver.DocumentLastChanceLookup; + if (lastChance == null || !lastChance.TrySetDocument(this)) { Trace.TraceInformation("{0}Failed to find a document, give up", tracePrefix); break; @@ -266,7 +267,7 @@ namespace Umbraco.Web.Routing // resolve template if (this.HasNode) - ResolveTemplate(); + LookupTemplate(); // loop while we don't have page, ie the redirect or access // got us to nowhere and now we need to run the notFoundLookup again @@ -383,9 +384,9 @@ namespace Umbraco.Web.Routing /// /// Resolves a template for the current node. /// - void ResolveTemplate() + void LookupTemplate() { - const string tracePrefix = "ResolveTemplate: "; + const string tracePrefix = "LookupTemplate: "; if (this.Node == null) throw new InvalidOperationException("There is no node."); @@ -446,7 +447,7 @@ namespace Umbraco.Web.Routing redirectId = -1; string redirectUrl = "#"; if (redirectId > 0) - redirectUrl = RoutingContext.NiceUrlResolver.GetNiceUrl(redirectId); + redirectUrl = RoutingContext.NiceUrlProvider.GetNiceUrl(redirectId); if (redirectUrl != "#") this.RedirectUrl = redirectUrl; } diff --git a/src/Umbraco.Web/Routing/Domains.cs b/src/Umbraco.Web/Routing/Domains.cs index 01b5def78a..72454c3718 100644 --- a/src/Umbraco.Web/Routing/Domains.cs +++ b/src/Umbraco.Web/Routing/Domains.cs @@ -8,20 +8,48 @@ using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Routing { + /// + /// Provides utilities to handle domains. + /// public class Domains { + /// + /// Represents an Umbraco domain and its normalized uri. + /// + /// + /// In Umbraco it is valid to create domains with name such as example.com, https://www.example.com, example.com/foo/. + /// The normalized uri of a domain begins with a scheme and ends with no slash, eg http://example.com/, https://www.example.com/, http://example.com/foo/. + /// public class DomainAndUri { + /// + /// The Umbraco domain. + /// public Domain Domain; + + /// + /// The normalized uri of the domain. + /// public Uri Uri; + /// + /// Gets a string that represents the instance. + /// + /// A string that represents the current instance. public override string ToString() { return string.Format("{{ \"{0}\", \"{1}\" }}", Domain.Name, Uri); } } - public static DomainAndUri ApplicableDomains(IEnumerable domains, Uri current, bool defaultToFirst) + /// + /// Finds the domain that best matches the current uri, into an enumeration of domains. + /// + /// The enumeration of Umbraco domains. + /// The uri of the current request, or null. + /// A value indicating whether to return the first domain of the list when no domain matches. + /// The domain and its normalized uri, that best matches the current uri, else the first domain (if defaultToFirst is true), else null. + public static DomainAndUri DomainMatch(IEnumerable domains, Uri current, bool defaultToFirst) { if (!domains.Any()) return null; @@ -57,6 +85,29 @@ namespace Umbraco.Web.Routing return domainAndUri; } + /// + /// Gets an enumeration of matching an enumeration of Umbraco domains. + /// + /// The enumeration of Umbraco domains. + /// The uri of the current request, or null. + /// The enumeration of matching the enumeration of Umbraco domains. + public static IEnumerable DomainMatches(IEnumerable domains, Uri current) + { + var scheme = current == null ? Uri.UriSchemeHttp : current.Scheme; + var domainsAndUris = domains + .Select(d => new { Domain = d, UriString = UriUtility.TrimPathEndSlash(UriUtility.StartWithScheme(d.Name, scheme)) }) + .OrderByDescending(t => t.UriString) + .Select(t => new DomainAndUri { Domain = t.Domain, Uri = new Uri(t.UriString) }); + return domainsAndUris; + } + + /// + /// 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('/'); diff --git a/src/Umbraco.Web/Routing/IRoutesCache.cs b/src/Umbraco.Web/Routing/IRoutesCache.cs index 0f4a1044ec..9033bc746e 100644 --- a/src/Umbraco.Web/Routing/IRoutesCache.cs +++ b/src/Umbraco.Web/Routing/IRoutesCache.cs @@ -1,11 +1,44 @@ namespace Umbraco.Web.Routing { + /// + /// Represents a bi-directional cache that binds node identifiers and routes. + /// + /// + /// The cache is used both for inbound (map a route to a node) and outbound (map a node to a url). + /// A route is [rootId]/path/to/node where rootId is the id of the node holding an Umbraco domain, or -1. + /// internal interface IRoutesCache { + /// + /// Stores a route for a node. + /// + /// The node identified. + /// The route. void Store(int nodeId, string route); + + /// + /// Gets a route for a node. + /// + /// The node identifier. + /// The route for the node, else null. string GetRoute(int nodeId); + + /// + /// Gets a node for a route. + /// + /// The route. + /// The node identified for the route, else zero. int GetNodeId(string route); + + /// + /// Clears the route for a node. + /// + /// The node identifier. void ClearNode(int nodeId); + + /// + /// Clears all routes. + /// void Clear(); } } \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/NiceUrlProvider.cs b/src/Umbraco.Web/Routing/NiceUrlProvider.cs new file mode 100644 index 0000000000..a124ba1d13 --- /dev/null +++ b/src/Umbraco.Web/Routing/NiceUrlProvider.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +using Umbraco.Core; +using Umbraco.Web.Routing; + +using umbraco; +using umbraco.IO; +using umbraco.cms.businesslogic.web; + +namespace Umbraco.Web.Routing +{ + /// + /// Provides nice urls for a nodes. + /// + internal class NiceUrlProvider + { + /// + /// Initializes a new instance of the class. + /// + /// The content store. + /// The Umbraco context. + public NiceUrlProvider(ContentStore contentStore, UmbracoContext umbracoContext) + { + _umbracoContext = umbracoContext; + _contentStore = contentStore; + } + + private readonly UmbracoContext _umbracoContext; + private readonly ContentStore _contentStore; + + // note: this could be a parameter... + const string UrlNameProperty = "@urlName"; + + #region GetNiceUrl + + /// + /// Gets the nice url of a node. + /// + /// The node id. + /// The nice url for the node. + /// The url is absolute or relative depending on the current url. + public string GetNiceUrl(int nodeId) + { + return GetNiceUrl(nodeId, _umbracoContext.UmbracoUrl, false); + } + + /// + /// Gets the nice url of a node. + /// + /// The node id. + /// The current url. + /// A value indicating whether the url should be absolute in any case. + /// The nice url for the node. + /// The url is absolute or relative depending on the current url, unless absolute is true, and then it is always absolute. + public string GetNiceUrl(int nodeId, Uri current, bool absolute) + { + string path; + Uri domainUri; + + // will not read cache if previewing! + var route = _umbracoContext.InPreviewMode + ? null + : _umbracoContext.RoutesCache.GetRoute(nodeId); + + if (route != null) + { + // route is / eg "-1/", "-1/foo", "123/", "123/foo/bar"... + int pos = route.IndexOf('/'); + path = route.Substring(pos); + int id = int.Parse(route.Substring(0, pos)); // will be -1 or 1234 + domainUri = id > 0 ? DomainUriAtNode(id, current) : null; + } + else + { + var node = _contentStore.GetNodeById(nodeId); + if (node == null) + return "#"; // legacy wrote to the log here... + + var pathParts = new List(); + int id = nodeId; + domainUri = DomainUriAtNode(id, current); + while (domainUri == null && id > 0) + { + pathParts.Add(_contentStore.GetNodeProperty(node, UrlNameProperty)); + node = _contentStore.GetNodeParent(node); + id = int.Parse(_contentStore.GetNodeProperty(node, "@id")); // will be -1 or 1234 + domainUri = id > 0 ? DomainUriAtNode(id, current) : null; + } + + // no domain, respect HideTopLevelNodeFromPath for legacy purposes + if (domainUri == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) + pathParts.RemoveAt(pathParts.Count - 1); + + pathParts.Reverse(); + path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc + route = id.ToString() + path; + + if (!_umbracoContext.InPreviewMode) + _umbracoContext.RoutesCache.Store(nodeId, route); + } + + return AssembleUrl(domainUri, path, current, absolute).ToString(); + } + + /// + /// Gets the nice urls of a node. + /// + /// The node id. + /// The current url. + /// An enumeration of all valid urls for the node. + /// The urls are absolute. A node can have more than one url if more than one domain is defined. + public IEnumerable GetNiceUrls(int nodeId, Uri current) + { + // this is for editContent.aspx which had its own, highly buggy, implementation of NiceUrl... + //TODO: finalize & test implementation then replace in editContent.aspx + + string path; + IEnumerable domainUris; + + // will not read cache if previewing! + var route = _umbracoContext.InPreviewMode + ? null + : _umbracoContext.RoutesCache.GetRoute(nodeId); + + if (route != null) + { + // route is / eg "-1/", "-1/foo", "123/", "123/foo/bar"... + int pos = route.IndexOf('/'); + path = route.Substring(pos); + int id = int.Parse(route.Substring(0, pos)); // will be -1 or 1234 + domainUris = id > 0 ? DomainUrisAtNode(id, current) : new Uri[] { }; + } + else + { + var node = _contentStore.GetNodeById(nodeId); + if (node == null) + return new string[] { "#" }; // legacy wrote to the log here... + + var pathParts = new List(); + int id = nodeId; + domainUris = DomainUrisAtNode(id, current); + while (!domainUris.Any() && id > 0) + { + pathParts.Add(_contentStore.GetNodeProperty(node, UrlNameProperty)); + node = _contentStore.GetNodeParent(node); + id = int.Parse(_contentStore.GetNodeProperty(node, "@id")); // will be -1 or 1234 + domainUris = id > 0 ? DomainUrisAtNode(id, current) : new Uri[] { }; + } + + // no domain, respect HideTopLevelNodeFromPath for legacy purposes + if (!domainUris.Any() && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) + pathParts.RemoveAt(pathParts.Count - 1); + + pathParts.Reverse(); + path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc + route = id.ToString() + path; + + if (!_umbracoContext.InPreviewMode) + _umbracoContext.RoutesCache.Store(nodeId, route); + } + + return AssembleUrls(domainUris, path, current).Select(uri => uri.ToString()); + } + + Uri AssembleUrl(Uri domainUri, string path, Uri current, bool absolute) + { + Uri uri; + + if (domainUri == null) + { + // no domain was found : return a relative url, add vdir if any + uri = new Uri(global::umbraco.IO.SystemDirectories.Root + path, UriKind.Relative); + } + else + { + // a domain was found : return an absolute or relative url + // cannot handle vdir, has to be in domain uri + if (!absolute && current != null && domainUri.GetLeftPart(UriPartial.Authority) == current.GetLeftPart(UriPartial.Authority)) + uri = new Uri(domainUri.AbsolutePath.TrimEnd('/') + path, UriKind.Relative); // relative + else + uri = new Uri(domainUri.GetLeftPart(UriPartial.Path).TrimEnd('/') + path); // absolute + } + + return UriFromUmbraco(uri); + } + + IEnumerable AssembleUrls(IEnumerable domainUris, string path, Uri current) + { + if (domainUris.Any()) + { + return domainUris.Select(domainUri => new Uri(domainUri.GetLeftPart(UriPartial.Path).TrimEnd('/') + path)); + } + else + { + // no domain was found : return a relative url, add vdir if any + return new Uri[] { new Uri(global::umbraco.IO.SystemDirectories.Root + path, UriKind.Relative) }; + } + } + + Uri DomainUriAtNode(int nodeId, Uri current) + { + // be safe + if (nodeId <= 0) + return null; + + // apply filter on domains defined on that node + var domainAndUri = Domains.DomainMatch(Domain.GetDomainsById(nodeId), current, true); + return domainAndUri == null ? null : domainAndUri.Uri; + } + + IEnumerable DomainUrisAtNode(int nodeId, Uri current) + { + // be safe + if (nodeId <= 0) + return new Uri[] { }; + + var domainAndUris = Domains.DomainMatches(Domain.GetDomainsById(nodeId), current); + return domainAndUris.Select(d => d.Uri); + } + + #endregion + + #region Map public urls to/from umbraco urls + + // fixme - what about vdir? + // path = path.Substring(UriUtility.AppVirtualPathPrefix.Length); // remove virtual directory + + public static Uri UriFromUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + if (path == "/") + return uri; + + if (!global::umbraco.GlobalSettings.UseDirectoryUrls) + path += ".aspx"; + else if (global::umbraco.UmbracoSettings.AddTrailingSlash) + path += "/"; + + return uri.Rewrite(path); + } + + public static Uri UriToUmbraco(Uri uri) + { + var path = uri.GetSafeAbsolutePath(); + + path = path.ToLower(); + if (path != "/") + path = path.TrimEnd('/'); + if (path.EndsWith(".aspx")) + path = path.Substring(0, path.Length - ".aspx".Length); + + return uri.Rewrite(path); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/RequestDocumentResolverWeightAttribute.cs b/src/Umbraco.Web/Routing/RequestDocumentResolverWeightAttribute.cs deleted file mode 100644 index c76d7d5cf6..0000000000 --- a/src/Umbraco.Web/Routing/RequestDocumentResolverWeightAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace Umbraco.Web.Routing -{ - /// - /// Specifies the relative weight of an IRequestDocumentResolver implementation. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - internal class RequestDocumentResolverWeightAttribute : Attribute - { - /// - /// Gets the default weight. - /// - public const int DefaultWeight = 100; - - /// - /// Initializes a new instance of the class with the weight. - /// - /// The weight. - public RequestDocumentResolverWeightAttribute(int weight) - : base() - { - this.Weight = weight; - } - - /// - /// Gets the weight. - /// - public int Weight { get; private set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/RequestDocumentResolversResolver.cs b/src/Umbraco.Web/Routing/RequestDocumentResolversResolver.cs new file mode 100644 index 0000000000..07735549e8 --- /dev/null +++ b/src/Umbraco.Web/Routing/RequestDocumentResolversResolver.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +using Umbraco.Core; +using Umbraco.Core.Resolving; + +namespace Umbraco.Web.Routing +{ + class RequestDocumentResolversResolver : ResolverBase + { + internal RequestDocumentResolversResolver(IEnumerable resolvers, IRequestDocumentLastChanceResolver lastChanceResolver) + { + _resolvers.AddRange(resolvers); + _lastChanceResolver.Value = lastChanceResolver; + } + + #region LastChanceResolver + + SingleResolved _lastChanceResolver = new SingleResolved(true); + + public IRequestDocumentLastChanceResolver RequestDocumentLastChanceResolver + { + get { return _lastChanceResolver.Value; } + set { _lastChanceResolver.Value = value; } + } + + #endregion + + #region Resolvers + + ManyWeightedResolved _resolvers = new ManyWeightedResolved(); + + public IEnumerable RequestDocumentResolvers + { + get { return _resolvers.Values; } + } + + public ManyWeightedResolved RequestDocumentResolversResolution + { + get { return _resolvers; } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/Routing/ResolveByAlias.cs b/src/Umbraco.Web/Routing/ResolveByAlias.cs index 3f41375309..321ef73bc0 100644 --- a/src/Umbraco.Web/Routing/ResolveByAlias.cs +++ b/src/Umbraco.Web/Routing/ResolveByAlias.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Xml; +using Umbraco.Core.Resolving; namespace Umbraco.Web.Routing { @@ -10,7 +11,7 @@ namespace Umbraco.Web.Routing // at the moment aliases are not cleaned up into nice urls // - [RequestDocumentResolverWeight(50)] + [ResolutionWeight(50)] internal class ResolveByAlias : IRequestDocumentResolver { static readonly TraceSource Trace = new TraceSource("ResolveByAlias"); diff --git a/src/Umbraco.Web/Routing/ResolveById.cs b/src/Umbraco.Web/Routing/ResolveById.cs index 954152a7e2..0e2bfa74e1 100644 --- a/src/Umbraco.Web/Routing/ResolveById.cs +++ b/src/Umbraco.Web/Routing/ResolveById.cs @@ -1,13 +1,14 @@ using System; using System.Diagnostics; using System.Xml; +using Umbraco.Core.Resolving; namespace Umbraco.Web.Routing { // handles /1234 where 1234 is the id of a document // - [RequestDocumentResolverWeight(20)] + [ResolutionWeight(20)] internal class ResolveById : IRequestDocumentResolver { static readonly TraceSource Trace = new TraceSource("ResolveById"); diff --git a/src/Umbraco.Web/Routing/ResolveByNiceUrl.cs b/src/Umbraco.Web/Routing/ResolveByNiceUrl.cs index d5d7cb1b8f..4d8ebe0266 100644 --- a/src/Umbraco.Web/Routing/ResolveByNiceUrl.cs +++ b/src/Umbraco.Web/Routing/ResolveByNiceUrl.cs @@ -1,12 +1,13 @@ using System.Diagnostics; using System.Xml; +using Umbraco.Core.Resolving; namespace Umbraco.Web.Routing { // handles "/foo/bar" where "/foo/bar" is the path to a document // - [RequestDocumentResolverWeight(10)] + [ResolutionWeight(10)] internal class ResolveByNiceUrl : IRequestDocumentResolver { static readonly TraceSource Trace = new TraceSource("ResolveByNiceUrl"); diff --git a/src/Umbraco.Web/Routing/ResolveByNiceUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ResolveByNiceUrlAndTemplate.cs index 1ccb043af6..a5a7473fd9 100644 --- a/src/Umbraco.Web/Routing/ResolveByNiceUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ResolveByNiceUrlAndTemplate.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Xml; +using Umbraco.Core.Resolving; using umbraco.cms.businesslogic.template; namespace Umbraco.Web.Routing @@ -8,7 +9,7 @@ namespace Umbraco.Web.Routing // handles /foo/bar/