using System; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Oqtane.Infrastructure; using Oqtane.Modules; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Services; using Oqtane.Shared; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { public static IServiceCollection AddOqtane(this IServiceCollection services, Runtime runtime, string[] supportedCultures) { LoadAssemblies(); LoadSatelliteAssemblies(supportedCultures); services.AddOqtaneServices(runtime); return services; } public static IServiceCollection AddOqtaneDbContext(this IServiceCollection services) { services.AddDbContext(options => { }); services.AddDbContext(options => { }); services.AddIdentityCore(options => { }) .AddEntityFrameworkStores() .AddSignInManager() .AddDefaultTokenProviders(); return services; } public static IServiceCollection AddOqtaneAuthorizationPolicies(this IServiceCollection services) { services.AddAuthorizationCore(options => { options.AddPolicy(PolicyNames.ViewPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.View))); options.AddPolicy(PolicyNames.EditPage, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Page, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ViewModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.View))); options.AddPolicy(PolicyNames.EditModule, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Module, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ViewFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.View))); options.AddPolicy(PolicyNames.EditFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Edit))); options.AddPolicy(PolicyNames.ListFolder, policy => policy.Requirements.Add(new PermissionRequirement(EntityNames.Folder, PermissionNames.Browse))); }); return services; } internal static IServiceCollection AddOqtaneScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services, IConfigurationRoot configurationRoot) { services.AddSingleton(configurationRoot); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); return services; } internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) { services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); return services; } public static IServiceCollection ConfigureOqtaneCookieOptions(this IServiceCollection services) { services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = false; options.Events.OnRedirectToLogin = context => { context.Response.StatusCode = 401; return Task.CompletedTask; }; }); return services; } public static IServiceCollection ConfigureOqtaneIdentityOptions(this IServiceCollection services) { services.Configure(options => { // Password settings options.Password.RequireDigit = false; options.Password.RequiredLength = 6; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequireLowercase = false; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); options.Lockout.MaxFailedAccessAttempts = 10; options.Lockout.AllowedForNewUsers = true; // User settings options.User.RequireUniqueEmail = false; }); return services; } internal static IServiceCollection TryAddHttpClientWithAuthenticationCookie(this IServiceCollection services) { if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { services.AddScoped(s => { // creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. var navigationManager = s.GetRequiredService(); var httpContextAccessor = s.GetRequiredService(); var authToken = httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"]; var client = new HttpClient(new HttpClientHandler { UseCookies = false }); if (authToken != null) { client.DefaultRequestHeaders.Add("Cookie", ".AspNetCore.Identity.Application=" + authToken); } client.BaseAddress = new Uri(navigationManager.Uri); return client; }); } return services; } internal static IServiceCollection TryAddSwagger(this IServiceCollection services, bool useSwagger) { if (useSwagger) { services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Oqtane", Version = "v1" }); }); } return services; } private static IServiceCollection AddOqtaneServices(this IServiceCollection services, Runtime runtime) { if (services is null) { throw new ArgumentNullException(nameof(services)); } var hostedServiceType = typeof(IHostedService); var assemblies = AppDomain.CurrentDomain.GetOqtaneAssemblies(); foreach (var assembly in assemblies) { // dynamically register module services, contexts, and repository classes var implementationTypes = assembly.GetInterfaces(); foreach (var implementationType in implementationTypes) { if (implementationType.AssemblyQualifiedName != null) { var serviceType = Type.GetType(implementationType.AssemblyQualifiedName.Replace(implementationType.Name, $"I{implementationType.Name}")); services.AddScoped(serviceType ?? implementationType, implementationType); } } // dynamically register hosted services var serviceTypes = assembly.GetTypes(hostedServiceType); foreach (var serviceType in serviceTypes) { if (serviceType.IsSubclassOf(typeof(HostedServiceBase))) { services.AddSingleton(hostedServiceType, serviceType); } } // register server startup services var startUps = assembly.GetInstances(); foreach (var startup in startUps) { startup.ConfigureServices(services); } if (runtime == Runtime.Server) { // register client startup services if running on server assembly.GetInstances() .ToList() .ForEach(x => x.ConfigureServices(services)); } } return services; } private static void LoadAssemblies() { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); if (assemblyPath == null) return; AssemblyLoadContext.Default.Resolving += ResolveDependencies; var assembliesFolder = new DirectoryInfo(assemblyPath); var assemblies = AppDomain.CurrentDomain.GetAssemblies(); // iterate through Oqtane assemblies in /bin ( filter is narrow to optimize loading process ) foreach (var dll in assembliesFolder.EnumerateFiles($"*.dll", SearchOption.TopDirectoryOnly).Where(f => f.IsOqtaneAssembly())) { AssemblyName assemblyName; try { assemblyName = AssemblyName.GetAssemblyName(dll.FullName); } catch { Console.WriteLine($"Not Assembly : {dll.Name}"); continue; } if (!assemblies.Any(a => AssemblyName.ReferenceMatchesDefinition(assemblyName, a.GetName()))) { try { var pdb = Path.ChangeExtension(dll.FullName, ".pdb"); Assembly assembly = null; // load assembly ( and symbols ) from stream to prevent locking files ( as long as dependencies are in /bin they will load as well ) if (File.Exists(pdb)) { assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(dll.FullName)), new MemoryStream(File.ReadAllBytes(pdb))); } else { assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(dll.FullName))); } Console.WriteLine($"Loaded : {assemblyName}"); } catch (Exception e) { Console.WriteLine($"Failed : {assemblyName}\n{e}"); } } } } private static void LoadSatelliteAssemblies(string[] supportedCultures) { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); if (assemblyPath == null) { return; } AssemblyLoadContext.Default.Resolving += ResolveDependencies; foreach (var culture in supportedCultures) { if (culture == Constants.DefaultCulture) { continue; } var assembliesFolder = new DirectoryInfo(Path.Combine(assemblyPath, culture)); if (assembliesFolder.Exists) { foreach (var assemblyFile in assembliesFolder.EnumerateFiles(Constants.SatelliteAssemblyExtension)) { AssemblyName assemblyName; try { assemblyName = AssemblyName.GetAssemblyName(assemblyFile.FullName); } catch { Console.WriteLine($"Not Satellite Assembly : {assemblyFile.Name}"); continue; } try { Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyFile.FullName))); Console.WriteLine($"Loaded : {assemblyName}"); } catch (Exception e) { Console.WriteLine($"Failed : {assemblyName}\n{e}"); } } } else { Console.WriteLine($"The satellite assemblies folder named '{culture}' is not found."); } } } private static Assembly ResolveDependencies(AssemblyLoadContext context, AssemblyName name) { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location) + Path.DirectorySeparatorChar + name.Name + ".dll"; if (File.Exists(assemblyPath)) { return context.LoadFromStream(new MemoryStream(File.ReadAllBytes(assemblyPath))); } else { return null; } } } }