Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

NopHtmlHelper.cs

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.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 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,
        INopAssetHelper bundleHelper,
        Lazy localizationService,
        IStoreContext storeContext,
        IUrlHelperFactory urlHelperFactory,
        IWebHostEnvironment webHostEnvironment,
        SeoSettings seoSettings)
    {
        _appSettings = appSettings;
        _htmlEncoder = htmlEncoder;
        _actionContextAccessor = actionContextAccessor;
        _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);

        _scriptParts[location].Add(new ScriptReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = urlHelper.IsLocalUrl(src),
            Src = urlHelper.Content(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);

        _scriptParts[location].Insert(0, new ScriptReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = urlHelper.IsLocalUrl(src),
            Src = urlHelper.Content(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 = string.Concat("/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);

        _cssParts.Add(new CssReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = urlHelper.IsLocalUrl(src),
            Src = urlHelper.Content(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);

        _cssParts.Insert(0, new CssReferenceMeta
        {
            ExcludeFromBundle = excludeFromBundle,
            IsLocal = urlHelper.IsLocalUrl(src),
            Src = urlHelper.Content(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 = string.Concat("/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;
    }

    #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
}