From 5f8747b4d3770ea2585a86f080a582f1826c3160 Mon Sep 17 00:00:00 2001
From: Stephan
Date: Wed, 4 May 2016 12:45:20 +0200
Subject: [PATCH] U4-1780 - detect colliding urls (#1243)
* U4-1780 - detect colliding urls
* U4-1780 - fix error message
* U4-1780 - fix XmlHelper issue with sortOrder
* U4-1780 - bugfix
---
src/Umbraco.Core/XmlHelper.cs | 10 +++-
src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 +
.../umbraco/config/lang/en_us.xml | 3 +-
src/Umbraco.Web/Models/ContentExtensions.cs | 2 +
.../PublishedContentCache.cs | 57 +++++++++++++++----
src/Umbraco.Web/Routing/DefaultUrlProvider.cs | 12 ++++
.../Routing/UrlProviderExtensions.cs | 24 ++++++++
7 files changed, 93 insertions(+), 16 deletions(-)
diff --git a/src/Umbraco.Core/XmlHelper.cs b/src/Umbraco.Core/XmlHelper.cs
index 1978c329ab..d45f0cac3b 100644
--- a/src/Umbraco.Core/XmlHelper.cs
+++ b/src/Umbraco.Core/XmlHelper.cs
@@ -210,6 +210,9 @@ namespace Umbraco.Core
var childNodesAndOrder = parentNode.SelectNodes(childNodesXPath).Cast()
.Select(x => Tuple.Create(x, orderBy(x))).ToArray();
+ // only one node = node is in the right place already, obviously
+ if (childNodesAndOrder.Length == 1) return false;
+
// find the first node with a sortOrder > node.sortOrder
var i = 0;
while (i < childNodesAndOrder.Length && childNodesAndOrder[i].Item2 <= nodeSortOrder)
@@ -220,17 +223,18 @@ namespace Umbraco.Core
{
// and node is just before, we're done already
// else we need to move it right before the node that was found
- if (i > 0 && childNodesAndOrder[i - 1].Item1 != node)
+ if (i == 0 || childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.InsertBefore(node, childNodesAndOrder[i].Item1);
return true;
}
}
- else
+ else // i == childNodesAndOrder.Length && childNodesAndOrder.Length > 1
{
// and node is the last one, we're done already
// else we need to append it as the last one
- if (i > 0 && childNodesAndOrder[i - 1].Item1 != node)
+ // (and i > 1, see above)
+ if (childNodesAndOrder[i - 1].Item1 != node)
{
parentNode.AppendChild(node);
return true;
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
index e62cbd7ed4..94963cddb1 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml
@@ -147,6 +147,7 @@
Properties
This document is published but is not visible because the parent '%0%' is unpublished
Oops: this document is published but is not in the cache (internal error)
+ Oops: this document is published but its url would collide with content %0%
Publish
Publication Status
Publish at
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
index c313d7ad6a..9743d3166e 100644
--- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
@@ -148,6 +148,7 @@
Properties
This document is published but is not visible because the parent '%0%' is unpublished
Oops: this document is published but is not in the cache (internal error)
+ Oops: this document is published but its url would collide with content %0%
Publish
Publication Status
Publish at
@@ -676,7 +677,7 @@ To manage your website, simply open the Umbraco back office and start adding con
Umbraco: Reset Password
Your username to login to the Umbraco back-office is: %0%
Click here to reset your password or copy/paste this URL into your browser:
%1%
]]>
-
+
Dashboard
diff --git a/src/Umbraco.Web/Models/ContentExtensions.cs b/src/Umbraco.Web/Models/ContentExtensions.cs
index 0d280c4c49..cfee14a2a8 100644
--- a/src/Umbraco.Web/Models/ContentExtensions.cs
+++ b/src/Umbraco.Web/Models/ContentExtensions.cs
@@ -48,6 +48,8 @@ namespace Umbraco.Web.Models
? null // for tests only
: umbracoContext.ContentCache.GetRouteById(contentId); // cached
+ if (route != null && route.StartsWith("err/")) route = null;
+
var domainHelper = new DomainHelper(domainService);
IDomain domain;
diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs
index d16133f170..8e9b529b7f 100644
--- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs
+++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs
@@ -55,19 +55,28 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// cache if we have a content and not previewing
if (content != null && preview == false)
- {
- var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/')));
- var iscanon =
- UnitTesting == false
- && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false;
- // and only if this is the canonical url (the one GetUrl would return)
- if (iscanon)
- _routesCache.Store(content.Id, route);
- }
+ AddToCacheIfDeepestRoute(umbracoContext, content, route);
return content;
}
+ private void AddToCacheIfDeepestRoute(UmbracoContext umbracoContext, IPublishedContent content, string route)
+ {
+ var domainRootNodeId = route.StartsWith("/") ? -1 : int.Parse(route.Substring(0, route.IndexOf('/')));
+
+ // so we have a route that maps to a content... say "1234/path/to/content" - however, there could be a
+ // domain set on "to" and route "4567/content" would also map to the same content - and due to how
+ // urls computing work (by walking the tree up to the first domain we find) it is that second route
+ // that would be returned - the "deepest" route - and that is the route we want to cache, *not* the
+ // longer one - so make sure we don't cache the wrong route
+
+ var deepest = UnitTesting == false
+ && DomainHelper.ExistsDomainInPath(umbracoContext.Application.Services.DomainService.GetAll(false), content.Path, domainRootNodeId) == false;
+
+ if (deepest)
+ _routesCache.Store(content.Id, route);
+ }
+
public virtual string GetRouteById(UmbracoContext umbracoContext, bool preview, int contentId)
{
// try to get from cache if not previewing
@@ -80,11 +89,35 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache
// else actually determine the route
route = DetermineRouteById(umbracoContext, preview, contentId);
- // cache if we have a route and not previewing
- if (route != null && preview == false)
+ // node not found
+ if (route == null)
+ return null;
+
+ // find the content back, detect routes collisions: we should find ourselves back,
+ // else it means that another content with "higher priority" is sharing the same route.
+ // perf impact:
+ // - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways)
+ // - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again
+ // assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed
+ var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision
+ if (loopId == 0)
+ {
+ var content = DetermineIdByRoute(umbracoContext, preview, route, GlobalSettings.HideTopLevelNodeFromPath);
+
+ // add the other route to cache so next time we have it already
+ if (content != null && preview == false)
+ AddToCacheIfDeepestRoute(umbracoContext, content, route);
+
+ loopId = content == null ? 0 : content.Id; // though... 0 here would be quite weird?
+ }
+
+ // cache if we have a route and not previewing and it's not a colliding route
+ // (the result of DetermineRouteById is always the deepest route)
+ if (/*route != null &&*/ preview == false && loopId == contentId)
_routesCache.Store(contentId, route);
- return route;
+ // return route if no collision, else report collision
+ return loopId == contentId ? route : ("err/" + loopId);
}
IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, bool preview, string route, bool hideTopLevelNode)
diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs
index f15c485560..915636889d 100644
--- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs
+++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs
@@ -52,6 +52,7 @@ namespace Umbraco.Web.Routing
// will not use cache if previewing
var route = umbracoContext.ContentCache.GetRouteById(id);
+
if (string.IsNullOrWhiteSpace(route))
{
LogHelper.Debug(
@@ -60,6 +61,14 @@ namespace Umbraco.Web.Routing
return null;
}
+ if (route.StartsWith("err/"))
+ {
+ LogHelper.Debug(
+ "Page with nodeId={0} has a colliding url with page with nodeId={1}.",
+ () => id, () => route.Substring(4));
+ return "#err-" + route.Substring(4);
+ }
+
var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService);
// extract domainUri and path
@@ -102,6 +111,9 @@ namespace Umbraco.Web.Routing
return null;
}
+ if (route.StartsWith("err/"))
+ return null;
+
var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService);
// extract domainUri and path
diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs
index 91860e03e6..0a20f51da7 100644
--- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs
+++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs
@@ -48,6 +48,30 @@ namespace Umbraco.Web.Routing
else
urls.Add(ui.Text("content", "parentNotPublished", parent.Name, umbracoContext.Security.CurrentUser));
}
+ else if (url.StartsWith("#err-"))
+ {
+ // route error, report
+ var id = int.Parse(url.Substring(5));
+ var o = umbracoContext.ContentCache.GetById(id);
+ string s;
+ if (o == null)
+ {
+ s = "(unknown)";
+ }
+ else
+ {
+ var l = new List();
+ while (o != null)
+ {
+ l.Add(o.Name);
+ o = o.Parent;
+ }
+ l.Reverse();
+ s = "/" + string.Join("/", l) + " (id=" + id + ")";
+
+ }
+ urls.Add(ui.Text("content", "routeError", s, umbracoContext.Security.CurrentUser));
+ }
else
{
urls.Add(url);