using System.Net; using System.Reflection; using System.Threading.RateLimiting; using Azure.Identity; using Azure.Storage.Blobs; using FluentValidation; using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Serialization; using Nop.Core; using Nop.Core.Configuration; using Nop.Core.Domain.Common; using Nop.Core.Http; using Nop.Core.Infrastructure; using Nop.Core.Security; using Nop.Data; using Nop.Services.Authentication; using Nop.Services.Authentication.External; using Nop.Services.Common; using Nop.Web.Framework.Mvc.ModelBinding; using Nop.Web.Framework.Mvc.ModelBinding.Binders; using Nop.Web.Framework.Mvc.Routing; using Nop.Web.Framework.Security.Captcha; using Nop.Web.Framework.Themes; using Nop.Web.Framework.Validators; using Nop.Web.Framework.WebOptimizer; using WebMarkupMin.AspNetCoreLatest; using WebMarkupMin.Core; using WebMarkupMin.NUglify; namespace Nop.Web.Framework.Infrastructure.Extensions; /// /// Represents extensions of IServiceCollection /// public static class ServiceCollectionExtensions { /// /// Configure base application settings /// /// Collection of service descriptors /// A builder for web applications and services public static void ConfigureApplicationSettings(this IServiceCollection services, WebApplicationBuilder builder) { //create default file provider CommonHelper.DefaultFileProvider = new NopFileProvider(builder.Environment); //register type finder var typeFinder = new WebAppTypeFinder(); Singleton.Instance = typeFinder; services.AddSingleton(typeFinder); //bind general configuration services.BindApplicationSettings(builder); } /// /// Bind application settings /// /// Collection of service descriptors /// A builder for web applications and services public static void BindApplicationSettings(this IServiceCollection services, WebApplicationBuilder builder) { var typeFinder = Singleton.Instance; //add configuration parameters var configurations = typeFinder .FindClassesOfType() .Select(configType => (IConfig)Activator.CreateInstance(configType)) .ToList(); foreach (var config in configurations) builder.Configuration.GetSection(config.Name).Bind(config, options => options.BindNonPublicProperties = true); var appSettings = Singleton.Instance; if (appSettings == null) { appSettings = AppSettingsHelper.SaveAppSettings(configurations, CommonHelper.DefaultFileProvider, false); services.AddSingleton(appSettings); } else { var needToUpdate = configurations.Any(conf => !appSettings.Configuration.ContainsKey(conf.Name)); AppSettingsHelper.SaveAppSettings(configurations, CommonHelper.DefaultFileProvider, needToUpdate); } } /// /// Add services to the application and configure service provider /// /// Collection of service descriptors /// A builder for web applications and services public static void ConfigureApplicationServices(this IServiceCollection services, WebApplicationBuilder builder) { //add accessor to HttpContext services.AddHttpContextAccessor(); //initialize plugins var mvcCoreBuilder = services.AddMvcCore(); var pluginConfig = new PluginConfig(); builder.Configuration.GetSection(nameof(PluginConfig)).Bind(pluginConfig, options => options.BindNonPublicProperties = true); mvcCoreBuilder.PartManager.InitializePlugins(pluginConfig); //bind plugins configurations services.BindApplicationSettings(builder); //create engine and configure service provider var engine = EngineContext.Create(); builder.Services.AddRateLimiter(options => { var settings = Singleton.Instance.Get(); options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.User.Identity?.Name ?? httpContext.Request.Headers.Host.ToString(), factory: partition => new FixedWindowRateLimiterOptions { AutoReplenishment = true, PermitLimit = settings.PermitLimit, QueueLimit = settings.QueueCount, Window = TimeSpan.FromMinutes(1) })); options.RejectionStatusCode = settings.RejectionStatusCode; }); engine.ConfigureServices(services, builder.Configuration); } /// /// Register HttpContextAccessor /// /// Collection of service descriptors public static void AddHttpContextAccessor(this IServiceCollection services) { services.AddSingleton(); } /// /// Adds services required for anti-forgery support /// /// Collection of service descriptors public static void AddAntiForgery(this IServiceCollection services) { //override cookie name services.AddAntiforgery(options => { options.Cookie.Name = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.AntiforgeryCookie}"; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); } /// /// Adds services required for application session state /// /// Collection of service descriptors public static void AddHttpSession(this IServiceCollection services) { services.AddSession(options => { options.Cookie.Name = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.SessionCookie}"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); } /// /// Adds services required for themes support /// /// Collection of service descriptors public static void AddThemes(this IServiceCollection services) { if (!DataSettingsManager.IsDatabaseInstalled()) return; //themes support services.Configure(options => { options.ViewLocationExpanders.Add(new ThemeableViewLocationExpander()); }); } /// /// Adds services required for distributed cache /// /// Collection of service descriptors public static void AddDistributedCache(this IServiceCollection services) { var appSettings = Singleton.Instance; var distributedCacheConfig = appSettings.Get(); if (!distributedCacheConfig.Enabled) return; switch (distributedCacheConfig.DistributedCacheType) { case DistributedCacheType.Memory: services.AddDistributedMemoryCache(); break; case DistributedCacheType.SqlServer: services.AddDistributedSqlServerCache(options => { options.ConnectionString = distributedCacheConfig.ConnectionString; options.SchemaName = distributedCacheConfig.SchemaName; options.TableName = distributedCacheConfig.TableName; }); break; case DistributedCacheType.Redis: services.AddStackExchangeRedisCache(options => { options.Configuration = distributedCacheConfig.ConnectionString; options.InstanceName = distributedCacheConfig.InstanceName ?? string.Empty; }); break; case DistributedCacheType.RedisSynchronizedMemory: services.AddStackExchangeRedisCache(options => { options.Configuration = distributedCacheConfig.ConnectionString; options.InstanceName = distributedCacheConfig.InstanceName ?? string.Empty; }); break; } } /// /// Adds data protection services /// /// Collection of service descriptors public static void AddNopDataProtection(this IServiceCollection services) { var appSettings = Singleton.Instance; if (appSettings.Get().Enabled && appSettings.Get().StoreDataProtectionKeys) { var blobServiceClient = new BlobServiceClient(appSettings.Get().ConnectionString); var blobContainerClient = blobServiceClient.GetBlobContainerClient(appSettings.Get().DataProtectionKeysContainerName); var blobClient = blobContainerClient.GetBlobClient(NopDataProtectionDefaults.AzureDataProtectionKeyFile); var dataProtectionBuilder = services.AddDataProtection().PersistKeysToAzureBlobStorage(blobClient); if (!appSettings.Get().DataProtectionKeysEncryptWithVault) return; var keyIdentifier = appSettings.Get().DataProtectionKeysVaultId; var credentialOptions = new DefaultAzureCredentialOptions(); var tokenCredential = new DefaultAzureCredential(credentialOptions); dataProtectionBuilder.ProtectKeysWithAzureKeyVault(new Uri(keyIdentifier), tokenCredential); } else { var dataProtectionKeysPath = CommonHelper.DefaultFileProvider.MapPath(NopDataProtectionDefaults.DataProtectionKeysPath); var dataProtectionKeysFolder = new System.IO.DirectoryInfo(dataProtectionKeysPath); //configure the data protection system to persist keys to the specified directory services.AddDataProtection().PersistKeysToFileSystem(dataProtectionKeysFolder); } } /// /// Adds authentication service /// /// Collection of service descriptors public static void AddNopAuthentication(this IServiceCollection services) { //set default authentication schemes var authenticationBuilder = services.AddAuthentication(options => { options.DefaultChallengeScheme = NopAuthenticationDefaults.AuthenticationScheme; options.DefaultScheme = NopAuthenticationDefaults.AuthenticationScheme; options.DefaultSignInScheme = NopAuthenticationDefaults.ExternalAuthenticationScheme; }); //add main cookie authentication authenticationBuilder.AddCookie(NopAuthenticationDefaults.AuthenticationScheme, options => { options.Cookie.Name = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.AuthenticationCookie}"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.LoginPath = NopAuthenticationDefaults.LoginPath; options.AccessDeniedPath = NopAuthenticationDefaults.AccessDeniedPath; }); //add external authentication authenticationBuilder.AddCookie(NopAuthenticationDefaults.ExternalAuthenticationScheme, options => { options.Cookie.Name = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.ExternalAuthenticationCookie}"; options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.LoginPath = NopAuthenticationDefaults.LoginPath; options.AccessDeniedPath = NopAuthenticationDefaults.AccessDeniedPath; }); //register and configure external authentication plugins now var typeFinder = Singleton.Instance; var externalAuthConfigurations = typeFinder.FindClassesOfType(); var externalAuthInstances = externalAuthConfigurations .Select(x => (IExternalAuthenticationRegistrar)Activator.CreateInstance(x)); foreach (var instance in externalAuthInstances) instance.Configure(authenticationBuilder); } /// /// Add and configure MVC for the application /// /// Collection of service descriptors /// A builder for configuring MVC services public static IMvcBuilder AddNopMvc(this IServiceCollection services) { //add basic MVC feature var mvcBuilder = services.AddControllersWithViews(); mvcBuilder.AddRazorRuntimeCompilation(); var appSettings = Singleton.Instance; if (appSettings.Get().UseSessionStateTempDataProvider) { //use session-based temp data provider mvcBuilder.AddSessionStateTempDataProvider(); } else { //use cookie-based temp data provider mvcBuilder.AddCookieTempDataProvider(options => { options.Cookie.Name = $"{NopCookieDefaults.Prefix}{NopCookieDefaults.TempDataCookie}"; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); } services.AddRazorPages(); //MVC now serializes JSON with camel case names by default, use this code to avoid it mvcBuilder.AddNewtonsoftJson(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver()); //set some options mvcBuilder.AddMvcOptions(options => { options.ModelBinderProviders.Insert(1, new NopModelBinderProvider()); //add custom display metadata provider options.ModelMetadataDetailsProviders.Add(new NopMetadataProvider()); //in .NET model binding for a non-nullable property may fail with an error message "The value '' is invalid" //here we set the locale name as the message, we'll replace it with the actual one later when not-null validation failed options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(_ => NopValidationDefaults.NotNullValidationLocaleName); }); //add fluent validation services.AddFluentValidationAutoValidation().AddFluentValidationClientsideAdapters(); //register all available validators from Nop assemblies var assemblies = mvcBuilder.PartManager.ApplicationParts .OfType() .Where(part => part.Name.StartsWith("Nop", StringComparison.InvariantCultureIgnoreCase)) .Select(part => part.Assembly); services.AddValidatorsFromAssemblies(assemblies); //register controllers as services, it'll allow to override them mvcBuilder.AddControllersAsServices(); return mvcBuilder; } /// /// Register custom RedirectResultExecutor /// /// Collection of service descriptors public static void AddNopRedirectResultExecutor(this IServiceCollection services) { //we use custom redirect executor as a workaround to allow using non-ASCII characters in redirect URLs services.AddScoped, NopRedirectResultExecutor>(); } /// /// Add and configure WebMarkupMin service /// /// Collection of service descriptors public static void AddNopWebMarkupMin(this IServiceCollection services) { //check whether database is installed if (!DataSettingsManager.IsDatabaseInstalled()) return; services .AddWebMarkupMin(options => { options.AllowMinificationInDevelopmentEnvironment = true; options.AllowCompressionInDevelopmentEnvironment = true; options.DisableMinification = !EngineContext.Current.Resolve().EnableHtmlMinification; options.DisableCompression = true; options.DisablePoweredByHttpHeaders = true; }) .AddHtmlMinification(options => { options.MinificationSettings.AttributeQuotesRemovalMode = HtmlAttributeQuotesRemovalMode.KeepQuotes; options.CssMinifierFactory = new NUglifyCssMinifierFactory(); options.JsMinifierFactory = new NUglifyJsMinifierFactory(); }) .AddXmlMinification(options => { var settings = options.MinificationSettings; settings.RenderEmptyTagsWithSpace = true; settings.CollapseTagsWithoutContent = true; }); } /// /// Adds WebOptimizer to the specified and enables CSS and JavaScript minification. /// /// Collection of service descriptors public static void AddNopWebOptimizer(this IServiceCollection services) { var appSettings = Singleton.Instance; var woConfig = appSettings.Get(); if (!woConfig.EnableCssBundling && !woConfig.EnableJavaScriptBundling) { services.AddScoped(); return; } //add minification & bundling var cssSettings = new CssBundlingSettings { FingerprintUrls = false, Minify = woConfig.EnableCssBundling }; var codeSettings = new CodeBundlingSettings { Minify = woConfig.EnableJavaScriptBundling, AdjustRelativePaths = false //disable this feature because it breaks function names that have "Url(" at the end }; services.AddWebOptimizer(null, cssSettings, codeSettings); services.AddScoped(); } /// /// Add and configure default HTTP clients /// /// Collection of service descriptors public static void AddNopHttpClients(this IServiceCollection services) { //default client services.AddHttpClient(NopHttpDefaults.DefaultHttpClient).WithProxy(); //client to request current store services.AddHttpClient(); //client to request nopCommerce official site services.AddHttpClient().WithProxy(); //client to request reCAPTCHA service services.AddHttpClient().WithProxy(); } }