using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.StaticFiles; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Nop.Core.Http; namespace Nop.Core; /// /// Represents a web helper /// public partial class WebHelper : IWebHelper { #region Fields protected readonly IActionContextAccessor _actionContextAccessor; protected readonly IHostApplicationLifetime _hostApplicationLifetime; protected readonly IHttpContextAccessor _httpContextAccessor; protected readonly IUrlHelperFactory _urlHelperFactory; protected readonly Lazy _storeContext; #endregion #region Ctor public WebHelper(IActionContextAccessor actionContextAccessor, IHostApplicationLifetime hostApplicationLifetime, IHttpContextAccessor httpContextAccessor, IUrlHelperFactory urlHelperFactory, Lazy storeContext) { _actionContextAccessor = actionContextAccessor; _hostApplicationLifetime = hostApplicationLifetime; _httpContextAccessor = httpContextAccessor; _urlHelperFactory = urlHelperFactory; _storeContext = storeContext; } #endregion #region Utilities /// /// Check whether current HTTP request is available /// /// True if available; otherwise false protected virtual bool IsRequestAvailable() { if (_httpContextAccessor?.HttpContext == null) return false; try { if (_httpContextAccessor.HttpContext?.Request == null) return false; } catch (Exception) { return false; } return true; } /// /// Is IP address specified /// /// IP address /// Result protected virtual bool IsIpAddressSet(IPAddress address) { var rez = address != null && address.ToString() != IPAddress.IPv6Loopback.ToString(); return rez; } #endregion #region Methods /// /// Get URL referrer if exists /// /// URL referrer public virtual string GetUrlReferrer() { if (!IsRequestAvailable()) return string.Empty; //URL referrer is null in some case (for example, in IE 8) return _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Referer]; } /// /// Get IP address from HTTP context /// /// String of IP address public virtual string GetCurrentIpAddress() { if (!IsRequestAvailable() || _httpContextAccessor.HttpContext!.Connection.RemoteIpAddress is not { } remoteIp) return string.Empty; return (remoteIp.Equals(IPAddress.IPv6Loopback) ? IPAddress.Loopback : remoteIp).ToString(); } /// /// Gets this page URL /// /// Value indicating whether to include query strings /// Value indicating whether to get SSL secured page URL. Pass null to determine automatically /// Value indicating whether to lowercase URL /// Page URL public virtual string GetThisPageUrl(bool includeQueryString, bool? useSsl = null, bool lowercaseUrl = false) { if (!IsRequestAvailable()) return string.Empty; //get store location var storeLocation = GetStoreLocation(useSsl ?? IsCurrentConnectionSecured()); //add local path to the URL var pageUrl = $"{storeLocation.TrimEnd('/')}{_httpContextAccessor.HttpContext.Request.Path}"; //add query string to the URL if (includeQueryString) pageUrl = $"{pageUrl}{_httpContextAccessor.HttpContext.Request.QueryString}"; //whether to convert the URL to lower case if (lowercaseUrl) pageUrl = pageUrl.ToLowerInvariant(); return pageUrl; } /// /// Gets a value indicating whether current connection is secured /// /// True if it's secured, otherwise false public virtual bool IsCurrentConnectionSecured() { if (!IsRequestAvailable()) return false; return _httpContextAccessor.HttpContext.Request.IsHttps; } /// /// Gets store host location /// /// Whether to get SSL secured URL /// Store host location public virtual string GetStoreHost(bool useSsl) { if (!IsRequestAvailable()) return string.Empty; //try to get host from the request HOST header var hostHeader = _httpContextAccessor.HttpContext.Request.Headers[HeaderNames.Host]; if (StringValues.IsNullOrEmpty(hostHeader)) return string.Empty; //add scheme to the URL var storeHost = $"{(useSsl ? Uri.UriSchemeHttps : Uri.UriSchemeHttp)}{Uri.SchemeDelimiter}{hostHeader.FirstOrDefault()}"; //ensure that host is ended with slash storeHost = $"{storeHost.TrimEnd('/')}/"; return storeHost; } /// /// Gets store location /// /// Whether to get SSL secured URL; pass null to determine automatically /// Store location public virtual string GetStoreLocation(bool? useSsl = null) { var storeLocation = string.Empty; //get store host var storeHost = GetStoreHost(useSsl ?? IsCurrentConnectionSecured()); if (!string.IsNullOrEmpty(storeHost)) { //add application path base if exists storeLocation = IsRequestAvailable() ? $"{storeHost.TrimEnd('/')}{_httpContextAccessor.HttpContext.Request.PathBase}" : storeHost; } //if host is empty (it is possible only when HttpContext is not available), use URL of a store entity configured in admin area if (string.IsNullOrEmpty(storeHost)) storeLocation = _storeContext.Value.GetCurrentStore()?.Url ?? throw new Exception("Current store cannot be loaded"); //ensure that URL is ended with slash storeLocation = $"{storeLocation.TrimEnd('/')}/"; return storeLocation; } /// /// Returns true if the requested resource is one of the typical resources that needn't be processed by the cms engine. /// /// True if the request targets a static resource file. public virtual bool IsStaticResource() { if (!IsRequestAvailable()) return false; string path = _httpContextAccessor.HttpContext.Request.Path; //a little workaround. FileExtensionContentTypeProvider contains most of static file extensions. So we can use it //source: https://github.com/aspnet/StaticFiles/blob/dev/src/Microsoft.AspNetCore.StaticFiles/FileExtensionContentTypeProvider.cs //if it can return content type, then it's a static file var contentTypeProvider = new FileExtensionContentTypeProvider(); return contentTypeProvider.TryGetContentType(path, out var _); } /// /// Modify query string of the URL /// /// Url to modify /// Query parameter key to add /// Query parameter values to add /// New URL with passed query parameter public virtual string ModifyQueryString(string url, string key, params string[] values) { if (string.IsNullOrEmpty(url)) return string.Empty; if (string.IsNullOrEmpty(key)) return url; //prepare URI object var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var isLocalUrl = urlHelper.IsLocalUrl(url); var uriStr = url; if (isLocalUrl) { var pathBase = _httpContextAccessor.HttpContext.Request.PathBase; uriStr = $"{GetStoreLocation().TrimEnd('/')}{(url.StartsWith(pathBase) ? url.Replace(pathBase, "") : url)}"; } var uri = new Uri(uriStr, UriKind.Absolute); //get current query parameters var queryParameters = QueryHelpers.ParseQuery(uri.Query); //and add passed one queryParameters[key] = string.Join(",", values); //add only first value //two the same query parameters? theoretically it's not possible. //but MVC has some ugly implementation for checkboxes and we can have two values //find more info here: http://www.mindstorminteractive.com/topics/jquery-fix-asp-net-mvc-checkbox-truefalse-value/ //we do this validation just to ensure that the first one is not overridden var queryBuilder = new QueryBuilder(queryParameters .ToDictionary(parameter => parameter.Key, parameter => parameter.Value.FirstOrDefault()?.ToString() ?? string.Empty)); //create new URL with passed query parameters url = $"{(isLocalUrl ? uri.LocalPath : uri.GetLeftPart(UriPartial.Path))}{queryBuilder.ToQueryString()}{uri.Fragment}"; return url; } /// /// Remove query parameter from the URL /// /// Url to modify /// Query parameter key to remove /// Query parameter value to remove; pass null to remove all query parameters with the specified key /// New URL without passed query parameter public virtual string RemoveQueryString(string url, string key, string value = null) { if (string.IsNullOrEmpty(url)) return string.Empty; if (string.IsNullOrEmpty(key)) return url; //prepare URI object var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var isLocalUrl = urlHelper.IsLocalUrl(url); var uri = new Uri(isLocalUrl ? $"{GetStoreLocation().TrimEnd('/')}{url}" : url, UriKind.Absolute); //get current query parameters var queryParameters = QueryHelpers.ParseQuery(uri.Query) .SelectMany(parameter => parameter.Value, (parameter, queryValue) => new KeyValuePair(parameter.Key, queryValue)) .ToList(); if (!string.IsNullOrEmpty(value)) { //remove a specific query parameter value if it's passed queryParameters.RemoveAll(parameter => parameter.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase) && parameter.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); } else { //or remove query parameter by the key queryParameters.RemoveAll(parameter => parameter.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)); } var queryBuilder = new QueryBuilder(queryParameters); //create new URL without passed query parameters url = $"{(isLocalUrl ? uri.LocalPath : uri.GetLeftPart(UriPartial.Path))}{queryBuilder.ToQueryString()}{uri.Fragment}"; return url; } /// /// Gets query string value by name /// /// Returned value type /// Query parameter name /// Query string value public virtual T QueryString(string name) { if (!IsRequestAvailable()) return default; if (StringValues.IsNullOrEmpty(_httpContextAccessor.HttpContext.Request.Query[name])) return default; return CommonHelper.To(_httpContextAccessor.HttpContext.Request.Query[name].ToString()); } /// /// Restart application domain /// public virtual void RestartAppDomain() { _hostApplicationLifetime.StopApplication(); } /// /// Gets a value that indicates whether the client is being redirected to a new location /// public virtual bool IsRequestBeingRedirected { get { var response = _httpContextAccessor.HttpContext.Response; //ASP.NET 4 style - return response.IsRequestBeingRedirected; int[] redirectionStatusCodes = [StatusCodes.Status301MovedPermanently, StatusCodes.Status302Found]; return redirectionStatusCodes.Contains(response.StatusCode); } } /// /// Gets or sets a value that indicates whether the client is being redirected to a new location using POST /// public virtual bool IsPostBeingDone { get { if (_httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem] == null) return false; return Convert.ToBoolean(_httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem]); } set => _httpContextAccessor.HttpContext.Items[NopHttpDefaults.IsPostBeingDoneRequestItem] = value; } /// /// Gets current HTTP request protocol /// public virtual string GetCurrentRequestProtocol() { return IsCurrentConnectionSecured() ? Uri.UriSchemeHttps : Uri.UriSchemeHttp; } /// /// Gets whether the specified HTTP request URI references the local host. /// /// HTTP request /// True, if HTTP request URI references to the local host public virtual bool IsLocalRequest(HttpRequest req) { //source: https://stackoverflow.com/a/41242493/7860424 var connection = req.HttpContext.Connection; if (IsIpAddressSet(connection.RemoteIpAddress)) { //We have a remote address set up return IsIpAddressSet(connection.LocalIpAddress) //Is local is same as remote, then we are local ? connection.RemoteIpAddress.Equals(connection.LocalIpAddress) //else we are remote if the remote IP address is not a loopback address : IPAddress.IsLoopback(connection.RemoteIpAddress); } return true; } /// /// Get the raw path and full query of request /// /// HTTP request /// Raw URL public virtual string GetRawUrl(HttpRequest request) { //first try to get the raw target from request feature //note: value has not been UrlDecoded var rawUrl = request.HttpContext.Features.Get()?.RawTarget; //or compose raw URL manually if (string.IsNullOrEmpty(rawUrl)) rawUrl = $"{request.PathBase}{request.Path}{request.QueryString}"; return rawUrl; } /// /// Gets whether the request is made with AJAX /// /// HTTP request /// Result public virtual bool IsAjaxRequest(HttpRequest request) { ArgumentNullException.ThrowIfNull(request); if (request.Headers == null) return false; return request.Headers.XRequestedWith == "XMLHttpRequest"; } #endregion }