using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Hosting; using Nop.Core; using Nop.Core.Configuration; using Nop.Core.Domain.Seo; using Nop.Services.Localization; using Nop.Web.Framework.Mvc.Routing; using Nop.Web.Framework.WebOptimizer; namespace Nop.Web.Framework.UI; /// /// Represents the HTML helper implementation /// public partial class NopHtmlHelper : INopHtmlHelper { #region Fields protected readonly AppSettings _appSettings; protected readonly HtmlEncoder _htmlEncoder; protected readonly IActionContextAccessor _actionContextAccessor; protected readonly IHtmlHelper _htmlHelper; protected readonly INopAssetHelper _bundleHelper; protected readonly Lazy _localizationService; protected readonly IStoreContext _storeContext; protected readonly IUrlHelperFactory _urlHelperFactory; protected readonly IWebHostEnvironment _webHostEnvironment; protected readonly SeoSettings _seoSettings; protected readonly Dictionary> _scriptParts = new(); protected readonly Dictionary> _inlineScriptParts = new(); protected readonly List _cssParts = new(); protected readonly List _canonicalUrlParts = new(); protected readonly List _headCustomParts = new(); protected readonly List _metaDescriptionParts = new(); protected readonly List _metaKeywordParts = new(); protected readonly List _pageCssClassParts = new(); protected readonly List _titleParts = new(); protected string _activeAdminMenuSystemName; protected string _editPageUrl; #endregion #region Ctor public NopHtmlHelper(AppSettings appSettings, HtmlEncoder htmlEncoder, IActionContextAccessor actionContextAccessor, IHtmlHelper htmlHelper, INopAssetHelper bundleHelper, Lazy localizationService, IStoreContext storeContext, IUrlHelperFactory urlHelperFactory, IWebHostEnvironment webHostEnvironment, SeoSettings seoSettings) { _appSettings = appSettings; _htmlEncoder = htmlEncoder; _actionContextAccessor = actionContextAccessor; _htmlHelper = htmlHelper; _bundleHelper = bundleHelper; _localizationService = localizationService; _storeContext = storeContext; _urlHelperFactory = urlHelperFactory; _webHostEnvironment = webHostEnvironment; _seoSettings = seoSettings; } #endregion #region Utilities protected static string GetAssetKey(string[] keys, string suffix) { ArgumentNullException.ThrowIfNull(keys?.Length > 0 ? keys : null, nameof(keys)); var hashInput = string.Join(',', keys); var input = MD5.HashData(Encoding.Unicode.GetBytes(hashInput)); var key = string.Concat(WebEncoders.Base64UrlEncode(input)); if (!string.IsNullOrEmpty(suffix)) key += suffix; return key.ToLower(); } #endregion #region Methods /// /// Add title element to the ]]> /// /// Title part public virtual void AddTitleParts(string part) { if (string.IsNullOrEmpty(part)) return; _titleParts.Add(part); } /// /// Append title element to the ]]> /// /// Title part public virtual void AppendTitleParts(string part) { if (string.IsNullOrEmpty(part)) return; _titleParts.Insert(0, part); } /// /// Generate all title parts /// /// A value indicating whether to insert a default title /// Title part /// A task that represents the asynchronous operation /// The task result contains generated HTML string public virtual async Task GenerateTitleAsync(bool addDefaultTitle = true, string part = "") { AppendTitleParts(part); var store = await _storeContext.GetCurrentStoreAsync(); var defaultTitle = await _localizationService.Value.GetLocalizedAsync(store, s => s.DefaultTitle); var specificTitle = string.Join(_seoSettings.PageTitleSeparator, _titleParts.AsEnumerable().Reverse().ToArray()); string result; if (!string.IsNullOrEmpty(specificTitle)) { if (addDefaultTitle) //store name + page title switch (_seoSettings.PageTitleSeoAdjustment) { case PageTitleSeoAdjustment.PagenameAfterStorename: { result = string.Join(_seoSettings.PageTitleSeparator, defaultTitle, specificTitle); } break; case PageTitleSeoAdjustment.StorenameAfterPagename: default: { result = string.Join(_seoSettings.PageTitleSeparator, specificTitle, defaultTitle); } break; } else //page title only result = specificTitle; } else //store name only result = defaultTitle; return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty)); } /// /// Add meta description element to the ]]> /// /// Meta description part public virtual void AddMetaDescriptionParts(string part) { if (string.IsNullOrEmpty(part)) return; _metaDescriptionParts.Add(part); } /// /// Append meta description element to the ]]> /// /// Meta description part public virtual void AppendMetaDescriptionParts(string part) { if (string.IsNullOrEmpty(part)) return; _metaDescriptionParts.Insert(0, part); } /// /// Generate all description parts /// /// Meta description part /// A task that represents the asynchronous operation /// The task result contains generated HTML string public virtual async Task GenerateMetaDescriptionAsync(string part = "") { AppendMetaDescriptionParts(part); var metaDescription = string.Join(", ", _metaDescriptionParts.AsEnumerable().Reverse().ToArray()); var result = !string.IsNullOrEmpty(metaDescription) ? metaDescription : await _localizationService.Value.GetLocalizedAsync(await _storeContext.GetCurrentStoreAsync(), s => s.DefaultMetaDescription); return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty)); } /// /// Add meta keyword element to the ]]> /// /// Meta keyword part public virtual void AddMetaKeywordParts(string part) { if (string.IsNullOrEmpty(part)) return; _metaKeywordParts.Add(part); } /// /// Append meta keyword element to the ]]> /// /// Meta keyword part public virtual void AppendMetaKeywordParts(string part) { if (string.IsNullOrEmpty(part)) return; _metaKeywordParts.Insert(0, part); } /// /// Generate all keyword parts /// /// Meta keyword part /// A task that represents the asynchronous operation /// The task result contains generated HTML string public virtual async Task GenerateMetaKeywordsAsync(string part = "") { AppendMetaKeywordParts(part); var metaKeyword = string.Join(", ", _metaKeywordParts.AsEnumerable().Reverse().ToArray()); var result = !string.IsNullOrEmpty(metaKeyword) ? metaKeyword : await _localizationService.Value.GetLocalizedAsync(await _storeContext.GetCurrentStoreAsync(), s => s.DefaultMetaKeywords); return new HtmlString(_htmlEncoder.Encode(result ?? string.Empty)); } /// /// Add script element /// /// A location of the script element /// Script path (minified version) /// Script path (full debug version). If empty, then minified version will be used /// A value indicating whether to exclude this script from bundling public virtual void AddScriptParts(ResourceLocation location, string src, string debugSrc = "", bool excludeFromBundle = false) { if (!_scriptParts.ContainsKey(location)) _scriptParts.Add(location, new List()); if (string.IsNullOrEmpty(src)) return; if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment()) src = debugSrc; ArgumentNullException.ThrowIfNull(_actionContextAccessor.ActionContext); var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase; var isLocal = urlHelper.IsLocalUrl(src); _scriptParts[location].Add(new ScriptReferenceMeta { ExcludeFromBundle = excludeFromBundle, IsLocal = isLocal, Src = isLocal ? urlHelper.Content(src).RemoveApplicationPathFromRawUrl(pathBase) : src }); } /// /// Append script element /// /// A location of the script element /// Script path (minified version) /// Script path (full debug version). If empty, then minified version will be used /// A value indicating whether to exclude this script from bundling public virtual void AppendScriptParts(ResourceLocation location, string src, string debugSrc = "", bool excludeFromBundle = false) { if (!_scriptParts.ContainsKey(location)) _scriptParts.Add(location, new List()); if (string.IsNullOrEmpty(src)) return; if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment()) src = debugSrc; ArgumentNullException.ThrowIfNull(_actionContextAccessor.ActionContext); var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase; var isLocal = urlHelper.IsLocalUrl(src); _scriptParts[location].Insert(0, new ScriptReferenceMeta { ExcludeFromBundle = excludeFromBundle, IsLocal = isLocal, Src = isLocal ? urlHelper.Content(src).RemoveApplicationPathFromRawUrl(pathBase) : src }); } /// /// Generate all script parts /// /// A location of the script element /// Generated HTML string public virtual IHtmlContent GenerateScripts(ResourceLocation location) { if (!_scriptParts.TryGetValue(location, out var value) || value == null) return HtmlString.Empty; if (!_scriptParts.Any()) return HtmlString.Empty; var result = new StringBuilder(); var woConfig = _appSettings.Get(); var pathBase = _actionContextAccessor.ActionContext?.HttpContext.Request.PathBase ?? PathString.Empty; if (woConfig.EnableJavaScriptBundling && value.Any(item => !item.ExcludeFromBundle)) { var sources = value.Where(item => !item.ExcludeFromBundle && item.IsLocal) .Select(item => item.Src) .Distinct().ToArray(); var bundleKey = $"/js/{GetAssetKey(sources, woConfig.JavaScriptBundleSuffix)}.js"; var bundleAsset = _bundleHelper.GetOrCreateJavaScriptAsset(bundleKey, sources); var route = _bundleHelper.CacheBusting(bundleAsset); result.AppendFormat("", MimeTypes.TextJavascript, pathBase, route); } var scripts = value.Where(item => !woConfig.EnableJavaScriptBundling || item.ExcludeFromBundle || !item.IsLocal) .Distinct(); foreach (var item in scripts) { if (!item.IsLocal) { result.AppendFormat("", MimeTypes.TextJavascript, item.Src); result.Append(Environment.NewLine); continue; } var asset = _bundleHelper.GetOrCreateJavaScriptAsset(item.Src); var route = _bundleHelper.CacheBusting(asset); result.AppendFormat("", MimeTypes.TextJavascript, pathBase, route); result.Append(Environment.NewLine); } return new HtmlString(result.ToString()); } /// /// Add inline script element /// /// A location of the script element /// Script public virtual void AddInlineScriptParts(ResourceLocation location, string script) { if (!_inlineScriptParts.ContainsKey(location)) _inlineScriptParts.Add(location, new()); if (string.IsNullOrEmpty(script)) return; if (_inlineScriptParts[location].Contains(script)) return; _inlineScriptParts[location].Add(script); } /// /// Append inline script element /// /// A location of the script element /// Script public virtual void AppendInlineScriptParts(ResourceLocation location, string script) { if (!_inlineScriptParts.ContainsKey(location)) _inlineScriptParts.Add(location, new()); if (string.IsNullOrEmpty(script)) return; if (_inlineScriptParts[location].Contains(script)) return; _inlineScriptParts[location].Insert(0, script); } /// /// Generate all inline script parts /// /// A location of the script element /// Generated HTML string public virtual IHtmlContent GenerateInlineScripts(ResourceLocation location) { if (!_inlineScriptParts.TryGetValue(location, out var value) || value == null) return HtmlString.Empty; if (!_inlineScriptParts.Any()) return HtmlString.Empty; var result = new StringBuilder(); foreach (var item in value) { result.Append(item); result.Append(Environment.NewLine); } return new HtmlString(result.ToString()); } /// /// Add CSS element /// /// Script path (minified version) /// Script path (full debug version). If empty, then minified version will be used /// A value indicating whether to exclude this style sheet from bundling public virtual void AddCssFileParts(string src, string debugSrc = "", bool excludeFromBundle = false) { if (string.IsNullOrEmpty(src)) return; if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment()) src = debugSrc; ArgumentNullException.ThrowIfNull(_actionContextAccessor.ActionContext); var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase; var isLocal = urlHelper.IsLocalUrl(src); _cssParts.Add(new CssReferenceMeta { ExcludeFromBundle = excludeFromBundle, IsLocal = isLocal, Src = isLocal ? urlHelper.Content(src).RemoveApplicationPathFromRawUrl(pathBase) : src }); } /// /// Append CSS element /// /// Script path (minified version) /// Script path (full debug version). If empty, then minified version will be used /// A value indicating whether to exclude this style sheet from bundling public virtual void AppendCssFileParts(string src, string debugSrc = "", bool excludeFromBundle = false) { if (string.IsNullOrEmpty(src)) return; if (!string.IsNullOrEmpty(debugSrc) && _webHostEnvironment.IsDevelopment()) src = debugSrc; ArgumentNullException.ThrowIfNull(_actionContextAccessor.ActionContext); var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext); var pathBase = _actionContextAccessor.ActionContext.HttpContext.Request.PathBase; var isLocal = urlHelper.IsLocalUrl(src); _cssParts.Insert(0, new CssReferenceMeta { ExcludeFromBundle = excludeFromBundle, IsLocal = isLocal, Src = isLocal ? urlHelper.Content(src).RemoveApplicationPathFromRawUrl(pathBase) : src }); } /// /// Generate all CSS parts /// /// Generated HTML string public virtual IHtmlContent GenerateCssFiles() { if (!_cssParts.Any()) return HtmlString.Empty; ArgumentNullException.ThrowIfNull(_actionContextAccessor.ActionContext); var result = new StringBuilder(); var woConfig = _appSettings.Get(); var pathBase = _actionContextAccessor.ActionContext?.HttpContext.Request.PathBase ?? PathString.Empty; if (woConfig.EnableCssBundling && _cssParts.Any(item => !item.ExcludeFromBundle)) { var bundleSuffix = woConfig.CssBundleSuffix; if (CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft) bundleSuffix += ".rtl"; var sources = _cssParts .Where(item => !item.ExcludeFromBundle && item.IsLocal) .Distinct() //remove the application path from the generated URL if exists .Select(item => item.Src).ToArray(); var bundleKey = $"/css/{GetAssetKey(sources, bundleSuffix)}.css"; var bundleAsset = _bundleHelper.GetOrCreateCssAsset(bundleKey, sources); var route = _bundleHelper.CacheBusting(bundleAsset); result.AppendFormat("", MimeTypes.TextCss, pathBase, route); } var styles = _cssParts .Where(item => !woConfig.EnableCssBundling || item.ExcludeFromBundle || !item.IsLocal) .Distinct(); foreach (var item in styles) { if (!item.IsLocal) { result.AppendFormat("", MimeTypes.TextCss, item.Src); result.Append(Environment.NewLine); continue; } var asset = _bundleHelper.GetOrCreateCssAsset(item.Src); var route = _bundleHelper.CacheBusting(asset); result.AppendFormat("", MimeTypes.TextCss, pathBase, route); result.AppendLine(); } return new HtmlString(result.ToString()); } /// /// Add canonical URL element to the ]]> /// /// Canonical URL part /// Whether to use canonical URLs with query string parameters public virtual void AddCanonicalUrlParts(string part, bool withQueryString = false) { if (string.IsNullOrEmpty(part)) return; if (withQueryString) { //add ordered query string parameters var queryParameters = _actionContextAccessor.ActionContext.HttpContext.Request.Query.OrderBy(parameter => parameter.Key) .ToDictionary(parameter => parameter.Key, parameter => parameter.Value.ToString()); part = QueryHelpers.AddQueryString(part, queryParameters); } _canonicalUrlParts.Add(part); } /// /// Append canonical URL element to the ]]> /// /// Canonical URL part public virtual void AppendCanonicalUrlParts(string part) { if (string.IsNullOrEmpty(part)) return; _canonicalUrlParts.Insert(0, part); } /// /// Generate all canonical URL parts /// /// Generated HTML string public virtual IHtmlContent GenerateCanonicalUrls() { var result = new StringBuilder(); foreach (var canonicalUrl in _canonicalUrlParts) { result.AppendFormat("", canonicalUrl); result.Append(Environment.NewLine); } return new HtmlString(result.ToString()); } /// /// Add any custom element to the ]]> element /// /// The entire element. For example, ]]> public virtual void AddHeadCustomParts(string part) { if (string.IsNullOrEmpty(part)) return; _headCustomParts.Add(part); } /// /// Append any custom element to the ]]> element /// /// The entire element. For example, ]]> public virtual void AppendHeadCustomParts(string part) { if (string.IsNullOrEmpty(part)) return; _headCustomParts.Insert(0, part); } /// /// Generate all custom elements /// /// Generated HTML string public virtual IHtmlContent GenerateHeadCustom() { //use only distinct rows var distinctParts = _headCustomParts.Distinct().ToList(); if (!distinctParts.Any()) return HtmlString.Empty; var result = new StringBuilder(); foreach (var path in distinctParts) { result.Append(path); result.Append(Environment.NewLine); } return new HtmlString(result.ToString()); } /// /// Add CSS class to the ]]> element /// /// CSS class public virtual void AddPageCssClassParts(string part) { if (string.IsNullOrEmpty(part)) return; _pageCssClassParts.Add(part); } /// /// Append CSS class to the ]]> element /// /// CSS class public virtual void AppendPageCssClassParts(string part) { if (string.IsNullOrEmpty(part)) return; _pageCssClassParts.Insert(0, part); } /// /// Generate all title parts /// /// CSS class /// Generated string public virtual string GeneratePageCssClasses(string part = "") { AppendPageCssClassParts(part); var result = string.Join(" ", _pageCssClassParts.AsEnumerable().Reverse().ToArray()); if (string.IsNullOrEmpty(result)) return string.Empty; return _htmlEncoder.Encode(result); } /// /// Specify "edit page" URL /// /// URL public virtual void AddEditPageUrl(string url) { _editPageUrl = url; } /// /// Get "edit page" URL /// /// URL public virtual string GetEditPageUrl() { return _editPageUrl; } /// /// Specify system name of admin menu item that should be selected (expanded) /// /// System name public virtual void SetActiveMenuItemSystemName(string systemName) { _activeAdminMenuSystemName = systemName; } /// /// Get system name of admin menu item that should be selected (expanded) /// /// System name public virtual string GetActiveMenuItemSystemName() { return _activeAdminMenuSystemName; } /// /// Get the route name associated with the request rendering this page /// /// A value indicating whether to build the name using engine information unless otherwise specified /// Route name public virtual string GetRouteName(bool handleDefaultRoutes = false) { var actionContext = _actionContextAccessor.ActionContext; if (actionContext is null) return string.Empty; var httpContext = actionContext.HttpContext; var routeName = httpContext.GetEndpoint()?.Metadata.GetMetadata()?.RouteName ?? string.Empty; if (!string.IsNullOrEmpty(routeName) && routeName != "areaRoute") return routeName; //then try to get a generic one (actually it's an action name, not the route) if (httpContext.GetRouteValue(NopRoutingDefaults.RouteValue.SeName) is not null && httpContext.GetRouteValue(NopRoutingDefaults.RouteValue.Action) is string actionName) return actionName; if (handleDefaultRoutes) return actionContext.ActionDescriptor switch { ControllerActionDescriptor controllerAction => string.Concat(controllerAction.ControllerName, controllerAction.ActionName), CompiledPageActionDescriptor compiledPage => string.Concat(compiledPage.AreaName, compiledPage.ViewEnginePath.Replace("/", "")), PageActionDescriptor pageAction => string.Concat(pageAction.AreaName, pageAction.ViewEnginePath.Replace("/", "")), _ => actionContext.ActionDescriptor.DisplayName?.Replace("/", "") ?? string.Empty }; return routeName; } /// /// Add JSON-LD to the ]]> element /// /// The JSON-LD serialized model> public virtual void AddJsonLdParts(string jsonLd) { if(_seoSettings.MicrodataEnabled) AddHeadCustomParts(""); } #endregion #region Nested classes /// /// JS file meta data /// protected partial record ScriptReferenceMeta { /// /// A value indicating whether to exclude the script from bundling /// public bool ExcludeFromBundle { get; init; } /// /// A value indicating whether the src is local /// public bool IsLocal { get; init; } /// /// Src for production /// public string Src { get; init; } } /// /// CSS file meta data /// protected partial record CssReferenceMeta { /// /// A value indicating whether to exclude the script from bundling /// public bool ExcludeFromBundle { get; init; } /// /// Src for production /// public string Src { get; init; } /// /// A value indicating whether the Src is local /// public bool IsLocal { get; init; } } #endregion }