// 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
{
///
/// An implementation of that contains methods to
/// build URLs for ASP.NET MVC within an application.
///
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;
///
/// Initializes a new instance of the class using the specified
/// .
///
/// The for the current request.
public UrlHelper(ActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
ActionContext = actionContext;
_routeValueDictionary = new RouteValueDictionary();
}
///
public ActionContext ActionContext { get; }
///
/// Gets the associated with the current request.
///
protected RouteValueDictionary AmbientValues => ActionContext.RouteData.Values;
///
/// Gets the associated with the current request.
///
protected HttpContext HttpContext => ActionContext.HttpContext;
///
/// Gets the top-level associated with the current request. Generally an
/// implementation.
///
protected IRouter Router => ActionContext.RouteData.Routers[0];
///
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);
}
///
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;
}
///
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);
}
///
/// Gets the for the specified and route
/// .
///
/// The name of the route that is used to generate the .
///
///
/// The . The uses these values, in combination with
/// , to generate the URL.
///
/// The .
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);
}
}
///
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;
}
///
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, 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;
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;
}
///
/// Generates the URL using the specified components.
///
/// The protocol for the URL, such as "http" or "https".
/// The host name for the URL.
/// The .
/// The fragment for the URL.
/// The generated URL.
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;
}
}
}