392 lines
14 KiB
C#
392 lines
14 KiB
C#
// Copyright (c) .NET Foundation. All rights reserved.
|
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Routing
|
|
{
|
|
/// <summary>
|
|
/// An implementation of <see cref="IUrlHelper"/> that contains methods to
|
|
/// build URLs for ASP.NET MVC within an application.
|
|
/// </summary>
|
|
public class UrlHelper : IUrlHelper
|
|
{
|
|
|
|
// Perf: Share the StringBuilder object across multiple calls of GenerateURL for this UrlHelper
|
|
private StringBuilder _stringBuilder;
|
|
// Perf: Reuse the RouteValueDictionary across multiple calls of Action for this UrlHelper
|
|
private readonly RouteValueDictionary _routeValueDictionary;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="UrlHelper"/> class using the specified
|
|
/// <paramref name="actionContext"/>.
|
|
/// </summary>
|
|
/// <param name="actionContext">The <see cref="Mvc.ActionContext"/> for the current request.</param>
|
|
public UrlHelper(ActionContext actionContext)
|
|
{
|
|
if (actionContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(actionContext));
|
|
}
|
|
|
|
ActionContext = actionContext;
|
|
_routeValueDictionary = new RouteValueDictionary();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public ActionContext ActionContext { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="RouteValueDictionary"/> associated with the current request.
|
|
/// </summary>
|
|
protected RouteValueDictionary AmbientValues => ActionContext.RouteData.Values;
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="Http.HttpContext"/> associated with the current request.
|
|
/// </summary>
|
|
protected HttpContext HttpContext => ActionContext.HttpContext;
|
|
|
|
/// <summary>
|
|
/// Gets the top-level <see cref="IRouter"/> associated with the current request. Generally an
|
|
/// <see cref="IRouteCollection"/> implementation.
|
|
/// </summary>
|
|
protected IRouter Router => ActionContext.RouteData.Routers[0];
|
|
|
|
/// <inheritdoc />
|
|
public virtual string Action(UrlActionContext actionContext)
|
|
{
|
|
if (actionContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(actionContext));
|
|
}
|
|
|
|
var valuesDictionary = GetValuesDictionary(actionContext.Values);
|
|
|
|
if (actionContext.Action == null)
|
|
{
|
|
object action;
|
|
if (!valuesDictionary.ContainsKey("action") &&
|
|
AmbientValues.TryGetValue("action", out action))
|
|
{
|
|
valuesDictionary["action"] = action;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
valuesDictionary["action"] = actionContext.Action;
|
|
}
|
|
|
|
if (actionContext.Controller == null)
|
|
{
|
|
object controller;
|
|
if (!valuesDictionary.ContainsKey("controller") &&
|
|
AmbientValues.TryGetValue("controller", out controller))
|
|
{
|
|
valuesDictionary["controller"] = controller;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
valuesDictionary["controller"] = actionContext.Controller;
|
|
}
|
|
|
|
var virtualPathData = GetVirtualPathData(routeName: null, values: valuesDictionary);
|
|
return GenerateUrl(actionContext.Protocol, actionContext.Host, virtualPathData, actionContext.Fragment);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual bool IsLocalUrl(string url)
|
|
{
|
|
if (string.IsNullOrEmpty(url))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Allows "/" or "/foo" but not "//" or "/\".
|
|
if (url[0] == '/')
|
|
{
|
|
// url is exactly "/"
|
|
if (url.Length == 1)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// url doesn't start with "//" or "/\"
|
|
if (url[1] != '/' && url[1] != '\\')
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Allows "~/" or "~/foo" but not "~//" or "~/\".
|
|
if (url[0] == '~' && url.Length > 1 && url[1] == '/')
|
|
{
|
|
// url is exactly "~/"
|
|
if (url.Length == 2)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// url doesn't start with "~//" or "~/\"
|
|
if (url[2] != '/' && url[2] != '\\')
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual string RouteUrl(UrlRouteContext routeContext)
|
|
{
|
|
if (routeContext == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(routeContext));
|
|
}
|
|
|
|
var valuesDictionary = routeContext.Values as RouteValueDictionary ?? GetValuesDictionary(routeContext.Values);
|
|
var virtualPathData = GetVirtualPathData(routeContext.RouteName, valuesDictionary);
|
|
return GenerateUrl(routeContext.Protocol, routeContext.Host, virtualPathData, routeContext.Fragment);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="VirtualPathData"/> for the specified <paramref name="routeName"/> and route
|
|
/// <paramref name="values"/>.
|
|
/// </summary>
|
|
/// <param name="routeName">The name of the route that is used to generate the <see cref="VirtualPathData"/>.
|
|
/// </param>
|
|
/// <param name="values">
|
|
/// The <see cref="RouteValueDictionary"/>. The <see cref="Router"/> uses these values, in combination with
|
|
/// <see cref="AmbientValues"/>, to generate the URL.
|
|
/// </param>
|
|
/// <returns>The <see cref="VirtualPathData"/>.</returns>
|
|
protected virtual VirtualPathData GetVirtualPathData(string routeName, RouteValueDictionary values)
|
|
{
|
|
var context = new VirtualPathContext(HttpContext, AmbientValues, values, routeName);
|
|
return Router.GetVirtualPath(context);
|
|
}
|
|
|
|
// Internal for unit testing.
|
|
internal void AppendPathAndFragment(StringBuilder builder, VirtualPathData pathData, string fragment)
|
|
{
|
|
var pathBase = HttpContext.Request.PathBase;
|
|
|
|
if (!pathBase.HasValue)
|
|
{
|
|
if (pathData.VirtualPath.Length == 0)
|
|
{
|
|
builder.Append("/");
|
|
}
|
|
else
|
|
{
|
|
if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
|
|
{
|
|
builder.Append("/");
|
|
}
|
|
|
|
builder.Append(pathData.VirtualPath);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (pathData.VirtualPath.Length == 0)
|
|
{
|
|
builder.Append(pathBase.Value);
|
|
}
|
|
else
|
|
{
|
|
builder.Append(pathBase.Value);
|
|
|
|
if (pathBase.Value.EndsWith("/", StringComparison.Ordinal))
|
|
{
|
|
builder.Length--;
|
|
}
|
|
|
|
if (!pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
|
|
{
|
|
builder.Append("/");
|
|
}
|
|
|
|
builder.Append(pathData.VirtualPath);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(fragment))
|
|
{
|
|
builder.Append("#").Append(fragment);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual string Content(string contentPath)
|
|
{
|
|
if (string.IsNullOrEmpty(contentPath))
|
|
{
|
|
return null;
|
|
}
|
|
else if (contentPath[0] == '~')
|
|
{
|
|
var segment = new PathString(contentPath.Substring(1));
|
|
var applicationPath = HttpContext.Request.PathBase;
|
|
|
|
return applicationPath.Add(segment).Value;
|
|
}
|
|
|
|
return contentPath;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public virtual string Link(string routeName, object values)
|
|
{
|
|
return RouteUrl(new UrlRouteContext()
|
|
{
|
|
RouteName = routeName,
|
|
Values = values,
|
|
Protocol = HttpContext.Request.Scheme,
|
|
Host = HttpContext.Request.Host.ToUriComponent()
|
|
});
|
|
}
|
|
|
|
private RouteValueDictionary GetValuesDictionary(object values)
|
|
{
|
|
// Perf: RouteValueDictionary can be cast to IDictionary<string, object>, but it is
|
|
// special cased to avoid allocating boxed Enumerator.
|
|
var routeValuesDictionary = values as RouteValueDictionary;
|
|
if (routeValuesDictionary != null)
|
|
{
|
|
_routeValueDictionary.Clear();
|
|
foreach (var kvp in routeValuesDictionary)
|
|
{
|
|
_routeValueDictionary.Add(kvp.Key, kvp.Value);
|
|
}
|
|
|
|
return _routeValueDictionary;
|
|
}
|
|
|
|
var dictionaryValues = values as IDictionary<string, object>;
|
|
if (dictionaryValues != null)
|
|
{
|
|
_routeValueDictionary.Clear();
|
|
foreach (var kvp in dictionaryValues)
|
|
{
|
|
_routeValueDictionary.Add(kvp.Key, kvp.Value);
|
|
}
|
|
|
|
return _routeValueDictionary;
|
|
}
|
|
|
|
return new RouteValueDictionary(values);
|
|
}
|
|
|
|
private StringBuilder GetStringBuilder()
|
|
{
|
|
if(_stringBuilder == null)
|
|
{
|
|
_stringBuilder = new StringBuilder();
|
|
}
|
|
|
|
return _stringBuilder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates the URL using the specified components.
|
|
/// </summary>
|
|
/// <param name="protocol">The protocol for the URL, such as "http" or "https".</param>
|
|
/// <param name="host">The host name for the URL.</param>
|
|
/// <param name="pathData">The <see cref="VirtualPathData"/>.</param>
|
|
/// <param name="fragment">The fragment for the URL.</param>
|
|
/// <returns>The generated URL.</returns>
|
|
protected virtual string GenerateUrl(string protocol, string host, VirtualPathData pathData, string fragment)
|
|
{
|
|
if (pathData == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// VirtualPathData.VirtualPath returns string.Empty instead of null.
|
|
Debug.Assert(pathData.VirtualPath != null);
|
|
|
|
// Perf: In most of the common cases, GenerateUrl is called with a null protocol, host and fragment.
|
|
// In such cases, we might not need to build any URL as the url generated is mostly same as the virtual path available in pathData.
|
|
// For such common cases, this FastGenerateUrl method saves a string allocation per GenerateUrl call.
|
|
string url;
|
|
if (TryFastGenerateUrl(protocol, host, pathData, fragment, out url))
|
|
{
|
|
return url;
|
|
}
|
|
|
|
var builder = GetStringBuilder();
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(protocol) && string.IsNullOrEmpty(host))
|
|
{
|
|
AppendPathAndFragment(builder, pathData, fragment);
|
|
// We're returning a partial URL (just path + query + fragment), but we still want it to be rooted.
|
|
if (builder.Length == 0 || builder[0] != '/')
|
|
{
|
|
builder.Insert(0, '/');
|
|
}
|
|
}
|
|
else
|
|
{
|
|
protocol = string.IsNullOrEmpty(protocol) ? "http" : protocol;
|
|
builder.Append(protocol);
|
|
|
|
builder.Append("://");
|
|
|
|
host = string.IsNullOrEmpty(host) ? HttpContext.Request.Host.Value : host;
|
|
builder.Append(host);
|
|
AppendPathAndFragment(builder, pathData, fragment);
|
|
}
|
|
|
|
var path = builder.ToString();
|
|
return path;
|
|
}
|
|
finally
|
|
{
|
|
// Clear the StringBuilder so that it can reused for the next call.
|
|
builder.Clear();
|
|
}
|
|
}
|
|
|
|
private bool TryFastGenerateUrl(
|
|
string protocol,
|
|
string host,
|
|
VirtualPathData pathData,
|
|
string fragment,
|
|
out string url)
|
|
{
|
|
var pathBase = HttpContext.Request.PathBase;
|
|
url = null;
|
|
|
|
if (string.IsNullOrEmpty(protocol)
|
|
&& string.IsNullOrEmpty(host)
|
|
&& string.IsNullOrEmpty(fragment)
|
|
&& !pathBase.HasValue)
|
|
{
|
|
if (pathData.VirtualPath.Length == 0)
|
|
{
|
|
url = "/";
|
|
return true;
|
|
}
|
|
else if (pathData.VirtualPath.StartsWith("/", StringComparison.Ordinal))
|
|
{
|
|
url = pathData.VirtualPath;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
} |