using System.Globalization; using System.Net; using System.Reflection; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; 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.Http; using Nop.Core.Infrastructure; using Nop.Data; using Nop.Data.Migrations; using Nop.Services.Authentication; using Nop.Services.Common; using Nop.Services.Installation; using Nop.Services.Localization; using Nop.Services.Logging; using Nop.Services.Media.RoxyFileman; using Nop.Services.Plugins; using Nop.Services.ScheduleTasks; 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 QuestPDF.Drawing; using WebMarkupMin.AspNetCoreLatest; using WebOptimizer; using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; namespace Nop.Web.Framework.Infrastructure.Extensions; /// /// Represents extensions of IApplicationBuilder /// public static class ApplicationBuilderExtensions { /// /// Configure the application HTTP request pipeline /// /// Builder for configuring an application's request pipeline public static void ConfigureRequestPipeline(this IApplicationBuilder application) { EngineContext.Current.ConfigureRequestPipeline(application); } public static async Task StartEngineAsync(this IApplicationBuilder _) { var engine = EngineContext.Current; //further actions are performed only when the database is installed if (DataSettingsManager.IsDatabaseInstalled()) { //log application start await engine.Resolve().InformationAsync("Application started"); //install and update plugins var pluginService = engine.Resolve(); await pluginService.InstallPluginsAsync(); await pluginService.UpdatePluginsAsync(); //insert new ACL permission if exists var permissionService = engine.Resolve(); await permissionService.InsertPermissionsAsync(); //update nopCommerce core and db var migrationManager = engine.Resolve(); var assembly = Assembly.GetAssembly(typeof(ApplicationBuilderExtensions)); migrationManager.ApplyUpMigrations(assembly, MigrationProcessType.Update); assembly = Assembly.GetAssembly(typeof(IMigrationManager)); migrationManager.ApplyUpMigrations(assembly, MigrationProcessType.Update); var taskScheduler = engine.Resolve(); await taskScheduler.InitializeAsync(); await taskScheduler.StartSchedulerAsync(); } } /// /// Add exception handling /// /// Builder for configuring an application's request pipeline public static void UseNopExceptionHandler(this IApplicationBuilder application) { var appSettings = EngineContext.Current.Resolve(); var webHostEnvironment = EngineContext.Current.Resolve(); var useDetailedExceptionPage = appSettings.Get().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()?.Error; if (exception == null) return; try { //check whether database is installed if (DataSettingsManager.IsDatabaseInstalled()) { //get current customer var currentCustomer = await EngineContext.Current.Resolve().GetCurrentCustomerAsync(); //log error await EngineContext.Current.Resolve().ErrorAsync(exception.Message, exception, currentCustomer); } } finally { //rethrow the exception to show the error page ExceptionDispatchInfo.Throw(exception); } }); }); } /// /// Adds a special handler that checks for responses with the 404 status code that do not have a body /// /// Builder for configuring an application's request pipeline public static void UsePageNotFound(this IApplicationBuilder application) { application.UseStatusCodePages(async context => { //handle 404 Not Found if (context.HttpContext.Response.StatusCode == StatusCodes.Status404NotFound) { var webHelper = EngineContext.Current.Resolve(); if (!webHelper.IsStaticResource()) { //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(); if (commonSettings.Log404Errors) { var logger = EngineContext.Current.Resolve(); var workContext = EngineContext.Current.Resolve(); await logger.ErrorAsync($"Error 404. The requested page ({originalPath}) was not found", customer: await workContext.GetCurrentCustomerAsync()); } } try { //get new path var pageNotFoundPath = "/page-not-found"; //re-execute request with new path context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + pageNotFoundPath); } finally { //return original path to request context.HttpContext.Request.QueryString = originalQueryString; context.HttpContext.Request.Path = originalPath; } } } }); } /// /// Adds a special handler that checks for responses with the 400 status code (bad request) /// /// Builder for configuring an application's request pipeline 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(); var workContext = EngineContext.Current.Resolve(); await logger.ErrorAsync("Error 400. Bad request", null, customer: await workContext.GetCurrentCustomerAsync()); } }); } /// /// Configure middleware for dynamically compressing HTTP responses /// /// Builder for configuring an application's request pipeline public static void UseNopResponseCompression(this IApplicationBuilder application) { if (!DataSettingsManager.IsDatabaseInstalled()) return; //whether to use compression (gzip by default) if (EngineContext.Current.Resolve().UseResponseCompression) application.UseResponseCompression(); } /// /// Adds WebOptimizer to the request execution pipeline /// /// Builder for configuring an application's request pipeline public static void UseNopWebOptimizer(this IApplicationBuilder application) { var appSettings = Singleton.Instance; var woConfig = appSettings.Get(); if (!woConfig.EnableCssBundling && !woConfig.EnableJavaScriptBundling) return; var fileProvider = EngineContext.Current.Resolve(); var webHostEnvironment = EngineContext.Current.Resolve(); 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")) } ]); } /// /// Configure static file serving /// /// Builder for configuring an application's request pipeline public static void UseNopStaticFiles(this IApplicationBuilder application) { var fileProvider = EngineContext.Current.Resolve(); var appSettings = EngineContext.Current.Resolve(); void staticFileResponse(StaticFileResponseContext context) { if (!string.IsNullOrEmpty(appSettings.Get().StaticFilesCacheControl)) context.Context.Response.Headers.Append(HeaderNames.CacheControl, appSettings.Get().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().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 }); //themes static files application.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(fileProvider.MapPath(@"Themes")), RequestPath = new PathString("/Themes"), OnPrepareResponse = staticFileResponse }); //plugins static files var staticFileOptions = new StaticFileOptions { FileProvider = new PhysicalFileProvider(fileProvider.MapPath(@"Plugins")), RequestPath = new PathString("/Plugins"), OnPrepareResponse = staticFileResponse }; //exclude files in blacklist if (!string.IsNullOrEmpty(appSettings.Get().PluginStaticFileExtensionsBlacklist)) { var fileExtensionContentTypeProvider = new FileExtensionContentTypeProvider(); foreach (var ext in appSettings.Get().PluginStaticFileExtensionsBlacklist .Split(';', ',') .Select(e => e.Trim().ToLowerInvariant()) .Select(e => $"{(e.StartsWith(".") ? string.Empty : ".")}{e}") .Where(fileExtensionContentTypeProvider.Mappings.ContainsKey)) { fileExtensionContentTypeProvider.Mappings.Remove(ext); } staticFileOptions.ContentTypeProvider = fileExtensionContentTypeProvider; } application.UseStaticFiles(staticFileOptions); //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().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(), RequestPath = new PathString(NopRoxyFilemanDefaults.DefaultRootDirectory), OnPrepareResponse = staticFileResponse }); } if (appSettings.Get().ServeUnknownFileTypes) { application.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(fileProvider.GetAbsolutePath(".well-known")), RequestPath = new PathString("/.well-known"), ServeUnknownFileTypes = true, }); } } /// /// Configure middleware checking whether requested page is keep alive page /// /// Builder for configuring an application's request pipeline public static void UseKeepAlive(this IApplicationBuilder application) { application.UseMiddleware(); } /// /// Configure middleware checking whether database is installed /// /// Builder for configuring an application's request pipeline public static void UseInstallUrl(this IApplicationBuilder application) { application.UseMiddleware(); } /// /// Adds the authentication middleware, which enables authentication capabilities. /// /// Builder for configuring an application's request pipeline public static void UseNopAuthentication(this IApplicationBuilder application) { //check whether database is installed if (!DataSettingsManager.IsDatabaseInstalled()) return; application.UseMiddleware(); } /// /// Configure PDF /// public static void UseNopPdf(this IApplicationBuilder _) { if (!DataSettingsManager.IsDatabaseInstalled()) return; var fileProvider = EngineContext.Current.Resolve(); var fontPaths = fileProvider.EnumerateFiles(fileProvider.MapPath("~/App_Data/Pdf/"), "*.ttf") ?? Enumerable.Empty(); //write placeholder characters instead of unavailable glyphs for both debug/release configurations QuestPDF.Settings.CheckIfAllTextGlyphsAreAvailable = false; foreach (var fp in fontPaths) { FontManager.RegisterFont(File.OpenRead(fp)); } } /// /// Configure the request localization feature /// /// Builder for configuring an application's request pipeline public static void UseNopRequestLocalization(this IApplicationBuilder application) { application.UseRequestLocalization(options => { if (!DataSettingsManager.IsDatabaseInstalled()) return; var languageService = EngineContext.Current.Resolve(); var localizationSettings = EngineContext.Current.Resolve(); //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().FirstOrDefault(); if (headerRequestCultureProvider is not null) options.RequestCultureProviders.Remove(headerRequestCultureProvider); options.AddInitialRequestCultureProvider(new NopSeoUrlCultureProvider()); var cookieRequestCultureProvider = options.RequestCultureProviders.OfType().FirstOrDefault(); if (cookieRequestCultureProvider is not null) cookieRequestCultureProvider.CookieName = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.CultureCookie}"; if (localizationSettings.AutomaticallyDetectLanguage) options.RequestCultureProviders.Add(new NopAcceptLanguageHeaderRequestCultureProvider()); }); } /// /// Configure Endpoints routing /// /// Builder for configuring an application's request pipeline public static void UseNopEndpoints(this IApplicationBuilder application) { //Execute the endpoint selected by the routing middleware application.UseEndpoints(endpoints => { //register all routes EngineContext.Current.Resolve().RegisterRoutes(endpoints); }); } /// /// Configure applying forwarded headers to their matching fields on the current request. /// /// Builder for configuring an application's request pipeline public static void UseNopProxy(this IApplicationBuilder application) { var appSettings = EngineContext.Current.Resolve(); var hostingConfig = appSettings.Get(); 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); } } /// /// Configure WebMarkupMin /// /// Builder for configuring an application's request pipeline public static void UseNopWebMarkupMin(this IApplicationBuilder application) { //check whether database is installed if (!DataSettingsManager.IsDatabaseInstalled()) return; application.UseWebMarkupMin(); } }