fix NiceUrl and add more tests

This commit is contained in:
Stephan
2012-09-30 12:24:23 -02:00
parent 926427ff68
commit 3e99dd0d55
8 changed files with 329 additions and 164 deletions

View File

@@ -18,7 +18,6 @@ namespace Umbraco.Web.Routing
/// </summary>
internal class NiceUrlProvider
{
internal const string NullUrl = "#";
/// <summary>
@@ -30,37 +29,56 @@ namespace Umbraco.Web.Routing
{
_umbracoContext = umbracoContext;
_publishedContentStore = publishedContentStore;
this.EnforceAbsoluteUrls = false;
}
private readonly UmbracoContext _umbracoContext;
private readonly IPublishedContentStore _publishedContentStore;
public bool EnforceAbsoluteUrls { get; set; }
#region GetNiceUrl
/// <summary>
/// Gets the nice url of a node.
/// </summary>
/// <param name="nodeId">The node id.</param>
/// <param name="nodeId">The node identifier.</param>
/// <returns>The nice url for the node.</returns>
/// <remarks>The url is absolute or relative depending on the current url.</remarks>
/// <remarks>The url is absolute or relative depending on the current url, settings, and options.</remarks>
public string GetNiceUrl(int nodeId)
{
return GetNiceUrl(nodeId, _umbracoContext.UmbracoUrl, false);
var absolute = UmbracoSettings.UseDomainPrefixes || this.EnforceAbsoluteUrls;
return GetNiceUrl(nodeId, _umbracoContext.UmbracoUrl, absolute);
}
/// <summary>
/// Gets the nice url of a node.
/// </summary>
/// <param name="nodeId">The node identifier.</param>
/// <param name="absolute">A value indicating whether the url should be absolute in any case.</param>
/// <returns>The nice url for the node.</returns>
/// <remarks>The url is absolute or relative depending on the current url, unless <c>absolute</c> is true, in which case the url is always absolute.</remarks>
public string GetNiceUrl(int nodeId, bool absolute)
{
return GetNiceUrl(nodeId, _umbracoContext.UmbracoUrl, absolute);
}
/// <summary>
/// Gets the nice url of a node.
/// </summary>
/// <param name="nodeId">The node id.</param>
/// <param name="current">The current url.</param>
/// <param name="current">The current absolute url.</param>
/// <param name="absolute">A value indicating whether the url should be absolute in any case.</param>
/// <returns>The nice url for the node.</returns>
/// <remarks>The url is absolute or relative depending on the current url, unless absolute is true, and then it is always absolute.</remarks>
/// <remarks>The url is absolute or relative depending on url indicated by <c>current</c>, unless <c>absolute</c> is true, in which case the url is always absolute.</remarks>
public string GetNiceUrl(int nodeId, Uri current, bool absolute)
{
Uri domainUri;
string path;
if (!current.IsAbsoluteUri)
throw new ArgumentException("Current url must be absolute.", "current");
// do not read cache if previewing
var route = _umbracoContext.InPreviewMode
? null
@@ -105,26 +123,7 @@ namespace Umbraco.Web.Routing
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (domainUri == null && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath)
{
// in theory if hideTopLevelNodeFromPath is true, then there should be only once
// top-level node, or else domains should be assigned. but for backward compatibility
// we add this check - we look for the document matching "/" and if it's not us, then
// we do not hide the top level path
// it has to be taken care of in IPublishedContentStore.GetDocumentByRoute too so if
// "/foo" fails (looking for "/*/foo") we try also "/foo".
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (node.Parent == null)
{
var rootNode = _publishedContentStore.GetDocumentByRoute(_umbracoContext, "/", true);
if (rootNode.Id == node.Id) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
{
pathParts.RemoveAt(pathParts.Count - 1);
}
}
ApplyHideTopLevelNodeFromPath(node, pathParts);
// assemble the route
pathParts.Reverse();
@@ -140,6 +139,15 @@ namespace Umbraco.Web.Routing
return AssembleUrl(domainUri, path, current, absolute).ToString();
}
#endregion
#region GetAlternateNiceUrls
public IEnumerable<string> GetAllAbsoluteNiceUrls(int nodeId)
{
return GetAlternateNiceUrls(nodeId, _umbracoContext.UmbracoUrl);
}
/// <summary>
/// Gets the nice urls of a node.
/// </summary>
@@ -147,7 +155,7 @@ namespace Umbraco.Web.Routing
/// <param name="current">The current url.</param>
/// <returns>An enumeration of all valid urls for the node.</returns>
/// <remarks>The urls are absolute. A node can have more than one url if more than one domain is defined.</remarks>
public IEnumerable<string> GetNiceUrls(int nodeId, Uri current)
public IEnumerable<string> GetAlternateNiceUrls(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
@@ -160,55 +168,77 @@ namespace Umbraco.Web.Routing
? null
: _umbracoContext.RoutesCache.GetRoute(nodeId);
if (route != null)
if (!string.IsNullOrEmpty(route))
{
// route is <id>/<path> eg "-1/", "-1/foo", "123/", "123/foo/bar"...
// there was a route in the cache - extract domainUri and path
// route is /<path> or <domainRootId>/<path>
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[] { };
path = pos == 0 ? route : route.Substring(pos);
domainUris = pos == 0 ? new Uri[] { } : DomainUrisAtNode(int.Parse(route.Substring(0, pos)), current);
}
else
{
// there was no route in the cache - create a route
var node = _publishedContentStore.GetDocumentById(_umbracoContext, nodeId);
if (node == null)
return new string[] { NullUrl }; // legacy wrote to the log here...
var pathParts = new List<string>();
int id = nodeId;
domainUris = DomainUrisAtNode(id, current);
while (!domainUris.Any() && id > 0)
{
LogHelper.Warn<NiceUrlProvider>(
"Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.",
nodeId);
return new string[] { NullUrl };
}
// walk up from that node until we hit a node with domains,
// or we reach the content root, collecting urls in the way
var pathParts = new List<string>();
var n = node;
domainUris = DomainUrisAtNode(n.Id, current);
while (!domainUris.Any() && n != null) // n is null at root
{
// get the url
var urlName = node.UrlName;
pathParts.Add(urlName);
node = node.Parent; //set to parent node
id = node.Id;
domainUris = id > 0 ? DomainUrisAtNode(id, current) : new Uri[] { };
// move to parent node
n = n.Parent;
domainUris = n == null ? new Uri[] { } : DomainUrisAtNode(n.Id, current);
}
// no domain, respect HideTopLevelNodeFromPath for legacy purposes
if (!domainUris.Any() && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath)
pathParts.RemoveAt(pathParts.Count - 1);
ApplyHideTopLevelNodeFromPath(node, pathParts);
// assemble the route
pathParts.Reverse();
path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc
route = id.ToString() + path;
route = (n == null ? "" : n.Id.ToString()) + path;
// do not store if previewing
if (!_umbracoContext.InPreviewMode)
_umbracoContext.RoutesCache.Store(nodeId, route);
}
// assemble the alternate urls from domainUris (maybe empty) and path
return AssembleUrls(domainUris, path, current).Select(uri => uri.ToString());
}
#endregion
#region Utilities
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(SystemDirectories.Root + path, UriKind.Relative);
// no domain was found : return an absolute or relative url
// handle vdir if any
if (!absolute || current == null)
uri = new Uri(UriUtility.ToAbsolute(path), UriKind.Relative);
else
uri = new Uri(current.GetLeftPart(UriPartial.Authority) + UriUtility.ToAbsolute(path));
}
else
{
@@ -229,17 +259,26 @@ namespace Umbraco.Web.Routing
return path == "/" ? path : path.TrimEnd('/');
}
// always build absolute urls unless we really cannot
IEnumerable<Uri> AssembleUrls(IEnumerable<Uri> domainUris, string path, Uri current)
{
if (domainUris.Any())
List<Uri> uris = new List<Uri>();
if (!domainUris.Any())
{
return domainUris.Select(domainUri => new Uri(domainUri.GetLeftPart(UriPartial.Path).TrimEnd('/') + path));
// no domain was found : return an absolute or relative url
// handle vdir if any
if (current == null)
uris.Add(new Uri(UriUtility.ToAbsolute(path), UriKind.Relative));
else
uris.Add(new Uri(current.GetLeftPart(UriPartial.Authority) + UriUtility.ToAbsolute(path)));
}
else
{
// no domain was found : return a relative url, add vdir if any
return new Uri[] { new Uri(SystemDirectories.Root + path, UriKind.Relative) };
// domains were found : return -- FIXME?
uris.AddRange(domainUris.Select(domainUri => new Uri(domainUri.GetLeftPart(UriPartial.Path).TrimEnd('/') + path)));
}
return uris.Select(uri => UriUtility.UriFromUmbraco(uri));
}
Uri DomainUriAtNode(int nodeId, Uri current)
@@ -263,6 +302,28 @@ namespace Umbraco.Web.Routing
return domainAndUris.Select(d => d.Uri);
}
void ApplyHideTopLevelNodeFromPath(Core.Models.IDocument node, List<string> pathParts)
{
// in theory if hideTopLevelNodeFromPath is true, then there should be only once
// top-level node, or else domains should be assigned. but for backward compatibility
// we add this check - we look for the document matching "/" and if it's not us, then
// we do not hide the top level path
// it has to be taken care of in IPublishedContentStore.GetDocumentByRoute too so if
// "/foo" fails (looking for "/*/foo") we try also "/foo".
// this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but
// that's the way it works pre-4.10 and we try to be backward compat for the time being
if (node.Parent == null)
{
var rootNode = _publishedContentStore.GetDocumentByRoute(_umbracoContext, "/", true);
if (rootNode.Id == node.Id) // remove only if we're the default node
pathParts.RemoveAt(pathParts.Count - 1);
}
else
{
pathParts.RemoveAt(pathParts.Count - 1);
}
}
#endregion
}
}

View File

@@ -317,7 +317,7 @@ namespace Umbraco.Web
public string NiceUrlWithDomain(int nodeId)
{
var niceUrlsProvider = UmbracoContext.Current.NiceUrlProvider;
return niceUrlsProvider.GetNiceUrl(nodeId, UmbracoContext.Current.UmbracoUrl, true);
return niceUrlsProvider.GetNiceUrl(nodeId, true);
}
#endregion

View File

@@ -7,47 +7,58 @@ using umbraco;
namespace Umbraco.Web
{
static class UriUtility
public static class UriUtility
{
static readonly string _appVirtualPath;
static readonly string _appVirtualPathPrefix;
static string _appPath;
static string _appPathPrefix;
static UriUtility()
{
// Virtual path
_appVirtualPath = HttpRuntime.AppDomainAppVirtualPath ?? "/";
_appVirtualPathPrefix = _appVirtualPath;
if (_appVirtualPathPrefix == "/")
_appVirtualPathPrefix = String.Empty;
SetAppDomainAppVirtualPath(HttpRuntime.AppDomainAppVirtualPath);
}
// internal for unit testing only
internal static void SetAppDomainAppVirtualPath(string appPath)
{
_appPath = appPath ?? "/";
_appPathPrefix = _appPath;
if (_appPathPrefix == "/")
_appPathPrefix = String.Empty;
}
// will be "/" or "/foo"
public static string AppVirtualPath
public static string AppPath
{
get { return _appVirtualPath; }
get { return _appPath; }
}
// will be "" or "/foo"
public static string AppVirtualPathPrefix
public static string AppPathPrefix
{
get { return _appVirtualPathPrefix; }
get { return _appPathPrefix; }
}
public static string ToAbsolute(string url)
// adds the virtual directory if any
// see also VirtualPathUtility.ToAbsolute
// FIXME
public static string ToAbsolute(string url)
{
return ResolveUrl(url);
//return ResolveUrl(url);
url = url.TrimStart('~');
return _appPathPrefix + url;
}
public static string ToAppRelative(string url)
// strips the virtual directory if any
// see also VirtualPathUtility.ToAppRelative
public static string ToAppRelative(string virtualPath)
{
if (url.StartsWith(_appVirtualPathPrefix))
url = url.Substring(_appVirtualPathPrefix.Length);
return url;
if (virtualPath.StartsWith(_appPathPrefix))
virtualPath = virtualPath.Substring(_appPathPrefix.Length);
return virtualPath;
}
// fixme - what about vdir?
// path = path.Substring(UriUtility.AppVirtualPathPrefix.Length); // remove virtual directory
// maps an internal umbraco uri to a public uri
// ie with virtual directory, .aspx if required...
public static Uri UriFromUmbraco(Uri uri)
{
var path = uri.GetSafeAbsolutePath();
@@ -59,19 +70,19 @@ namespace Umbraco.Web
else if (UmbracoSettings.AddTrailingSlash)
path += "/";
path = ToAbsolute(path);
return uri.Rewrite(path);
}
/// <summary>
/// Converts a Uri to a path based URI that is lower cased
/// </summary>
/// <param name="uri"></param>
/// <returns></returns>
public static Uri UriToUmbraco(Uri uri)
// maps a public uri to an internal umbraco uri
// ie no virtual directory, no .aspx, lowercase...
public static Uri UriToUmbraco(Uri uri)
{
var path = uri.GetSafeAbsolutePath();
path = path.ToLower();
path = ToAppRelative(path); // strip vdir if any
//we need to check if the path is /default.aspx because this will occur when using a
//web server pre IIS 7 when requesting the root document
@@ -116,7 +127,7 @@ namespace Umbraco.Web
}
StringBuilder sbUrl = new StringBuilder();
sbUrl.Append(HttpRuntime.AppDomainAppVirtualPath);
sbUrl.Append(_appPathPrefix);
if (sbUrl.Length == 0 || sbUrl[sbUrl.Length - 1] != '/') sbUrl.Append('/');
// found question mark already? query string, do not touch!

View File

@@ -20,6 +20,7 @@ using umbraco.cms.businesslogic.web;
using umbraco.presentation;
using umbraco.cms.businesslogic.skinning;
using System.Collections.Generic;
using System.Linq;
namespace umbraco.cms.presentation
{
@@ -180,7 +181,7 @@ namespace umbraco.cms.presentation
publishProps.addProperty(ui.Text("content", "expireDate", base.getUser()), dpExpire);
// url's
updateLinks();
UpdateNiceUrls();
linkProps.addProperty(ui.Text("content", "urls", base.getUser()), l);
if (domainText.Text != "")
@@ -323,7 +324,7 @@ namespace umbraco.cms.presentation
UnPublish.Visible = true;
_documentHasPublishedVersion = _document.HasPublishedVersion();
updateLinks();
UpdateNiceUrls();
}
else
{
@@ -353,57 +354,36 @@ namespace umbraco.cms.presentation
}
private void updateLinks()
{
if (_documentHasPublishedVersion)
{
// zb-00007 #29928 : refactor
string currentLink = library.NiceUrl(_document.Id);
l.Text = "<a href=\"" + currentLink + "\" target=\"_blank\">" + currentLink + "</a>";
void UpdateNiceUrls()
{
if (!_documentHasPublishedVersion)
{
l.Text = "<i>" + ui.Text("content", "itemNotPublished", base.getUser()) + "</i>";
return;
}
// domains
domainText.Text = "";
foreach (string s in _document.Path.Split(','))
{
if (int.Parse(s) > -1)
{
cms.businesslogic.web.Document dChild = new cms.businesslogic.web.Document(int.Parse(s));
if (dChild.Published)
{
cms.businesslogic.web.Domain[] domains = cms.businesslogic.web.Domain.GetDomainsById(int.Parse(s));
if (domains.Length > 0)
{
for (int i = 0; i < domains.Length; i++)
{
string tempLink = "";
if (library.NiceUrl(int.Parse(s)) == "")
tempLink = "<em>N/A</em>";
else if (int.Parse(s) != _document.Id)
{
string tempNiceUrl = library.NiceUrl(int.Parse(s));
var niceUrlProvider = Umbraco.Web.UmbracoContext.Current.RoutingContext.NiceUrlProvider;
var url = niceUrlProvider.GetNiceUrl(_document.Id);
string niceUrl = tempNiceUrl != "/" ? currentLink.Replace(tempNiceUrl.Replace(".aspx", ""), "") : currentLink;
if (!niceUrl.StartsWith("/"))
niceUrl = "/" + niceUrl;
if (url == "#")
{
var parent = _document;
while (parent.Published && parent.ParentId > 0)
parent = new Document(_document.ParentId);
if (parent.Published)
l.Text = "<i>" + ui.Text("content", "parentNotPublished", "???", base.getUser()) + "</i>";
else
l.Text = "<i>" + ui.Text("content", "parentNotPublished", parent.Text, base.getUser()) + "</i>";
return;
}
tempLink = "http://" + domains[i].Name + niceUrl;
}
else
tempLink = "http://" + domains[i].Name;
l.Text = string.Format("<a href=\"{0}\" target=\"_blank\">{0}</a>", url);
domainText.Text += "<a href=\"" + tempLink + "\" target=\"_blank\">" + tempLink + "</a><br/>";
}
}
}
else
l.Text = "<i>" + ui.Text("content", "parentNotPublished", dChild.Text, base.getUser()) + "</i>";
}
}
}
else
l.Text = "<i>" + ui.Text("content", "itemNotPublished", base.getUser()) + "</i>";
}
var lb = new System.Text.StringBuilder();
foreach (var altUrl in niceUrlProvider.GetAllAbsoluteNiceUrls(_document.Id).Where(u => u != url))
lb.AppendFormat("<a href=\"{0}\" target=\"_blank\">{0}</a><br />", altUrl);
domainText.Text = lb.ToString();
}
/// <summary>
/// Clears the page of all controls and shows a simple message. Used if users don't have visible access to the page.