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 d9b450b2a5..bc25fc0fbc 100644 --- a/src/Umbraco.Web/Routing/DocumentRequest.cs +++ b/src/Umbraco.Web/Routing/DocumentRequest.cs @@ -155,7 +155,7 @@ namespace Umbraco.Web.Routing 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) 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 0771fa556f..9033bc746e 100644 --- a/src/Umbraco.Web/Routing/IRoutesCache.cs +++ b/src/Umbraco.Web/Routing/IRoutesCache.cs @@ -1,19 +1,44 @@ namespace Umbraco.Web.Routing { - // this is a bi-directional cache that contains - // - nodeId to route (used for NiceUrl) - // - route to nodeId (used for inbound requests) - // - // a route is [rootId]/path/to/node - // where rootId is the id of the "site root" node - // - + /// + /// 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 index 00a72b264c..a124ba1d13 100644 --- a/src/Umbraco.Web/Routing/NiceUrlProvider.cs +++ b/src/Umbraco.Web/Routing/NiceUrlProvider.cs @@ -13,10 +13,15 @@ using umbraco.cms.businesslogic.web; namespace Umbraco.Web.Routing { /// - /// Resolves NiceUrls for a given node id + /// 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; @@ -104,20 +109,67 @@ namespace Umbraco.Web.Routing /// 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) + public IEnumerable GetNiceUrls(int nodeId, Uri current) { - // fixme - to be implemented - // this is for editContent.aspx which currently has its own, highly buggy, implementation of NiceUrl... - throw new NotImplementedException(); + // 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 domain, string path, Uri current, bool absolute) + Uri AssembleUrl(Uri domainUri, string path, Uri current, bool absolute) { Uri uri; - if (domain == null) + 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); @@ -126,15 +178,28 @@ namespace Umbraco.Web.Routing { // 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 + 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(domain.GetLeftPart(UriPartial.Path).TrimEnd('/') + path); // absolute + 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 @@ -142,10 +207,20 @@ namespace Umbraco.Web.Routing return null; // apply filter on domains defined on that node - var domainAndUri = Domains.ApplicableDomains(Domain.GetDomainsById(nodeId), current, true); + 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 diff --git a/src/Umbraco.Web/Routing/RoutesCacheResolver.cs b/src/Umbraco.Web/Routing/RoutesCacheResolver.cs index 98d1114866..04a49f4e66 100644 --- a/src/Umbraco.Web/Routing/RoutesCacheResolver.cs +++ b/src/Umbraco.Web/Routing/RoutesCacheResolver.cs @@ -3,12 +3,22 @@ using Umbraco.Core.Resolving; namespace Umbraco.Web.Routing { + /// + /// Resolves the implementation. + /// class RoutesCacheResolver : SingleResolverBase { + /// + /// Initializes a new instance of the class with an implementation. + /// + /// The implementation. internal RoutesCacheResolver(IRoutesCache routesCache) : base(routesCache) { } + /// + /// Gets or sets the implementation. + /// public IRoutesCache RoutesCache { get { return this.Value; } diff --git a/src/Umbraco.Web/Routing/RoutingContext.cs b/src/Umbraco.Web/Routing/RoutingContext.cs index 19aa35fea8..dbb3123034 100644 --- a/src/Umbraco.Web/Routing/RoutingContext.cs +++ b/src/Umbraco.Web/Routing/RoutingContext.cs @@ -5,11 +5,17 @@ namespace Umbraco.Web.Routing { /// - /// represents a request for one specified Umbraco document to be rendered by one specified template, - /// using one particular culture. + /// Provides context for the routing of a request. /// internal class RoutingContext { + /// + /// Initializes a new instance of the class. + /// + /// The Umbraco context. + /// The document lookups resolver. + /// The content store. + /// The nice urls resolver. public RoutingContext( UmbracoContext umbracoContext, DocumentLookupsResolver documentLookupsResolver, @@ -22,9 +28,24 @@ namespace Umbraco.Web.Routing this.NiceUrlProvider = niceUrlResolver; } + /// + /// Gets the Umbraco context. + /// public UmbracoContext UmbracoContext { get; private set; } + + /// + /// Gets the document lookups resolver. + /// public DocumentLookupsResolver DocumentLookupsResolver { get; private set; } + + /// + /// Gets the content store. + /// public ContentStore ContentStore { get; private set; } + + /// + /// Gets the nice urls provider. + /// public NiceUrlProvider NiceUrlProvider { get; private set; } } } \ No newline at end of file