FruitBank/Presentation/Nop.Web.Framework/Infrastructure/Extensions/ApplicationBuilderExtension...

550 lines
23 KiB
C#

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;
/// <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);
}
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<ILogger>().InformationAsync("Application started");
//install and update plugins
var pluginService = engine.Resolve<IPluginService>();
await pluginService.InstallPluginsAsync();
await pluginService.UpdatePluginsAsync();
//insert new ACL permission if exists
var permissionService = engine.Resolve<IPermissionService>();
await permissionService.InsertPermissionsAsync();
//update nopCommerce core and db
var migrationManager = engine.Resolve<IMigrationManager>();
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<ITaskScheduler>();
await taskScheduler.InitializeAsync();
await taskScheduler.StartSchedulerAsync();
}
}
/// <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)
{
var webHelper = EngineContext.Current.Resolve<IWebHelper>();
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<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());
}
}
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;
}
}
}
});
}
/// <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 });
//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<CommonConfig>().PluginStaticFileExtensionsBlacklist))
{
var fileExtensionContentTypeProvider = new FileExtensionContentTypeProvider();
foreach (var ext in appSettings.Get<CommonConfig>().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<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>();
//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));
}
}
/// <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();
}
}