diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 0083cce0..1d7ddc81 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -149,10 +149,16 @@ { folder = await FolderService.AddFolderAsync(folder); } - - await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId); - await logger.LogInformation("Folder Saved {Folder}", folder); - NavigationManager.NavigateTo(NavigateUrl()); + if (folder != null) + { + await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId); + await logger.LogInformation("Folder Saved {Folder}", folder); + NavigationManager.NavigateTo(NavigateUrl()); + } + else + { + AddModuleMessage("An Error Was Encountered Saving The Folder", MessageType.Error); + } } else { diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Edit.razor index 84e83aae..4faa3dd5 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Edit.razor @@ -4,9 +4,8 @@ @namespace [Owner].[Module]s.Modules @inherits ModuleBase +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate @@ -31,7 +30,6 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Actions => "Add,Edit"; - I[Module]Service [Module]Service; int _id; string _name; string _createdby; @@ -43,7 +41,6 @@ { try { - [Module]Service = new [Module]Service(http, sitestate); if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor index 1c92fc1a..fceedfea 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/Index.razor @@ -3,9 +3,8 @@ @namespace [Owner].[Module]s.Modules @inherits ModuleBase +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate @if (_[Module]s == null) { @@ -71,14 +70,12 @@ else @code { - I[Module]Service [Module]Service; List<[Module]> _[Module]s; protected override async Task OnInitializedAsync() { try { - [Module]Service = new [Module]Service(http, sitestate); _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs index 35a74a27..6a1f21b1 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/External/Client/ModuleInfo.cs @@ -10,7 +10,6 @@ namespace [Owner].[Module]s.Modules Name = "[Module]", Description = "[Module]", Version = "1.0.0", - Dependencies = "[Owner].[Module]s.Shared.Oqtane", ServerManagerType = "[ServerManagerType]", ReleaseVersions = "1.0.0" }; diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Edit.razor index 84e83aae..4faa3dd5 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Edit.razor @@ -4,9 +4,8 @@ @namespace [Owner].[Module]s.Modules @inherits ModuleBase +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate
@@ -31,7 +30,6 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Actions => "Add,Edit"; - I[Module]Service [Module]Service; int _id; string _name; string _createdby; @@ -43,7 +41,6 @@ { try { - [Module]Service = new [Module]Service(http, sitestate); if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor index fdb11b98..c2c129de 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/Index.razor @@ -3,9 +3,8 @@ @namespace [Owner].[Module]s.Modules @inherits ModuleBase +@inject I[Module]Service [Module]Service @inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate @if (_[Module]s == null) { @@ -62,14 +61,12 @@ else @code { - I[Module]Service [Module]Service; List<[Module]> _[Module]s; protected override async Task OnInitializedAsync() { try { - [Module]Service = new [Module]Service(http, sitestate); _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) diff --git a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs index a95a461e..6a1f21b1 100644 --- a/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs +++ b/Oqtane.Client/Modules/Admin/ModuleCreator/Templates/Internal/Oqtane.Client/Modules/[Module]/ModuleInfo.cs @@ -10,7 +10,6 @@ namespace [Owner].[Module]s.Modules Name = "[Module]", Description = "[Module]", Version = "1.0.0", - Dependencies = "[Owner].[Module]s.Module.Shared", ServerManagerType = "[ServerManagerType]", ReleaseVersions = "1.0.0" }; diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index 9df38687..c9c94bc7 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -3,9 +3,8 @@ @using Oqtane.Modules.Controls @namespace Oqtane.Modules.HtmlText @inherits ModuleBase +@inject IHtmlTextService HtmlTextService @inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate @if (_content != null) { @@ -14,7 +13,8 @@ Cancel @if (!string.IsNullOrEmpty(_content)) { -

+
+
} } @@ -35,8 +35,7 @@ { try { - var htmltextservice = new HtmlTextService(http, sitestate); - var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId); + var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); if (htmltext != null) { _content = htmltext.Content; @@ -65,19 +64,18 @@ try { - var htmltextservice = new HtmlTextService(http, sitestate); - var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId); + var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); if (htmltext != null) { htmltext.Content = content; - await htmltextservice.UpdateHtmlTextAsync(htmltext); + await HtmlTextService.UpdateHtmlTextAsync(htmltext); } else { htmltext = new HtmlTextInfo(); htmltext.ModuleId = ModuleState.ModuleId; htmltext.Content = content; - await htmltextservice.AddHtmlTextAsync(htmltext); + await HtmlTextService.AddHtmlTextAsync(htmltext); } await logger.LogInformation("Html/Text Content Saved {HtmlText}", htmltext); diff --git a/Oqtane.Client/Modules/HtmlText/Index.razor b/Oqtane.Client/Modules/HtmlText/Index.razor index c76afa02..6821c425 100644 --- a/Oqtane.Client/Modules/HtmlText/Index.razor +++ b/Oqtane.Client/Modules/HtmlText/Index.razor @@ -1,21 +1,13 @@ @using Oqtane.Modules.HtmlText.Services -@using Oqtane.Modules.HtmlText.Models @namespace Oqtane.Modules.HtmlText @inherits ModuleBase -@inject NavigationManager NavigationManager -@inject HttpClient http -@inject SiteState sitestate +@inject IHtmlTextService HtmlTextService @((MarkupString)content) @if (PageState.EditMode) { -
-} - -@if (PageState.EditMode) -{ -

+


} @code { @@ -25,8 +17,7 @@ { try { - var htmltextservice = new HtmlTextService(http, sitestate); - var htmltext = await htmltextservice.GetHtmlTextAsync(ModuleState.ModuleId); + var htmltext = await HtmlTextService.GetHtmlTextAsync(ModuleState.ModuleId); if (htmltext != null) { content = htmltext.Content; diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index cb73b9a4..51ab7887 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -8,7 +8,7 @@ using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Services { - public class HtmlTextService : ServiceBase, IHtmlTextService + public class HtmlTextService : ServiceBase, IHtmlTextService, IService { private readonly SiteState _siteState; diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index c81f8d10..188da769 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -9,7 +9,7 @@ using Oqtane.UI; namespace Oqtane.Modules { - public class ModuleBase : ComponentBase, IModuleControl + public abstract class ModuleBase : ComponentBase, IModuleControl { private Logger _logger; diff --git a/Oqtane.Client/Program.cs b/Oqtane.Client/Program.cs index 74fa8de4..26bac719 100644 --- a/Oqtane.Client/Program.cs +++ b/Oqtane.Client/Program.cs @@ -4,8 +4,10 @@ using System.Threading.Tasks; using Oqtane.Services; using System.Reflection; using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Net.Http.Json; using Oqtane.Modules; using Oqtane.Shared; using Oqtane.Providers; @@ -19,10 +21,9 @@ namespace Oqtane.Client { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("app"); + HttpClient httpClient = new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)}; - builder.Services.AddSingleton( - new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) } - ); + builder.Services.AddSingleton(httpClient); builder.Services.AddOptions(); // register auth services @@ -57,14 +58,16 @@ namespace Oqtane.Client builder.Services.AddScoped(); builder.Services.AddScoped(); + await LoadClientAssemblies(httpClient); + // dynamically register module contexts and repository services Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { - Type[] implementationtypes = assembly.GetTypes() - .Where(item => item.GetInterfaces().Contains(typeof(IService))) - .ToArray(); - foreach (Type implementationtype in implementationtypes) + var implementationTypes = assembly.GetTypes() + .Where(item => item.GetInterfaces().Contains(typeof(IService))); + + foreach (Type implementationtype in implementationTypes) { Type servicetype = Type.GetType(implementationtype.AssemblyQualifiedName.Replace(implementationtype.Name, "I" + implementationtype.Name)); if (servicetype != null) @@ -76,9 +79,27 @@ namespace Oqtane.Client builder.Services.AddScoped(implementationtype, implementationtype); // no interface defined for service } } + + assembly.GetInstances() + .ToList() + .ForEach(x => x.ConfigureServices(builder.Services)); } await builder.Build().RunAsync(); } + + private static async Task LoadClientAssemblies(HttpClient http) + { + var list = await http.GetFromJsonAsync>($"/~/api/ModuleDefinition/load"); + // get list of loaded assemblies on the client ( in the client-side hosting module the browser client has its own app domain ) + var assemblyList = AppDomain.CurrentDomain.GetAssemblies().Select(a => a.GetName().Name).ToList(); + foreach (var name in list) + { + if (assemblyList.Contains(name)) continue; + // download assembly from server and load + var bytes = await http.GetByteArrayAsync($"/~/api/ModuleDefinition/load/{name}.dll"); + Assembly.Load(bytes); + } + } } } diff --git a/Oqtane.Client/Services/AliasService.cs b/Oqtane.Client/Services/AliasService.cs index 1e0f5ac8..21df7dd0 100644 --- a/Oqtane.Client/Services/AliasService.cs +++ b/Oqtane.Client/Services/AliasService.cs @@ -5,16 +5,21 @@ using System.Linq; using System.Collections.Generic; using System.Net; using System; - +using Oqtane.Shared; namespace Oqtane.Services { public class AliasService : ServiceBase, IAliasService { - - public AliasService(HttpClient http) :base(http) { } - private string Apiurl => CreateApiUrl("Alias"); + private readonly SiteState _siteState; + + public AliasService(HttpClient http, SiteState siteState) : base(http) + { + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl(_siteState.Alias, "Alias"); public async Task> GetAliasesAsync() { diff --git a/Oqtane.Client/Services/JobLogService.cs b/Oqtane.Client/Services/JobLogService.cs index c15940b5..aa518771 100644 --- a/Oqtane.Client/Services/JobLogService.cs +++ b/Oqtane.Client/Services/JobLogService.cs @@ -3,14 +3,20 @@ using System.Threading.Tasks; using System.Net.Http; using System.Linq; using System.Collections.Generic; +using Oqtane.Shared; namespace Oqtane.Services { public class JobLogService : ServiceBase, IJobLogService { - public JobLogService(HttpClient http) :base(http) { } + private readonly SiteState _siteState; - private string Apiurl => CreateApiUrl("JobLog"); + public JobLogService(HttpClient http, SiteState siteState) : base(http) + { + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl(_siteState.Alias, "JobLog"); public async Task> GetJobLogsAsync() { diff --git a/Oqtane.Client/Services/JobService.cs b/Oqtane.Client/Services/JobService.cs index 14cd96ad..3161cd90 100644 --- a/Oqtane.Client/Services/JobService.cs +++ b/Oqtane.Client/Services/JobService.cs @@ -3,15 +3,21 @@ using System.Threading.Tasks; using System.Net.Http; using System.Linq; using System.Collections.Generic; +using Oqtane.Shared; namespace Oqtane.Services { public class JobService : ServiceBase, IJobService { - public JobService(HttpClient http) : base(http) { } + private readonly SiteState _siteState; - private string Apiurl => CreateApiUrl("Job"); + public JobService(HttpClient http, SiteState siteState) : base(http) + { + _siteState = siteState; + } + private string Apiurl => CreateApiUrl(_siteState.Alias, "Job"); + public async Task> GetJobsAsync() { List jobs = await GetJsonAsync>(Apiurl); diff --git a/Oqtane.Client/Services/ServiceBase.cs b/Oqtane.Client/Services/ServiceBase.cs index 57bad2b7..091709f2 100644 --- a/Oqtane.Client/Services/ServiceBase.cs +++ b/Oqtane.Client/Services/ServiceBase.cs @@ -135,13 +135,13 @@ namespace Oqtane.Services //TODO Missing content JSON validation } - // create an API Url which is tenant agnostic ( for use with entities in the MasterDB ) + // create an API Url which is tenant agnostic ( for use during installation ) public string CreateApiUrl(string serviceName) { return CreateApiUrl(null, serviceName); } - // create an API Url which is tenant aware ( for use with entities in the TenantDB ) + // create an API Url which is tenant aware ( for use with repositories ) public string CreateApiUrl(Alias alias, string serviceName) { string apiurl = "/"; diff --git a/Oqtane.Client/Services/SqlService.cs b/Oqtane.Client/Services/SqlService.cs index a99fbd23..719156b2 100644 --- a/Oqtane.Client/Services/SqlService.cs +++ b/Oqtane.Client/Services/SqlService.cs @@ -1,4 +1,5 @@ using Oqtane.Models; +using Oqtane.Shared; using System.Net.Http; using System.Threading.Tasks; @@ -6,9 +7,14 @@ namespace Oqtane.Services { public class SqlService : ServiceBase, ISqlService { - public SqlService(HttpClient http) : base(http) { } + private readonly SiteState _siteState; - private string Apiurl => CreateApiUrl("Sql"); + public SqlService(HttpClient http, SiteState siteState) : base(http) + { + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl(_siteState.Alias, "Sql"); public async Task ExecuteQueryAsync(SqlQuery sqlquery) { diff --git a/Oqtane.Client/Services/TenantService.cs b/Oqtane.Client/Services/TenantService.cs index d8e9be02..d644348e 100644 --- a/Oqtane.Client/Services/TenantService.cs +++ b/Oqtane.Client/Services/TenantService.cs @@ -3,14 +3,20 @@ using System.Net.Http; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; +using Oqtane.Shared; namespace Oqtane.Services { public class TenantService : ServiceBase, ITenantService { - public TenantService(HttpClient http) : base(http) { } + private readonly SiteState _siteState; - private string Apiurl => CreateApiUrl("Tenant"); + public TenantService(HttpClient http, SiteState siteState) : base(http) + { + _siteState = siteState; + } + + private string Apiurl => CreateApiUrl(_siteState.Alias, "Tenant"); public async Task> GetTenantsAsync() { diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 6c3223c9..0c30bbdf 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -16,6 +16,7 @@ using System.Net; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; +using Microsoft.AspNetCore.Routing.Constraints; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -194,7 +195,7 @@ namespace Oqtane.Controllers CreateDirectory(folderPath); string filename = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1); // check for allowable file extensions - if (Constants.UploadableFiles.Contains(Path.GetExtension(filename).Replace(".", ""))) + if (Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(filename).ToLower().Replace(".", ""))) { try { @@ -317,7 +318,7 @@ namespace Oqtane.Controllers } // check for allowable file extensions - if (!Constants.UploadableFiles.Contains(Path.GetExtension(filename)?.Replace(".", ""))) + if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(filename)?.ToLower().Replace(".", ""))) { System.IO.File.Delete(Path.Combine(folder, filename + ".tmp")); } @@ -396,12 +397,13 @@ namespace Oqtane.Controllers [HttpGet("download/{id}")] public IActionResult Download(int id) { + string errorpath = Path.Combine(GetFolderPath("images"), "error.png"); Models.File file = _files.GetFile(id); if (file != null) { if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.Permissions)) { - string filepath = Path.Combine(GetFolderPath(file.Folder) , file.Name); + string filepath = Path.Combine(GetFolderPath(file.Folder), file.Name); if (System.IO.File.Exists(filepath)) { byte[] filebytes = System.IO.File.ReadAllBytes(filepath); @@ -411,21 +413,24 @@ namespace Oqtane.Controllers { _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FileId} {FilePath}", id, filepath); HttpContext.Response.StatusCode = 404; - return null; + byte[] filebytes = System.IO.File.ReadAllBytes(errorpath); + return File(filebytes, "application/octet-stream", file.Name); } } else { _logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access File {FileId}", id); HttpContext.Response.StatusCode = 401; - return null; + byte[] filebytes = System.IO.File.ReadAllBytes(errorpath); + return File(filebytes, "application/octet-stream", file.Name); } } else { _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Not Found {FileId}", id); HttpContext.Response.StatusCode = 404; - return null; + byte[] filebytes = System.IO.File.ReadAllBytes(errorpath); + return File(filebytes, "application/octet-stream", "error.png"); } } @@ -469,7 +474,7 @@ namespace Oqtane.Controllers file.ImageHeight = 0; file.ImageWidth = 0; - if (Constants.ImageFiles.Contains(file.Extension)) + if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower())) { FileStream stream = new FileStream(filepath, FileMode.Open, FileAccess.Read); using (var image = Image.FromStream(stream)) diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 68a01e23..34c13a1b 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -10,7 +10,6 @@ using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; -using System.IO; namespace Oqtane.Controllers { @@ -106,13 +105,23 @@ namespace Oqtane.Controllers } if (_userPermissions.IsAuthorized(User,PermissionNames.Edit, permissions)) { - if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null) + if (FolderPathValid(folder)) { - Folder parent = _folders.GetFolder(folder.ParentId.Value); - folder.Path = Utilities.PathCombine(parent.Path, folder.Name,"\\"); + if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null) + { + Folder parent = _folders.GetFolder(folder.ParentId.Value); + folder.Path = Utilities.PathCombine(parent.Path, folder.Name); + } + folder.Path = Utilities.PathCombine(folder.Path, "\\"); + folder = _folders.AddFolder(folder); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder); + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Name Not Valid {Folder}", folder); + HttpContext.Response.StatusCode = 401; + folder = null; } - folder = _folders.AddFolder(folder); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder); } else { @@ -131,13 +140,23 @@ namespace Oqtane.Controllers { if (ModelState.IsValid && _userPermissions.IsAuthorized(User, EntityNames.Folder, folder.FolderId, PermissionNames.Edit)) { - if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null) + if (FolderPathValid(folder)) { - Folder parent = _folders.GetFolder(folder.ParentId.Value); - folder.Path = Utilities.PathCombine(parent.Path, folder.Name,"\\"); + if (string.IsNullOrEmpty(folder.Path) && folder.ParentId != null) + { + Folder parent = _folders.GetFolder(folder.ParentId.Value); + folder.Path = Utilities.PathCombine(parent.Path, folder.Name); + } + folder.Path = Utilities.PathCombine(folder.Path, "\\"); + folder = _folders.UpdateFolder(folder); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Updated {Folder}", folder); + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Name Not Valid {Folder}", folder); + HttpContext.Response.StatusCode = 401; + folder = null; } - folder = _folders.UpdateFolder(folder); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Updated {Folder}", folder); } else { @@ -191,5 +210,11 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = 401; } } + + private bool FolderPathValid(Folder folder) + { + // prevent folder path traversal and reserved devices + return (!folder.Name.Contains("\\") && !folder.Name.Contains("/") && !Constants.ReservedDevices.Split(',').Contains(folder.Name.ToUpper())); + } } } diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index e1b9534c..fcd9dcd8 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -170,6 +170,16 @@ namespace Oqtane.Controllers return null; } } + // GET api//load/assembyname + [HttpGet("load")] + public List Load() + { + var assemblies = AppDomain.CurrentDomain.GetOqtaneClientAssemblies(); + var list = AppDomain.CurrentDomain.GetOqtaneClientAssemblies().Select(a => a.GetName().Name).ToList(); + var deps = assemblies.SelectMany(a => a.GetReferencedAssemblies()).Distinct(); + list.AddRange(deps.Where(a=>a.Name.EndsWith(".oqtane",StringComparison.OrdinalIgnoreCase)).Select(a=>a.Name)); + return list; + } // POST api/?moduleid=x [HttpPost] diff --git a/Oqtane.Server/Controllers/SiteTemplateController.cs b/Oqtane.Server/Controllers/SiteTemplateController.cs index 2f709fb1..c63170c1 100644 --- a/Oqtane.Server/Controllers/SiteTemplateController.cs +++ b/Oqtane.Server/Controllers/SiteTemplateController.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Oqtane.Models; using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.Controllers { @@ -17,6 +19,7 @@ namespace Oqtane.Controllers // GET: api/ [HttpGet] + [Authorize(Roles = Constants.HostRole)] public IEnumerable Get() { return _siteTemplates.GetSiteTemplates(); diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs new file mode 100644 index 00000000..d1c77301 --- /dev/null +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Oqtane.Infrastructure; + +namespace Oqtane.Extensions +{ + public static class ApplicationBuilderExtensions + { + public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env) + { + var startUps = AppDomain.CurrentDomain + .GetOqtaneAssemblies() + .SelectMany(x => x.GetInstances()); + + foreach (var startup in startUps) + { + startup.Configure(app, env); + } + + return app; + } + } +} diff --git a/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs index fdb801df..b3f489bf 100644 --- a/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneMvcBuilderExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Oqtane.Infrastructure; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection @@ -30,6 +31,22 @@ namespace Microsoft.Extensions.DependencyInjection } } } + + return mvcBuilder; + } + + + public static IMvcBuilder ConfigureOqtaneMvc(this IMvcBuilder mvcBuilder) + { + var startUps = AppDomain.CurrentDomain + .GetOqtaneAssemblies() + .SelectMany(x => x.GetInstances()); + + foreach (var startup in startUps) + { + startup.ConfigureMvc(mvcBuilder); + } + return mvcBuilder; } } diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 5c093f48..3e778869 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -8,21 +8,23 @@ using Microsoft.Extensions.Hosting; using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Modules; +using Oqtane.Services; using Oqtane.Shared; +using Oqtane.UI; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtaneParts(this IServiceCollection services) + public static IServiceCollection AddOqtaneParts(this IServiceCollection services, Runtime runtime) { LoadAssemblies(); - services.AddOqtaneServices(); + services.AddOqtaneServices(runtime); return services; } - private static IServiceCollection AddOqtaneServices(this IServiceCollection services) + private static IServiceCollection AddOqtaneServices(this IServiceCollection services, Runtime runtime) { if (services is null) { @@ -53,11 +55,24 @@ namespace Microsoft.Extensions.DependencyInjection services.AddSingleton(hostedServiceType, serviceType); } } + + var startUps = assembly.GetInstances(); + foreach (var startup in startUps) + { + startup.ConfigureServices(services); + } + + if (runtime == Runtime.Server) + { + assembly.GetInstances() + .ToList() + .ForEach(x => x.ConfigureServices(services)); + } } - return services; } + private static void LoadAssemblies() { var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs b/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs new file mode 100644 index 00000000..bbdff5d6 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/IServerStartup.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Infrastructure +{ + public interface IServerStartup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + void ConfigureServices(IServiceCollection services); + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + void Configure(IApplicationBuilder app, IWebHostEnvironment env); + void ConfigureMvc(IMvcBuilder mvcBuilder); + } +} + diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index de4c89a7..34f73ffe 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -14,11 +14,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using Oqtane.Services; -using Oqtane.Shared; +using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane { @@ -26,6 +28,7 @@ namespace Oqtane { public IConfigurationRoot Configuration { get; } private string _webRoot; + private Runtime _runtime; public Startup(IWebHostEnvironment env) { @@ -33,6 +36,9 @@ namespace Oqtane .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); Configuration = builder.Build(); + + _runtime = (Configuration.GetSection("Runtime").Value == "WebAssembly") ? Runtime.WebAssembly : Runtime.Server; + _webRoot = env.WebRootPath; AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(env.ContentRootPath, "Data")); } @@ -187,14 +193,13 @@ namespace Oqtane services.AddTransient(); services.AddTransient(); - // load the external assemblies into the app domain - services.AddOqtaneParts(); + // load the external assemblies into the app domain, install services + services.AddOqtaneParts(_runtime); services.AddMvc() + .AddNewtonsoftJson() .AddOqtaneApplicationParts() // register any Controllers from custom modules - .AddNewtonsoftJson(); - - + .ConfigureOqtaneMvc(); // any additional configuration from IStart classes. services.AddSwaggerGen(c => { @@ -217,14 +222,12 @@ namespace Oqtane // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } - app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseBlazorFrameworkFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); - app.UseSwagger(); app.UseSwaggerUI(c => { @@ -237,6 +240,7 @@ namespace Oqtane endpoints.MapControllers(); endpoints.MapFallbackToPage("/_Host"); }); + app.ConfigureOqtaneAssemblies(env); } } } diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css index a9e5c15e..fba5b909 100644 --- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css +++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css @@ -101,6 +101,12 @@ flex-direction: row; } + .app-logo { + display: block; + margin-left: auto; + margin-right: auto; + } + .breadcrumbs { position: fixed; left: 275px; @@ -163,7 +169,13 @@ } } -@media (max-width: 767px) { +@media (max-width: 767px) { + .app-logo { + height: 80px; + display: flex; + align-items: center; + } + .breadcrumbs { position: fixed; top: 150px; diff --git a/Oqtane.Server/wwwroot/images/error.png b/Oqtane.Server/wwwroot/images/error.png new file mode 100644 index 00000000..0095d2f1 Binary files /dev/null and b/Oqtane.Server/wwwroot/images/error.png differ diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 50c8aa9b..5f41709c 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -81,14 +81,26 @@ window.interop = { if (link.href !== url) { link.setAttribute('href', url); } - if (type !== "" && link.type !== type) { - link.setAttribute('type', type); + if (type !== "") { + if (link.type !== type) { + link.setAttribute('type', type); + } + } else { + link.removeAttribute('type'); } - if (integrity !== "" && link.integrity !== integrity) { - link.setAttribute('integrity', integrity); + if (integrity !== "") { + if (link.integrity !== integrity) { + link.setAttribute('integrity', integrity); + } + } else { + link.removeAttribute('integrity'); } - if (crossorigin !== "" && link.crossOrigin !== crossorigin) { - link.setAttribute('crossorigin', crossorigin); + if (crossorigin !== "") { + if (link.crossOrigin !== crossorigin) { + link.setAttribute('crossorigin', crossorigin); + } + } else { + link.removeAttribute('crossorigin'); } } }, @@ -126,11 +138,19 @@ window.interop = { if (script.src !== src) { script.src = src; } - if (integrity !== "" && script.integrity !== integrity) { - script.setAttribute('integrity', integrity); + if (integrity !== "") { + if (script.integrity !== integrity) { + script.setAttribute('integrity', integrity); + } + } else { + script.removeAttribute('integrity'); } - if (crossorigin !== "" && script.crossorigin !== crossorigin) { - script.setAttribute('crossorigin', crossorigin); + if (crossorigin !== "") { + if (script.crossOrigin !== crossorigin) { + script.setAttribute('crossorigin', crossorigin); + } + } else { + script.removeAttribute('crossorigin'); } } else { diff --git a/Oqtane.Server/Extensions/AssemblyExtensions.cs b/Oqtane.Shared/Extensions/AssemblyExtensions.cs similarity index 54% rename from Oqtane.Server/Extensions/AssemblyExtensions.cs rename to Oqtane.Shared/Extensions/AssemblyExtensions.cs index 443b8cc6..4742d368 100644 --- a/Oqtane.Server/Extensions/AssemblyExtensions.cs +++ b/Oqtane.Shared/Extensions/AssemblyExtensions.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Oqtane.Modules; +using Oqtane.Services; using Oqtane.Shared; +using Oqtane.Themes; // ReSharper disable once CheckNamespace namespace System.Reflection @@ -31,7 +34,29 @@ namespace System.Reflection } return assembly.GetTypes() - .Where(t => t.GetInterfaces().Contains(interfaceType)); + //.Where(t => t.GetInterfaces().Contains(interfaceType)); + .Where(x => interfaceType.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract); + } + + public static IEnumerable GetTypes(this Assembly assembly) + { + return assembly.GetTypes(typeof(T)); + } + + public static IEnumerable GetInstances(this Assembly assembly) where T : class + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + var type = typeof(T); + var list = assembly.GetTypes() + .Where(x => type.IsAssignableFrom(x) && !x.IsInterface && !x.IsAbstract && !x.IsGenericType); + + foreach (var type1 in list) + { + if (Activator.CreateInstance(type1) is T instance) yield return instance; + } } public static bool IsOqtaneAssembly(this Assembly assembly) @@ -48,5 +73,10 @@ namespace System.Reflection { return appDomain.GetAssemblies().Where(a => a.IsOqtaneAssembly()); } + public static IEnumerable GetOqtaneClientAssemblies(this AppDomain appDomain) + { + return appDomain.GetOqtaneAssemblies() + .Where(a => a.GetTypes().Any() || a.GetTypes().Any() || a.GetTypes().Any()); + } } } diff --git a/Oqtane.Shared/Interfaces/IClientStartup.cs b/Oqtane.Shared/Interfaces/IClientStartup.cs new file mode 100644 index 00000000..c063431c --- /dev/null +++ b/Oqtane.Shared/Interfaces/IClientStartup.cs @@ -0,0 +1,11 @@ + +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Services +{ + public interface IClientStartup + { + // This method gets called by the runtime. Use this method to add services to the container. + void ConfigureServices(IServiceCollection services); + } +} diff --git a/Oqtane.Client/Modules/IModuleControl.cs b/Oqtane.Shared/Interfaces/IModuleControl.cs similarity index 80% rename from Oqtane.Client/Modules/IModuleControl.cs rename to Oqtane.Shared/Interfaces/IModuleControl.cs index c4f2fdee..ee5120ba 100644 --- a/Oqtane.Client/Modules/IModuleControl.cs +++ b/Oqtane.Shared/Interfaces/IModuleControl.cs @@ -7,6 +7,6 @@ namespace Oqtane.Modules SecurityAccessLevel SecurityAccessLevel { get; } // defines the security access level for this control - defaults to View string Title { get; } // title to display for this control - defaults to module title string Actions { get; } // allows for routing by configuration rather than by convention ( comma delimited ) - defaults to using component file name - bool UseAdminContainer { get; } // container for embedding module control - defaults to true + bool UseAdminContainer { get; } // container for embedding module control - defaults to true. false will suppress the default modal UI popup behavior and render the component in the page. } } diff --git a/Oqtane.Client/Themes/IThemeControl.cs b/Oqtane.Shared/Interfaces/IThemeControl.cs similarity index 100% rename from Oqtane.Client/Themes/IThemeControl.cs rename to Oqtane.Shared/Interfaces/IThemeControl.cs diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index c50111cd..eca4e4ee 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -18,6 +18,7 @@ + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index c50347e2..30ca2950 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -43,5 +43,6 @@ public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png"; public const string UploadableFiles = "jpg,jpeg,jpe,gif,bmp,png,mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg"; + public const string ReservedDevices = "CON,NUL,PRN,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9"; } }