Webiant Logo Webiant Logo
  1. No results found.

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

ApplicationBuilderExtensions.cs

using System.Globalization;
using System.Net;
using System.Runtime.ExceptionServices;
using iTextSharp.text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
using Nop.Core;
using Nop.Core.Configuration;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Localization;
using Nop.Core.Domain.Media;
using Nop.Core.Events;
using Nop.Core.Http;
using Nop.Core.Infrastructure;
using Nop.Data;
using Nop.Services.Authentication;
using Nop.Services.Common;
using Nop.Services.Installation;
using Nop.Services.Localization;
using Nop.Services.Logging;
using Nop.Services.Media;
using Nop.Services.Media.RoxyFileman;
using Nop.Services.Security;
using Nop.Services.Seo;
using Nop.Web.Framework.Globalization;
using Nop.Web.Framework.Mvc.Routing;
using Nop.Web.Framework.WebOptimizer;
using WebMarkupMin.AspNetCoreLatest;
using WebOptimizer;
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;

namespace Nop.Web.Framework.Infrastructure.Extensions;

/// <summary>
/// Represents extensions of IApplicationBuilder
/// </summary>
public static class ApplicationBuilderExtensions
{
    /// <summary>
    /// Configure the application HTTP request pipeline
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void ConfigureRequestPipeline(this IApplicationBuilder application)
    {
        EngineContext.Current.ConfigureRequestPipeline(application);
    }

    /// <summary>
    /// Publish AppStarted event
    /// </summary>
    /// <param name="_">Builder for configuring an application's request pipeline</param>
    /// <returns>A task that represents the asynchronous operation</returns>
    public static async Task PublishAppStartedEventAsync(this IApplicationBuilder _)
    {
        //publish AppStartedEvent
        var eventPublisher = EngineContext.Current.Resolve<IEventPublisher>();
        await eventPublisher.PublishAsync(new AppStartedEvent());
    }

    /// <summary>
    /// Add exception handling
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopExceptionHandler(this IApplicationBuilder application)
    {
        var appSettings = EngineContext.Current.Resolve<AppSettings>();
        var webHostEnvironment = EngineContext.Current.Resolve<IWebHostEnvironment>();
        var useDetailedExceptionPage = appSettings.Get<CommonConfig>().DisplayFullErrorStack || webHostEnvironment.IsDevelopment();
        if (useDetailedExceptionPage)
        {
            //get detailed exceptions for developing and testing purposes
            application.UseDeveloperExceptionPage();
        }
        else
        {
            //or use special exception handler
            application.UseExceptionHandler("/Error/Error");
        }

        //log errors
        application.UseExceptionHandler(handler =>
        {
            handler.Run(async context =>
            {
                var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
                if (exception == null)
                    return;

                try
                {
                    //check whether database is installed
                    if (DataSettingsManager.IsDatabaseInstalled())
                    {
                        //get current customer
                        var currentCustomer = await EngineContext.Current.Resolve<IWorkContext>().GetCurrentCustomerAsync();

                        //log error
                        await EngineContext.Current.Resolve<ILogger>().ErrorAsync(exception.Message, exception, currentCustomer);
                    }
                }
                finally
                {
                    //rethrow the exception to show the error page
                    ExceptionDispatchInfo.Throw(exception);
                }
            });
        });
    }

    /// <summary>
    /// Adds a special handler that checks for responses with the 404 status code that do not have a body
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UsePageNotFound(this IApplicationBuilder application)
    {
        application.UseStatusCodePages(async context =>
        {
            //handle 404 Not Found
            if (context.HttpContext.Response.StatusCode != StatusCodes.Status404NotFound)
                return;

            //return a browser-dependent error message indicating the static file can't be found
            var webHelper = EngineContext.Current.Resolve<IWebHelper>();
            if (webHelper.IsStaticResource())
                return;

            //get original path and query
            var originalPath = context.HttpContext.Request.Path;
            var originalQueryString = context.HttpContext.Request.QueryString;

            if (DataSettingsManager.IsDatabaseInstalled())
            {
                var commonSettings = EngineContext.Current.Resolve<CommonSettings>();

                if (commonSettings.Log404Errors)
                {
                    var logger = EngineContext.Current.Resolve<ILogger>();
                    var workContext = EngineContext.Current.Resolve<IWorkContext>();

                    await logger.ErrorAsync($"Error 404. The requested page ({originalPath}) was not found",
                        customer: await workContext.GetCurrentCustomerAsync());
                }
            }

            var routeValuesFeature = context.HttpContext.Features.Get<IRouteValuesFeature>();

            //store the original paths so we can check it later
            context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(new StatusCodeReExecuteFeature
            {
                OriginalPathBase = context.HttpContext.Request.PathBase.Value!,
                OriginalPath = originalPath.Value!,
                OriginalQueryString = originalQueryString.HasValue ? originalQueryString.Value : null,
                Endpoint = context.HttpContext.GetEndpoint(),
                RouteValues = routeValuesFeature?.RouteValues
            });

            string getLanguageRouteValue()
            {
                if (!DataSettingsManager.IsDatabaseInstalled())
                    return string.Empty;

                var localizationSettings = EngineContext.Current.Resolve<LocalizationSettings>();
                if (!localizationSettings.SeoFriendlyUrlsForLanguagesEnabled)
                    return string.Empty;

                return routeValuesFeature?.RouteValues.GetValueOrDefault(NopRoutingDefaults.RouteValue.Language) is string lang ? $"/{lang}" : "/en";
            }

            //set new path
            context.HttpContext.Request.Path = $"{getLanguageRouteValue()}/page-not-found";
            context.HttpContext.Request.QueryString = QueryString.Empty;

            //since we're going to re-invoke the middleware pipeline we need to reset the endpoint and route values
            context.HttpContext.SetEndpoint(null);
            if (routeValuesFeature is not null)
                routeValuesFeature.RouteValues = null!;

            try
            {
                //re-execute request with new path
                await context.Next(context.HttpContext);
            }
            finally
            {
                //return original path to request
                context.HttpContext.Request.QueryString = originalQueryString;
                context.HttpContext.Request.Path = originalPath;
                context.HttpContext.Features.Set<IStatusCodeReExecuteFeature>(null);
            }
        });
    }

    /// <summary>
    /// Adds a special handler that checks for responses with the 400 status code (bad request)
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseBadRequestResult(this IApplicationBuilder application)
    {
        application.UseStatusCodePages(async context =>
        {
            //handle 404 (Bad request)
            if (context.HttpContext.Response.StatusCode == StatusCodes.Status400BadRequest)
            {
                var logger = EngineContext.Current.Resolve<ILogger>();
                var workContext = EngineContext.Current.Resolve<IWorkContext>();
                await logger.ErrorAsync("Error 400. Bad request", null, customer: await workContext.GetCurrentCustomerAsync());
            }
        });
    }

    /// <summary>
    /// Configure middleware for dynamically compressing HTTP responses
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopResponseCompression(this IApplicationBuilder application)
    {
        if (!DataSettingsManager.IsDatabaseInstalled())
            return;

        //whether to use compression (gzip by default)
        if (EngineContext.Current.Resolve<CommonSettings>().UseResponseCompression)
            application.UseResponseCompression();
    }

    /// <summary>
    /// Adds WebOptimizer to the <see cref="IApplicationBuilder"/> request execution pipeline
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopWebOptimizer(this IApplicationBuilder application)
    {
        var appSettings = Singleton<AppSettings>.Instance;
        var woConfig = appSettings.Get<WebOptimizerConfig>();

        if (!woConfig.EnableCssBundling && !woConfig.EnableJavaScriptBundling)
            return;

        var fileProvider = EngineContext.Current.Resolve<INopFileProvider>();
        var webHostEnvironment = EngineContext.Current.Resolve<IWebHostEnvironment>();

        application.UseWebOptimizer(webHostEnvironment,
        [
            new FileProviderOptions
            {
                RequestPath = new PathString("/Plugins"),
                FileProvider = new PhysicalFileProvider(fileProvider.MapPath(@"Plugins"))
            },
            new FileProviderOptions
            {
                RequestPath = new PathString("/Themes"),
                FileProvider = new PhysicalFileProvider(fileProvider.MapPath(@"Themes"))
            }
        ]);
    }

    /// <summary>
    /// Configure static file serving
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopStaticFiles(this IApplicationBuilder application)
    {
        var fileProvider = EngineContext.Current.Resolve<INopFileProvider>();
        var appSettings = EngineContext.Current.Resolve<AppSettings>();

        void staticFileResponse(StaticFileResponseContext context)
        {
            if (!string.IsNullOrEmpty(appSettings.Get<CommonConfig>().StaticFilesCacheControl))
                context.Context.Response.Headers.Append(HeaderNames.CacheControl, appSettings.Get<CommonConfig>().StaticFilesCacheControl);
        }

        //add handling if sitemaps 
        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath(NopSeoDefaults.SitemapXmlDirectory)),
            RequestPath = new PathString($"/{NopSeoDefaults.SitemapXmlDirectory}"),
            OnPrepareResponse = context =>
            {
                if (!DataSettingsManager.IsDatabaseInstalled() ||
                    !EngineContext.Current.Resolve<SitemapXmlSettings>().SitemapXmlEnabled)
                {
                    context.Context.Response.StatusCode = StatusCodes.Status403Forbidden;
                    context.Context.Response.ContentLength = 0;
                    context.Context.Response.Body = Stream.Null;
                }
            }
        });

        //common static files
        application.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = staticFileResponse });

        //images
        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.GetLocalImagesPath(EngineContext.Current.Resolve<MediaSettings>())),
            RequestPath = new PathString("/images"),
            OnPrepareResponse = staticFileResponse
        });

        //themes static files
        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.MapPath("Themes")),
            RequestPath = new PathString("/Themes"),
            OnPrepareResponse = staticFileResponse
        });

        //plugins static files
        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.MapPath("Plugins")),
            RequestPath = new PathString("/Plugins"),
            OnPrepareResponse = staticFileResponse
        });

        //add support for backups
        var provider = new FileExtensionContentTypeProvider
        {
            Mappings = { [".bak"] = MimeTypes.ApplicationOctetStream }
        };

        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath(NopCommonDefaults.DbBackupsPath)),
            RequestPath = new PathString("/db_backups"),
            ContentTypeProvider = provider,
            OnPrepareResponse = context =>
            {
                if (!DataSettingsManager.IsDatabaseInstalled() ||
                    !EngineContext.Current.Resolve<IPermissionService>().AuthorizeAsync(StandardPermission.System.MANAGE_MAINTENANCE).Result)
                {
                    context.Context.Response.StatusCode = StatusCodes.Status404NotFound;
                    context.Context.Response.ContentLength = 0;
                    context.Context.Response.Body = Stream.Null;
                }
            }
        });

        //add support for webmanifest files
        provider.Mappings[".webmanifest"] = MimeTypes.ApplicationManifestJson;

        application.UseStaticFiles(new StaticFileOptions
        {
            FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath("icons")),
            RequestPath = "/icons",
            ContentTypeProvider = provider
        });

        if (DataSettingsManager.IsDatabaseInstalled())
        {
            application.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = EngineContext.Current.Resolve<IRoxyFilemanFileProvider>(),
                RequestPath = new PathString(NopRoxyFilemanDefaults.DefaultRootDirectory),
                OnPrepareResponse = staticFileResponse
            });
        }

        if (appSettings.Get<CommonConfig>().ServeUnknownFileTypes)
        {
            application.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath(".well-known")),
                RequestPath = new PathString("/.well-known"),
                ServeUnknownFileTypes = true,
            });
        }
    }

    /// <summary>
    /// Configure middleware checking whether requested page is keep alive page
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseKeepAlive(this IApplicationBuilder application)
    {
        application.UseMiddleware<KeepAliveMiddleware>();
    }

    /// <summary>
    /// Configure middleware checking whether database is installed
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseInstallUrl(this IApplicationBuilder application)
    {
        application.UseMiddleware<InstallUrlMiddleware>();
    }

    /// <summary>
    /// Adds the authentication middleware, which enables authentication capabilities.
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopAuthentication(this IApplicationBuilder application)
    {
        //check whether database is installed
        if (!DataSettingsManager.IsDatabaseInstalled())
            return;

        application.UseMiddleware<AuthenticationMiddleware>();
    }

    /// <summary>
    /// Configure PDF
    /// </summary>
    public static void UseNopPdf(this IApplicationBuilder _)
    {
        if (!DataSettingsManager.IsDatabaseInstalled())
            return;

        var fileProvider = EngineContext.Current.Resolve<INopFileProvider>();

        var fontPaths = fileProvider.EnumerateFiles(fileProvider.MapPath("~/App_Data/Pdf/"), "*.ttf") ?? Enumerable.Empty<string>();
        foreach (var fp in fontPaths)
        {
            FontFactory.Register(fp, fileProvider.GetFileNameWithoutExtension(fp));
        }
    }

    /// <summary>
    /// Configure the request localization feature
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopRequestLocalization(this IApplicationBuilder application)
    {
        application.UseRequestLocalization(options =>
        {
            if (!DataSettingsManager.IsDatabaseInstalled())
                return;

            var languageService = EngineContext.Current.Resolve<ILanguageService>();
            var localizationSettings = EngineContext.Current.Resolve<LocalizationSettings>();

            //prepare supported cultures
            var cultures = languageService
                .GetAllLanguages()
                .OrderBy(language => language.DisplayOrder)
                .Select(language => new CultureInfo(language.LanguageCulture))
                .ToList();
            options.SupportedCultures = cultures;
            options.SupportedUICultures = cultures;
            options.DefaultRequestCulture = new RequestCulture(cultures.FirstOrDefault() ?? new CultureInfo(NopCommonDefaults.DefaultLanguageCulture));
            options.ApplyCurrentCultureToResponseHeaders = true;

            //configure culture providers
            var headerRequestCultureProvider = options.RequestCultureProviders.OfType<AcceptLanguageHeaderRequestCultureProvider>().FirstOrDefault();
            if (headerRequestCultureProvider is not null)
                options.RequestCultureProviders.Remove(headerRequestCultureProvider);

            options.AddInitialRequestCultureProvider(new NopSeoUrlCultureProvider());
            var cookieRequestCultureProvider = options.RequestCultureProviders.OfType<CookieRequestCultureProvider>().FirstOrDefault();
            if (cookieRequestCultureProvider is not null)
                cookieRequestCultureProvider.CookieName = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.CultureCookie}";

            if (localizationSettings.AutomaticallyDetectLanguage)
                options.RequestCultureProviders.Add(new NopAcceptLanguageHeaderRequestCultureProvider());
        });
    }

    /// <summary>
    /// Configure Endpoints routing
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopEndpoints(this IApplicationBuilder application)
    {
        //Execute the endpoint selected by the routing middleware
        application.UseEndpoints(endpoints =>
        {
            //register all routes
            EngineContext.Current.Resolve<IRoutePublisher>().RegisterRoutes(endpoints);
        });
    }

    /// <summary>
    /// Configure applying forwarded headers to their matching fields on the current request.
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopProxy(this IApplicationBuilder application)
    {
        var appSettings = EngineContext.Current.Resolve<AppSettings>();
        var hostingConfig = appSettings.Get<HostingConfig>();

        if (hostingConfig.UseProxy)
        {
            var options = new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
                // IIS already serves as a reverse proxy and will add X-Forwarded headers to all requests,
                // so we need to increase this limit, otherwise, passed forwarding headers will be ignored.
                ForwardLimit = 2
            };

            if (!string.IsNullOrEmpty(hostingConfig.ForwardedForHeaderName))
                options.ForwardedForHeaderName = hostingConfig.ForwardedForHeaderName;

            if (!string.IsNullOrEmpty(hostingConfig.ForwardedProtoHeaderName))
                options.ForwardedProtoHeaderName = hostingConfig.ForwardedProtoHeaderName;

            options.KnownNetworks.Clear();
            options.KnownProxies.Clear();

            if (!string.IsNullOrEmpty(hostingConfig.KnownProxies))
            {
                foreach (var strIp in hostingConfig.KnownProxies.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                {
                    if (IPAddress.TryParse(strIp, out var ip))
                        options.KnownProxies.Add(ip);
                }
            }

            if (!string.IsNullOrEmpty(hostingConfig.KnownNetworks))
            {
                foreach (var strIpNet in hostingConfig.KnownNetworks.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())
                {
                    var ipNetParts = strIpNet.Split("/");
                    if (ipNetParts.Length == 2)
                    {
                        if (IPAddress.TryParse(ipNetParts[0], out var ip) && int.TryParse(ipNetParts[1], out var length))
                            options.KnownNetworks.Add(new IPNetwork(ip, length));
                    }
                }
            }

            if (options.KnownProxies.Count > 1 || options.KnownNetworks.Count > 1)
                options.ForwardLimit = null; //disable the limit, because KnownProxies is configured

            //configure forwarding
            application.UseForwardedHeaders(options);
        }
    }

    /// <summary>
    /// Configure WebMarkupMin
    /// </summary>
    /// <param name="application">Builder for configuring an application's request pipeline</param>
    public static void UseNopWebMarkupMin(this IApplicationBuilder application)
    {
        //check whether database is installed
        if (!DataSettingsManager.IsDatabaseInstalled())
            return;

        application.UseWebMarkupMin();
    }
}