// 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; } } }