From cfefe35e3fe8097e3772f568b9037ebd963e3ade Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 20 Dec 2024 16:56:05 -0500 Subject: [PATCH 001/118] Update README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3280b496..cb5e7da8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[6.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0) was released on November 14, 2024 and is a major release including 39 pull requests by 6 different contributors, pushing the total number of project commits all-time to over 6000. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) was released on December 20, 2024 and is a maintenance release including 58 pull requests by 7 different contributors, pushing the total number of project commits all-time to over 6100. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) @@ -86,6 +86,10 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[6.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1) (Dec 20, 2024) +- [x] Stabilization improvements +- [x] JavaScript improvements in Blazor Static Server Rendering (SSR) + [6.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0) (Nov 14, 2024) - [x] Migration to .NET 9 From af7b4db062f81b93d7b0309cac3be42586f1a4dd Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 23 Dec 2024 22:10:51 +0800 Subject: [PATCH 002/118] Fix #4947: check the 2FA settings. --- Oqtane.Server/Managers/UserManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 7745f429..fa265f19 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -512,7 +512,10 @@ namespace Oqtane.Managers user = _users.GetUser(user.Username); if (user != null) { - if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) + var alias = _tenantManager.GetAlias(); + var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false"; + var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired; + if (twoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) { user.IsAuthenticated = true; } From 1a925221b7bbe3233a4f4a13ca33dc93b0a3a70c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Dec 2024 09:26:50 -0500 Subject: [PATCH 003/118] fix #4946 - allow administrators to access user roles via API --- Oqtane.Server/Controllers/UserRoleController.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/Controllers/UserRoleController.cs b/Oqtane.Server/Controllers/UserRoleController.cs index 38f89705..124e0597 100644 --- a/Oqtane.Server/Controllers/UserRoleController.cs +++ b/Oqtane.Server/Controllers/UserRoleController.cs @@ -42,7 +42,7 @@ namespace Oqtane.Controllers int UserId = -1; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && (userid != null && int.TryParse(userid, out UserId) || rolename != null)) { - if (IsAuthorized(UserId, rolename)) + if (IsAuthorized(UserId, rolename, SiteId)) { var userroles = _userRoles.GetUserRoles(SiteId).ToList(); if (UserId != -1) @@ -82,7 +82,7 @@ namespace Oqtane.Controllers public UserRole Get(int id) { var userrole = _userRoles.GetUserRole(id); - if (userrole != null && SiteValid(userrole.Role.SiteId) && IsAuthorized(userrole.UserId, userrole.Role.Name)) + if (userrole != null && SiteValid(userrole.Role.SiteId) && IsAuthorized(userrole.UserId, userrole.Role.Name, userrole.Role.SiteId ?? -1)) { return Filter(userrole, _userPermissions.GetUser().UserId); } @@ -101,17 +101,21 @@ namespace Oqtane.Controllers } } - private bool IsAuthorized(int userId, string roleName) + private bool IsAuthorized(int userId, string roleName, int siteId) { bool authorized = true; if (userId != -1) { - authorized = _userPermissions.GetUser(User).UserId == userId; + authorized = (_userPermissions.GetUser(User).UserId == userId); } if (authorized && !string.IsNullOrEmpty(roleName)) { authorized = User.IsInRole(roleName); } + if (!authorized) + { + authorized = _userPermissions.IsAuthorized(User, siteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin); + } return authorized; } From 1e71e32c74dd59a0581d10ebf08ecc189fed6474 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Dec 2024 11:16:34 -0500 Subject: [PATCH 004/118] remove unnecessary reference to System.Text.Json in Shared project --- Oqtane.Shared/Oqtane.Shared.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index e5209a21..0835b56b 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -23,7 +23,6 @@ - From 36d5747b4f0fd319bad712a1eb87495e41bce448 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Dec 2024 11:24:24 -0500 Subject: [PATCH 005/118] update package references --- .../Oqtane.Database.PostgreSQL.csproj | 2 +- Oqtane.Server/Oqtane.Server.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index ef79fea1..e99d360b 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -35,7 +35,7 @@ - + diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index ba241482..baf54968 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -36,7 +36,7 @@ - + all @@ -45,10 +45,10 @@ - - + + - + From 997e9213f2ca4028256ae8148398df29e1bb928a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Dec 2024 14:09:54 -0500 Subject: [PATCH 006/118] updated default module template to use Service consistently --- .../Client/Services/[Module]Service.cs | 4 +- .../Server/Controllers/[Module]Controller.cs | 44 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs index 23f10772..94baa74d 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs @@ -21,7 +21,7 @@ namespace [Owner].Module.[Module].Services public async Task Get[Module]Async(int [Module]Id, int ModuleId) { - return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", EntityNames.Module, ModuleId)); + return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId)); } public async Task Add[Module]Async(Models.[Module] [Module]) @@ -36,7 +36,7 @@ namespace [Owner].Module.[Module].Services public async Task Delete[Module]Async(int [Module]Id, int ModuleId) { - await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", EntityNames.Module, ModuleId)); + await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId)); } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs index 559f9697..5dd25b2b 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Controllers/[Module]Controller.cs @@ -5,31 +5,32 @@ using Microsoft.AspNetCore.Http; using Oqtane.Shared; using Oqtane.Enums; using Oqtane.Infrastructure; -using [Owner].Module.[Module].Repository; +using [Owner].Module.[Module].Services; using Oqtane.Controllers; using System.Net; +using System.Threading.Tasks; namespace [Owner].Module.[Module].Controllers { [Route(ControllerRoutes.ApiRoute)] public class [Module]Controller : ModuleControllerBase { - private readonly I[Module]Repository _[Module]Repository; + private readonly I[Module]Service _[Module]Service; - public [Module]Controller(I[Module]Repository [Module]Repository, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) + public [Module]Controller(I[Module]Service [Module]Service, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) { - _[Module]Repository = [Module]Repository; + _[Module]Service = [Module]Service; } // GET: api/?moduleid=x [HttpGet] [Authorize(Policy = PolicyNames.ViewModule)] - public IEnumerable Get(string moduleid) + public async Task> Get(string moduleid) { int ModuleId; if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) { - return _[Module]Repository.Get[Module]s(ModuleId); + return await _[Module]Service.Get[Module]sAsync(ModuleId); } else { @@ -40,18 +41,18 @@ namespace [Owner].Module.[Module].Controllers } // GET api//5 - [HttpGet("{id}")] + [HttpGet("{id}/{moduleid}")] [Authorize(Policy = PolicyNames.ViewModule)] - public Models.[Module] Get(int id) + public async Task Get(int id, int moduleid) { - Models.[Module] [Module] = _[Module]Repository.Get[Module](id); + Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid); if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) { return [Module]; } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", id, moduleid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; return null; } @@ -60,12 +61,11 @@ namespace [Owner].Module.[Module].Controllers // POST api/ [HttpPost] [Authorize(Policy = PolicyNames.EditModule)] - public Models.[Module] Post([FromBody] Models.[Module] [Module]) + public async Task Post([FromBody] Models.[Module] [Module]) { if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) { - [Module] = _[Module]Repository.Add[Module]([Module]); - _logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]); + [Module] = await _[Module]Service.Add[Module]Async([Module]); } else { @@ -79,12 +79,11 @@ namespace [Owner].Module.[Module].Controllers // PUT api//5 [HttpPut("{id}")] [Authorize(Policy = PolicyNames.EditModule)] - public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module]) + public async Task Put(int id, [FromBody] Models.[Module] [Module]) { - if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null) + if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) { - [Module] = _[Module]Repository.Update[Module]([Module]); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]); + [Module] = await _[Module]Service.Update[Module]Async([Module]); } else { @@ -96,19 +95,18 @@ namespace [Owner].Module.[Module].Controllers } // DELETE api//5 - [HttpDelete("{id}")] + [HttpDelete("{id}/{moduleid}")] [Authorize(Policy = PolicyNames.EditModule)] - public void Delete(int id) + public async Task Delete(int id, int moduleid) { - Models.[Module] [Module] = _[Module]Repository.Get[Module](id); + Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid); if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) { - _[Module]Repository.Delete[Module](id); - _logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", id); + await _[Module]Service.Delete[Module]Async(id, [Module].ModuleId); } else { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id}", id); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", id, moduleid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } From 2d9396b2458d2bf8913e712043c206b00a84988e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 23 Dec 2024 14:47:17 -0500 Subject: [PATCH 007/118] add back System.Text.Json to Shared project (#4929) --- Oqtane.Shared/Oqtane.Shared.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 0835b56b..e5209a21 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -23,6 +23,7 @@ + From d976cc6c19ee566fe4c1d6d590bdc05478591ef2 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 25 Dec 2024 10:04:25 -0500 Subject: [PATCH 008/118] Update SECURITY.md --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 35181493..0f88a666 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Reporting a Vulnerability -We make every effort to ensure rapid and thorough analysis of reported issues and, where appropriate, provide workarounds and updated application releases to fix them. If you identify a potential security vulnerability please report it via [the GitHub feature for reporting a security vulnerability](https://github.com/oqtane/oqtane.framework/security/advisories/new). +We make every effort to ensure rapid and thorough analysis of reported issues and, where appropriate, provide workarounds and updated application releases to fix them. If you identify a potential security vulnerability please report it via support@oqtane.org. All submitted information is viewed only by members of the Oqtane Security Team, and will not be discussed outside the Team without the permission of the person/company who reported the issue. Each confirmed issue is assigned a severity level (critical, moderate, or low) corresponding to its potential impact on an Oqtane installation. @@ -12,4 +12,4 @@ All submitted information is viewed only by members of the Oqtane Security Team, Once an issue has been resolved via a public release of Oqtane, the release notes on GitHub are updated to reflect that security bulletins exist for the release. We strongly suggest using the "Watch" option on GitHub for "Releases" at a minimum to receive notifications of updated Oqtane releases. -As a general policy, Oqtane does not issue Hot Fix releases to prior versions of the software. If a remediation is possible via configuration it shall be noted as applicable in the posted bulletins. \ No newline at end of file +As a general policy, Oqtane does not issue Hot Fix releases to prior versions of the software. If a remediation is possible via configuration it shall be noted as applicable in the posted bulletins. From 44ce68097beb15f8d40302ea4996c0f1ddc47f15 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 31 Dec 2024 08:17:58 -0500 Subject: [PATCH 009/118] fix #4957 - unable to login after password reset --- Oqtane.Server/Managers/UserManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index fa265f19..c1a91f5d 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -490,6 +490,9 @@ namespace Oqtane.Managers var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password); if (result.Succeeded) { + user = _users.GetUser(user.Username); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username); user.Password = ""; } From cc5727b7fa2518f3069979322a89e3d54ecf5583 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:16:35 -0800 Subject: [PATCH 010/118] Prepare 6.0.2 and update package dependencies --- Oqtane.Maui/Oqtane.Maui.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 01d3486d..6b696214 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -30,7 +30,7 @@ com.oqtane.maui - 6.0.1 + 6.0.2 1 @@ -72,9 +72,9 @@ - - - + + + From 7184f7f6355192670f3c5c8a8a31a1a11cec141a Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:20:13 -0800 Subject: [PATCH 011/118] Prepare 6.0.2 and update package dependencies --- Oqtane.Server/Oqtane.Server.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index baf54968..a9060e71 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -47,7 +47,7 @@ - + From e8a41ccb47df2e9aed8ee6bab318f238e152e70e Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:21:10 -0800 Subject: [PATCH 012/118] Prepare 6.0.2 --- Oqtane.Client/Oqtane.Client.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index d0d4769f..c08c9181 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net9.0 Exe Debug;Release - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane From 2bb5494b843df5b5fe7ded6aaee8b59cffe63209 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:22:10 -0800 Subject: [PATCH 013/118] Prepare 6.0.2 --- Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index b9ec837a..68095cbc 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git true From 1cfbf61a305feafacc051cd8b0b07cdd10b5095b Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:23:15 -0800 Subject: [PATCH 014/118] Prepare 6.0.2 --- Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index e99d360b..2e66cbfc 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git true From 5ef2e49d9c79e92126fed92e9a1cd4c836309fdf Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:25:34 -0800 Subject: [PATCH 015/118] Prepare 6.0.2 --- Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 8313aa59..2fad42e3 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git true From efafe89b425b7c4e842e6aae8de4e718a4173dad Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:26:45 -0800 Subject: [PATCH 016/118] Prepare 6.0.2 --- Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index 266edec6..4212d9fe 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git true From aa32beb341652c663e54c9281fb07f295a811447 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:28:32 -0800 Subject: [PATCH 017/118] Update Oqtane.Shared.csproj --- Oqtane.Shared/Oqtane.Shared.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index e5209a21..1c72ab77 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane From b35e4bddd014408ac9581abc5754c952e0ea7954 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:28:48 -0800 Subject: [PATCH 018/118] Prepare 6.0.2 From ec0a77230c8d84659249408168950d9449ac93dc Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:29:32 -0800 Subject: [PATCH 019/118] Prepare 6.0.2 --- Oqtane.Package/install.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index dee9ddf2..b94436f3 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.1.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.2.Install.zip" -Force From 2aef96ad4f2b7e17827a6307a75fcf89fb2e26f0 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:29:52 -0800 Subject: [PATCH 020/118] Prepare 6.0.2 --- Oqtane.Package/upgrade.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 5e734637..5fbb0191 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.1.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.2.Upgrade.zip" -Force From 3aea412fe90fddd66d62c6a6060acead61dfb8e4 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:31:23 -0800 Subject: [PATCH 021/118] Prepare 6.0.2 --- Oqtane.Package/Oqtane.Updater.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 89768e12..ea5a8715 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 6.0.1 + 6.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 readme.md icon.png oqtane From 4b8b93e1b8f09b4afe7d5e2c79df46c794148f3e Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:31:47 -0800 Subject: [PATCH 022/118] Prepare 6.0.2 --- Oqtane.Package/Oqtane.Shared.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 885247ad..1b8db074 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 6.0.1 + 6.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 readme.md icon.png oqtane From 127b2ca86d49e1e9975b0b5b7db2e0a5b7dba8f1 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:32:09 -0800 Subject: [PATCH 023/118] Prepare 6.0.2 --- Oqtane.Package/Oqtane.Server.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 42d74fe8..8e0a0fcc 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 6.0.1 + 6.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 readme.md icon.png oqtane From f4b00b01d0131353b67e2ab2c6c18707cfb25f21 Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:34:21 -0800 Subject: [PATCH 024/118] Prepare 6.0.2 --- Oqtane.Package/Oqtane.Framework.nuspec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 60049a32..a268e043 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 6.0.1 + 6.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v6.0.1/Oqtane.Framework.6.0.1.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/download/v6.0.2/Oqtane.Framework.6.0.2.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 readme.md icon.png oqtane framework From 96cc726e22a6996a4b1e657d642364b5627709ba Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:35:00 -0800 Subject: [PATCH 025/118] Prepare 6.0.2 --- Oqtane.Package/Oqtane.Client.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 3620b920..1d82676d 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 6.0.1 + 6.0.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 readme.md icon.png oqtane From 26bb743679459ac517315f4a279f1ef5b4a6441f Mon Sep 17 00:00:00 2001 From: Cody Date: Tue, 31 Dec 2024 10:37:27 -0800 Subject: [PATCH 026/118] Prepare 6.0.2 --- Oqtane.Updater/Oqtane.Updater.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 24dd1348..79332b79 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net9.0 Exe - 6.0.1 + 6.0.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 https://github.com/oqtane/oqtane.framework Git Oqtane From af4e19a57e570eff51846e6ce5f965accf390c5f Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Tue, 7 Jan 2025 04:14:31 +0100 Subject: [PATCH 027/118] Removed unused Using statements from the SiteTemplates --- .../Infrastructure/SiteTemplates/AdminSiteTemplate.cs | 6 +++--- .../SiteTemplates/DefaultSiteTemplate.cs | 11 +++++------ .../Infrastructure/SiteTemplates/EmptySiteTemplate.cs | 8 +++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index e3fd1cd4..bbfe9eb9 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -1,8 +1,8 @@ -using Oqtane.Models; -using Oqtane.Infrastructure; using System.Collections.Generic; -using Oqtane.Shared; using Oqtane.Documentation; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Shared; namespace Oqtane.SiteTemplates { diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 96b762f9..d6f41e91 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -1,12 +1,11 @@ -using Oqtane.Models; -using Oqtane.Infrastructure; using System.Collections.Generic; -using Oqtane.Repository; -using Microsoft.AspNetCore.Hosting; -using Oqtane.Extensions; -using Oqtane.Shared; using System.IO; +using Microsoft.AspNetCore.Hosting; using Oqtane.Documentation; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.SiteTemplates { diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs index 4f03604c..265fca52 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs @@ -1,10 +1,8 @@ -using Oqtane.Models; -using Oqtane.Infrastructure; using System.Collections.Generic; -using Oqtane.Extensions; -using Oqtane.Repository; -using Oqtane.Shared; using Oqtane.Documentation; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Shared; namespace Oqtane.SiteTemplates { From 5a4cdc5354bbab378362695c4c65a552da33c1f3 Mon Sep 17 00:00:00 2001 From: beolafsen <76835718+beolafsen@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:31:35 +0100 Subject: [PATCH 028/118] Issue #4977 --- Oqtane.Client/Themes/Controls/Theme/Login.razor | 8 +++++--- Oqtane.Client/Themes/Controls/Theme/UserProfile.razor | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/Login.razor b/Oqtane.Client/Themes/Controls/Theme/Login.razor index 8b01e34c..69bd2922 100644 --- a/Oqtane.Client/Themes/Controls/Theme/Login.razor +++ b/Oqtane.Client/Themes/Controls/Theme/Login.razor @@ -8,14 +8,14 @@ { @if (PageState.Runtime == Runtime.Hybrid) { - + } else {
- +
} } @@ -23,7 +23,7 @@ { @if (ShowLogin) { - @SharedLocalizer["Login"] + @SharedLocalizer["Login"] } } @@ -32,4 +32,6 @@ { [Parameter] public bool ShowLogin { get; set; } = true; + [Parameter] + public string CssClass { get; set; } = "btn btn-primary"; } \ No newline at end of file diff --git a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor index 7646d320..b418a602 100644 --- a/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor +++ b/Oqtane.Client/Themes/Controls/Theme/UserProfile.razor @@ -8,13 +8,13 @@ @if (PageState.User != null) { - @PageState.User.Username + @PageState.User.Username } else { @if (ShowRegister && PageState.Site.AllowRegistration) { - @Localizer["Register"] + @Localizer["Register"] } } @@ -23,6 +23,8 @@ [Parameter] public bool ShowRegister { get; set; } + [Parameter] + public string CssClass { get; set; } = "btn btn-primary"; private string _returnurl = ""; From 3468cba0004044dae40c75d0f3e2f0024146c137 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 13 Jan 2025 07:48:30 -0500 Subject: [PATCH 029/118] fix #4969 - improve feedback and flow when connection string points to an invalid database --- Oqtane.Server/Controllers/SettingController.cs | 2 +- Oqtane.Server/Infrastructure/DatabaseManager.cs | 10 +++++++--- Oqtane.Server/Program.cs | 5 ++++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 298b6b01..2579d379 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -64,7 +64,7 @@ namespace Oqtane.Controllers } else { - // suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies + // suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions if (entityName != EntityNames.Visitor) { _logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings {EntityName} {EntityId}", entityName, entityId); diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 30593b3b..cf7a8e79 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -91,7 +91,7 @@ namespace Oqtane.Infrastructure // get configuration if (install == null) { - // startup or silent installation + // startup or auotmated installation install = new InstallConfig { ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey), @@ -111,7 +111,7 @@ namespace Oqtane.Infrastructure if (!string.IsNullOrEmpty(install.ConnectionString) && !string.IsNullOrEmpty(install.Aliases) && !string.IsNullOrEmpty(install.HostPassword) && !string.IsNullOrEmpty(install.HostEmail)) { - // silent install + // automated install install.SiteTemplate = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate); install.DefaultTheme = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme); install.DefaultContainer = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer); @@ -120,7 +120,11 @@ namespace Oqtane.Infrastructure } else { - // silent installation is missing required information + if (!string.IsNullOrEmpty(install.ConnectionString)) + { + // automated installation is missing required information + result.Message = $"Error Installing Master Database For {SettingKeys.ConnectionStringKey}: {install.ConnectionString}. If You Are Trying To Execute An Automated Installation You Must Include The HostEmail, HostPassword, And DefaultAlias In appsettings.json."; + } install.ConnectionString = ""; } } diff --git a/Oqtane.Server/Program.cs b/Oqtane.Server/Program.cs index bd6a9471..24eeee11 100644 --- a/Oqtane.Server/Program.cs +++ b/Oqtane.Server/Program.cs @@ -25,7 +25,10 @@ namespace Oqtane.Server filelogger.LogError($"[Oqtane.Server.Program.Main] {install.Message}"); } } - host.Run(); + else + { + host.Run(); + } } public static IWebHost BuildWebHost(string[] args) => From 0ac6a62b8632ac59ea4b9ee2b271f45e898a4fc1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 13 Jan 2025 07:50:05 -0500 Subject: [PATCH 030/118] fix comment spelling --- Oqtane.Server/Infrastructure/DatabaseManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index cf7a8e79..3509eafa 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -91,7 +91,7 @@ namespace Oqtane.Infrastructure // get configuration if (install == null) { - // startup or auotmated installation + // startup or automated installation install = new InstallConfig { ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey), From a59ec0258bf6aa683201c10b1467f1c8a0d15275 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 13 Jan 2025 14:42:19 -0500 Subject: [PATCH 031/118] improve filtering logic in UserRole API --- .../Controllers/UserRoleController.cs | 62 +++++++++---------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/Oqtane.Server/Controllers/UserRoleController.cs b/Oqtane.Server/Controllers/UserRoleController.cs index 124e0597..bc557e75 100644 --- a/Oqtane.Server/Controllers/UserRoleController.cs +++ b/Oqtane.Server/Controllers/UserRoleController.cs @@ -121,41 +121,39 @@ namespace Oqtane.Controllers private UserRole Filter(UserRole userrole, int userid) { - // clone object to avoid mutating cache - UserRole filtered = null; - - if (userrole != null) + // include all properties if authorized + if (_userPermissions.IsAuthorized(User, userrole.User.SiteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin)) { - filtered = new UserRole(); - - // public properties - filtered.UserRoleId = userrole.UserRoleId; - filtered.UserId = userrole.UserId; - filtered.RoleId = userrole.RoleId; - - filtered.User = new User(); - filtered.User.SiteId = userrole.User.SiteId; - filtered.User.UserId = userrole.User.UserId; - filtered.User.Username = userrole.User.Username; - filtered.User.DisplayName = userrole.User.DisplayName; - - filtered.Role = new Role(); - filtered.Role.SiteId = userrole.Role.SiteId; - filtered.Role.RoleId = userrole.Role.RoleId; - filtered.Role.Name = userrole.Role.Name; - - // include private properties if administrator - if (_userPermissions.IsAuthorized(User, filtered.User.SiteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin)) - { - filtered.User.Email = userrole.User.Email; - filtered.User.PhotoFileId = userrole.User.PhotoFileId; - filtered.User.LastLoginOn = userrole.User.LastLoginOn; - filtered.User.LastIPAddress = userrole.User.LastIPAddress; - filtered.User.CreatedOn = userrole.User.CreatedOn; - } + return userrole; } + else + { + // clone object to avoid mutating cache + UserRole filtered = null; - return filtered; + if (userrole != null) + { + filtered = new UserRole(); + + // include public properties + filtered.UserRoleId = userrole.UserRoleId; + filtered.UserId = userrole.UserId; + filtered.RoleId = userrole.RoleId; + + filtered.User = new User(); + filtered.User.SiteId = userrole.User.SiteId; + filtered.User.UserId = userrole.User.UserId; + filtered.User.Username = userrole.User.Username; + filtered.User.DisplayName = userrole.User.DisplayName; + + filtered.Role = new Role(); + filtered.Role.SiteId = userrole.Role.SiteId; + filtered.Role.RoleId = userrole.Role.RoleId; + filtered.Role.Name = userrole.Role.Name; + } + + return filtered; + } } // POST api/ From 1fb54a0b0fab298b929097bc040e858a02f977e6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 13 Jan 2025 15:14:13 -0500 Subject: [PATCH 032/118] include option for external login to save tokens --- Oqtane.Client/Modules/Admin/Users/Index.razor | 14 +++++++++++++- .../Resources/Modules/Admin/Users/Index.resx | 6 ++++++ .../OqtaneSiteAuthenticationBuilderExtensions.cs | 7 ++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index a5211c55..c6114047 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -379,7 +379,16 @@ else -
+
+ +
+ +
+
+
@@ -497,6 +506,7 @@ else private string _roleclaimmappings; private string _synchronizeroles; private string _profileclaimtypes; + private string _savetokens; private string _domainfilter; private string _createusers; private string _verifyusers; @@ -577,6 +587,7 @@ else _roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", ""); _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); + _savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false"); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); @@ -666,6 +677,7 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true); settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:SaveTokens", _savetokens, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 381eff20..021788e8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -495,4 +495,10 @@ OpenID Connect (OIDC) + + Save Tokens? + + + Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie. + \ No newline at end of file diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 56b0d3bd..0a7b1094 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -47,7 +47,6 @@ namespace Oqtane.Extensions // default options options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.RequireHttpsMetadata = true; - options.SaveTokens = false; options.GetClaimsFromUserInfoEndpoint = true; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect; options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure @@ -63,6 +62,7 @@ namespace Oqtane.Extensions options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); + options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); @@ -102,7 +102,6 @@ namespace Oqtane.Extensions // default options options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2; - options.SaveTokens = false; // site options options.AuthorizationEndpoint = sitesettings.GetValue("ExternalLogin:AuthorizationUrl", ""); @@ -111,6 +110,7 @@ namespace Oqtane.Extensions options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", ""); options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); + options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); options.Scope.Clear(); foreach (var scope in sitesettings.GetValue("ExternalLogin:Scopes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) { @@ -228,7 +228,6 @@ namespace Oqtane.Extensions var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); if (identity.Label == ExternalLoginStatus.Success) { - identity.AddClaim(new Claim("access_token", context.AccessToken)); context.Principal = new ClaimsPrincipal(identity); } @@ -304,8 +303,6 @@ namespace Oqtane.Extensions var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); if (identity.Label == ExternalLoginStatus.Success) { - // include access token - identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData)); context.Principal = new ClaimsPrincipal(identity); } else From 9508983b156136aa68a6adc7da5ab7ee28c345cf Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 14 Jan 2025 19:58:56 +0800 Subject: [PATCH 033/118] Fix #4954: use Pomelo.EntityFrameworkCore.MySql package. --- Oqtane.Database.MySQL/MySQLDatabase.cs | 6 ++--- .../Oqtane.Database.MySQL.csproj | 4 +-- .../Infrastructure/UpgradeManager.cs | 26 +++++++++++++++++++ Oqtane.Shared/Shared/Constants.cs | 4 +-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Oqtane.Database.MySQL/MySQLDatabase.cs b/Oqtane.Database.MySQL/MySQLDatabase.cs index 648e903e..32759f21 100644 --- a/Oqtane.Database.MySQL/MySQLDatabase.cs +++ b/Oqtane.Database.MySQL/MySQLDatabase.cs @@ -1,9 +1,9 @@ using System.Data; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using MySql.Data.MySqlClient; -using MySql.EntityFrameworkCore.Metadata; using Oqtane.Databases; namespace Oqtane.Database.MySQL @@ -25,7 +25,7 @@ namespace Oqtane.Database.MySQL public override OperationBuilder AddAutoIncrementColumn(ColumnsBuilder table, string name) { - return table.Column(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn); + return table.Column(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn); } public override string ConcatenateSql(params string[] values) @@ -86,7 +86,7 @@ namespace Oqtane.Database.MySQL public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) { - return optionsBuilder.UseMySQL(connectionString); + return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); } private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query) diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index 68095cbc..d73c96ad 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -33,8 +33,8 @@ - + @@ -42,7 +42,7 @@ - + diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 0ba6642b..3d082cae 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -74,6 +74,9 @@ namespace Oqtane.Infrastructure case "6.0.1": Upgrade_6_0_1(tenant, scope); break; + case "6.0.2": + Upgrade_6_0_2(tenant, scope); + break; } } } @@ -494,6 +497,29 @@ namespace Oqtane.Infrastructure } } + private void Upgrade_6_0_2(Tenant tenant, IServiceScope scope) + { + // remove MySql.EntityFrameworkCore package + string[] assemblies = { + "MySql.EntityFrameworkCore.dll" + }; + + foreach (var assembly in assemblies) + { + try + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var filepath = Path.Combine(binFolder, assembly); + if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); + } + catch (Exception ex) + { + // error deleting asesmbly + _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: 6.0.2 Upgrade Error Removing {assembly} - {ex}")); + } + } + } + private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) { var tenants = scope.ServiceProvider.GetRequiredService(); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 7f56d72f..04d3bda7 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "6.0.1"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1"; + public static readonly string Version = "6.0.2"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.0.2"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; From 4521f8a7748458bed0187f05666126f96f508aad Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 14 Jan 2025 08:43:23 -0500 Subject: [PATCH 034/118] introduce RemoveAssemblies() method in UpgradeManager --- .../Infrastructure/UpgradeManager.cs | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 3d082cae..b4e8aedf 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -481,43 +481,17 @@ namespace Oqtane.Infrastructure "System.Text.Json.dll" }; - foreach (var assembly in assemblies) - { - try - { - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - var filepath = Path.Combine(binFolder, assembly); - if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); - } - catch (Exception ex) - { - // error deleting asesmbly - _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: 6.0.1 Upgrade Error Removing {assembly} - {ex}")); - } - } + RemoveAssemblies(assemblies, "6.0.1"); } private void Upgrade_6_0_2(Tenant tenant, IServiceScope scope) { - // remove MySql.EntityFrameworkCore package + // remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql) string[] assemblies = { "MySql.EntityFrameworkCore.dll" }; - foreach (var assembly in assemblies) - { - try - { - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - var filepath = Path.Combine(binFolder, assembly); - if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); - } - catch (Exception ex) - { - // error deleting asesmbly - _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: 6.0.2 Upgrade Error Removing {assembly} - {ex}")); - } - } + RemoveAssemblies(assemblies, "6.0.2"); } private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) @@ -530,5 +504,23 @@ namespace Oqtane.Infrastructure sites.CreatePages(site, pageTemplates, null); } } + + private void RemoveAssemblies(string[] assemblies, string version) + { + foreach (var assembly in assemblies) + { + try + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var filepath = Path.Combine(binFolder, assembly); + if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); + } + catch (Exception ex) + { + // error deleting asesmbly + _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: {version} Upgrade Error Removing {assembly} - {ex}")); + } + } + } } } From 51600bbcb026a68e1969678601b08b6f0055137c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 14 Jan 2025 15:42:40 -0500 Subject: [PATCH 035/118] fix #4984 - ensure personalized page path does not contain illegal characters --- Oqtane.Server/Controllers/PageController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 44e38808..03a602e0 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -189,14 +189,15 @@ namespace Oqtane.Controllers User user = _userPermissions.GetUser(User); if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid)) { - page = _pages.GetPage(parent.Path + "/" + user.Username, parent.SiteId); + var path = parent.Path + "/" + Utilities.GetFriendlyUrl(user.Username); + page = _pages.GetPage(path, parent.SiteId); if (page == null) { page = new Page(); page.SiteId = parent.SiteId; page.ParentId = parent.PageId; page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username; - page.Path = parent.Path + "/" + user.Username; + page.Path = path; page.Title = page.Name + " - " + parent.Name; page.Order = 0; page.IsNavigation = false; From 74bfb46f73a5e6fbc139f4b53dc3e37fc56ccd87 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 15 Jan 2025 11:56:44 -0500 Subject: [PATCH 036/118] fix #4984 - redirect not working for personalized pages --- Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor | 3 ++- Oqtane.Client/UI/SiteRouter.razor | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 5b8922f5..717cc31e 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -131,6 +131,7 @@ if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)) { page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId); + PageState.EditMode = true; } if (_showEditMode) @@ -153,7 +154,7 @@ { if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)) { - NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString())); + NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString().ToLower())); } } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 31ac6c21..11619c43 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -244,7 +244,7 @@ // look for personalized page if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)) { - var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId); + var personalized = await PageService.GetPageAsync(route.PagePath + "/" + Utilities.GetFriendlyUrl(user.Username), site.SiteId); if (personalized != null) { // redirect to the personalized page From 5bb98eb5b24064d47ac534fe43f7fb36e2def4c8 Mon Sep 17 00:00:00 2001 From: beolafsen <76835718+beolafsen@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:32:18 +0100 Subject: [PATCH 037/118] Trim ModuleOwner and ModuleName before create --- Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor index 60241e2a..bcb78bc5 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Create.razor @@ -111,6 +111,8 @@ private async Task CreateModule() { validated = true; + _owner = _owner.Trim(); + _module = _module.Trim(); var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { From 334054bcd42eb43001b45c379be5a8ac53f967b1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 16 Jan 2025 09:25:27 -0500 Subject: [PATCH 038/118] fix #4984 - path mapping for personalized pages --- Oqtane.Client/Modules/Admin/Pages/Edit.razor | 8 +++- .../Resources/Modules/Admin/Pages/Edit.resx | 10 ++++- Oqtane.Client/UI/SiteRouter.razor | 6 ++- Oqtane.Server/Controllers/PageController.cs | 38 ++++++++++++++++--- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 78dec87a..436eb45a 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -116,7 +116,7 @@
- +
@@ -263,6 +263,12 @@
+
+ +
+ +
+
diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx index b142a19c..cf720e19 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx @@ -169,7 +169,7 @@ Select whether the page is part of the site navigation or hidden - Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'. + Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'. Optionally enter a url which this page should redirect to when a user navigates to it @@ -297,4 +297,10 @@ Expiry Date: - + + Url Path: + + + Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash. + + \ No newline at end of file diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 11619c43..68996c8b 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -20,7 +20,7 @@ @if (!string.IsNullOrEmpty(_error)) { - + } @DynamicComponent @@ -244,7 +244,9 @@ // look for personalized page if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)) { - var personalized = await PageService.GetPageAsync(route.PagePath + "/" + Utilities.GetFriendlyUrl(user.Username), site.SiteId); + var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.PageId}"; + var path = (user.Settings.ContainsKey(settingName)) ? user.Settings[settingName] : Utilities.GetFriendlyUrl(user.Username); + var personalized = await PageService.GetPageAsync(route.PagePath + "/" + path, site.SiteId); if (personalized != null) { // redirect to the personalized page diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 03a602e0..97c303ac 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -9,7 +9,6 @@ using System.Net; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; -using System; namespace Oqtane.Controllers { @@ -189,16 +188,16 @@ namespace Oqtane.Controllers User user = _userPermissions.GetUser(User); if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid)) { - var path = parent.Path + "/" + Utilities.GetFriendlyUrl(user.Username); - page = _pages.GetPage(path, parent.SiteId); + var path = Utilities.GetFriendlyUrl(user.Username); + page = _pages.GetPage(parent.Path + "/" + path, parent.SiteId); if (page == null) { page = new Page(); page.SiteId = parent.SiteId; page.ParentId = parent.PageId; - page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username; - page.Path = path; - page.Title = page.Name + " - " + parent.Name; + page.Name = user.Username; + page.Path = parent.Path + "/" + path; + page.Title = ((!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username) + " - " + parent.Name; page.Order = 0; page.IsNavigation = false; page.Url = ""; @@ -251,6 +250,11 @@ namespace Oqtane.Controllers _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Create); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); + + // set user personalized page path + var setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false }; + _settings.AddSetting(setting); + _syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update); } } else @@ -341,6 +345,28 @@ namespace Oqtane.Controllers _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); + + // personalized page + if (page.UserId != null && currentPage.Path != page.Path) + { + // set user personalized page path + var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.ParentId}"; + var path = page.Path.Substring(page.Path.LastIndexOf("/") + 1); + var settings = _settings.GetSettings(EntityNames.User, page.UserId.Value).ToList(); + var setting = settings.FirstOrDefault(item => item.SettingName == settingName); + if (setting == null) + { + setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false }; + _settings.AddSetting(setting); + } + else + { + setting.SettingValue = path; + _settings.UpdateSetting(setting); + } + _syncManager.AddSyncEvent(_alias, EntityNames.User, page.UserId.Value, SyncEventActions.Update); + } + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page); } else From e7444a0194a189b8ad6887de13410866a970c157 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 16 Jan 2025 12:26:10 -0500 Subject: [PATCH 039/118] fix #4986 - allow Resources which have Reload specified to be used in Interactive rendering --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- Oqtane.Client/Themes/ThemeBase.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index b99e7d29..ee326db8 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -98,7 +98,7 @@ namespace Oqtane.Modules var inline = 0; foreach (Resource resource in resources) { - if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) + if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) { if (!string.IsNullOrEmpty(resource.Url)) { diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 6db6a028..62bc44cc 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -62,7 +62,7 @@ namespace Oqtane.Themes var inline = 0; foreach (Resource resource in resources) { - if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) + if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) { if (!string.IsNullOrEmpty(resource.Url)) { From 0204ff8dd585040c7e6babe483e1f5c2f1d99c0e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 16 Jan 2025 14:06:13 -0500 Subject: [PATCH 040/118] script reload improvements --- Oqtane.Client/UI/Head.razor | 2 +- Oqtane.Client/UI/ThemeBuilder.razor | 13 ++- Oqtane.Server/Components/App.razor | 24 ++++-- Oqtane.Server/wwwroot/js/reload.js | 94 +++++++++++---------- Oqtane.Shared/Enums/ResourceLoadBehavior.cs | 10 +++ Oqtane.Shared/Models/Resource.cs | 26 ++++-- Oqtane.Shared/Models/Script.cs | 27 +++++- 7 files changed, 129 insertions(+), 67 deletions(-) create mode 100644 Oqtane.Shared/Enums/ResourceLoadBehavior.cs diff --git a/Oqtane.Client/UI/Head.razor b/Oqtane.Client/UI/Head.razor index c3b0f667..5ab1bc26 100644 --- a/Oqtane.Client/UI/Head.razor +++ b/Oqtane.Client/UI/Head.razor @@ -70,7 +70,7 @@ if (!script.Contains("><") && !script.Contains("data-reload")) { // add data-reload attribute to inline script - headcontent = headcontent.Replace(script, script.Replace("") + 1; - await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("") - pos), location.ToString().ToLower(), dataAttributes); + count += 1; + id = $"page{PageState.Page.PageId}-script{count}"; } + var pos = script.IndexOf(">") + 1; + await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("") - pos), location.ToString().ToLower(), dataAttributes); } index = content.IndexOf(""; + } + else { var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; var dataAttributes = ""; + if (!resource.DataAttributes.ContainsKey("data-reload")) + { + switch (resource.LoadBehavior) + { + case ResourceLoadBehavior.Once: + dataAttributes += " data-reload=\"once\""; + break; + case ResourceLoadBehavior.Always: + dataAttributes += " data-reload=\"always\""; + break; + } + } if (resource.DataAttributes != null && resource.DataAttributes.Count > 0) { foreach (var attribute in resource.DataAttributes) @@ -552,10 +568,6 @@ ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + ">"; } - else - { - return ""; - } } private void SetLocalizationCookie(string cookieValue) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index bb9ad076..66ac9a4c 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -1,67 +1,74 @@ -const scriptInfoBySrc = new Map(); +const scriptKeys = new Set(); + +export function onUpdate() { + // determine if this is an enhanced navigation + let enhancedNavigation = scriptKeys.size !== 0; + + // iterate over all script elements in document + const scripts = document.getElementsByTagName('script'); + for (const script of Array.from(scripts)) { + // only process scripts that include a data-reload attribute + if (script.hasAttribute('data-reload')) { + let key = getKey(script); + + if (enhancedNavigation) { + // reload the script if data-reload is always or if the script has not been loaded previously and data-reload is once + let dataReload = script.getAttribute('data-reload'); + if (dataReload === 'always' || (!scriptKeys.has(key) && dataReload == 'once')) { + reloadScript(script); + } + } + + // save the script key + if (!scriptKeys.has(key)) { + scriptKeys.add(key); + } + } + } +} function getKey(script) { - if (script.hasAttribute("src") && script.src !== "") { + if (script.src) { return script.src; + } else if (script.id) { + return script.id; } else { return script.innerHTML; } } -export function onUpdate() { - let timestamp = Date.now(); - let enhancedNavigation = scriptInfoBySrc.size !== 0; - - // iterate over all script elements in page - const scripts = document.getElementsByTagName("script"); - for (const script of Array.from(scripts)) { - let key = getKey(script); - let scriptInfo = scriptInfoBySrc.get(key); - if (!scriptInfo) { - // new script added - scriptInfo = { timestamp: timestamp }; - scriptInfoBySrc.set(key, scriptInfo); - if (enhancedNavigation) { - reloadScript(script); - } - } else { - // existing script - scriptInfo.timestamp = timestamp; - if (script.hasAttribute("data-reload") && script.getAttribute("data-reload") === "true") { - reloadScript(script); - } +function reloadScript(script) { + try { + if (isValid(script)) { + replaceScript(script); } - } - - // remove scripts that are no longer referenced - for (const [key, scriptInfo] of scriptInfoBySrc) { - if (scriptInfo.timestamp !== timestamp) { - scriptInfoBySrc.delete(key); + } catch (error) { + if (script.src) { + console.error(`Script Reload failed to load external script: ${script.src}`, error); + } else { + console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error); } } } -function reloadScript(script) { - try { - replaceScript(script); - } catch (error) { - if (script.hasAttribute("src") && script.src !== "") { - console.error("Failed to load external script: ${script.src}", error); - } else { - console.error("Failed to load inline script: ${script.innerHtml}", error); - } +function isValid(script) { + if (script.innerHTML.includes('document.write(')) { + console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`); + return false; } + return true; } function replaceScript(script) { return new Promise((resolve, reject) => { - var newScript = document.createElement("script"); + var newScript = document.createElement('script'); // replicate attributes and content for (let i = 0; i < script.attributes.length; i++) { newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); } newScript.innerHTML = script.innerHTML; + newScript.removeAttribute('data-reload'); // dynamically injected scripts cannot be async or deferred newScript.async = false; @@ -70,11 +77,10 @@ function replaceScript(script) { newScript.onload = () => resolve(); newScript.onerror = (error) => reject(error); - // remove existing script + // remove existing script element script.remove(); - // replace with new script to force reload in Blazor + // replace with new script element to force reload in Blazor document.head.appendChild(newScript); }); -} - +} \ No newline at end of file diff --git a/Oqtane.Shared/Enums/ResourceLoadBehavior.cs b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs new file mode 100644 index 00000000..091a014f --- /dev/null +++ b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs @@ -0,0 +1,10 @@ +namespace Oqtane.Shared +{ + public enum ResourceLoadBehavior + { + Once, + Always, + Never, + BlazorPageScript + } +} diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 92b64bef..f3ee3349 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Oqtane.Shared; namespace Oqtane.Models @@ -13,7 +12,7 @@ namespace Oqtane.Models private string _url; /// - /// A so the Interop can properly create `script` or `link` tags + /// A to define the type of resource ie. Script or Stylesheet /// public ResourceType ResourceType { get; set; } @@ -45,7 +44,7 @@ namespace Oqtane.Models public string CrossOrigin { get; set; } /// - /// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process + /// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process (for Interactive rendering only) /// public string Bundle { get; set; } @@ -60,7 +59,7 @@ namespace Oqtane.Models public ResourceLocation Location { get; set; } /// - /// Allows specification of inline script - not applicable to Stylesheets + /// For Scripts this allows for the specification of inline script - not applicable to Stylesheets /// public string Content { get; set; } @@ -70,9 +69,9 @@ namespace Oqtane.Models public string RenderMode { get; set; } /// - /// Indicates that a script should be reloaded on every page transition - not applicable to Stylesheets + /// Specifies how a script should be loaded in Static rendering - not applicable to Stylesheets /// - public bool Reload { get; set; } + public ResourceLoadBehavior LoadBehavior { get; set; } /// /// Cusotm data-* attributes for scripts - not applicable to Stylesheets @@ -96,7 +95,7 @@ namespace Oqtane.Models resource.Location = Location; resource.Content = Content; resource.RenderMode = RenderMode; - resource.Reload = Reload; + resource.LoadBehavior = LoadBehavior; resource.DataAttributes = new Dictionary(); if (DataAttributes != null && DataAttributes.Count > 0) { @@ -125,5 +124,18 @@ namespace Oqtane.Models }; } } + + [Obsolete("Reload is deprecated. Use LoadBehavior property instead for scripts.", false)] + public bool Reload + { + get => (LoadBehavior == ResourceLoadBehavior.BlazorPageScript); + set + { + if (value) + { + LoadBehavior = ResourceLoadBehavior.BlazorPageScript; + }; + } + } } } diff --git a/Oqtane.Shared/Models/Script.cs b/Oqtane.Shared/Models/Script.cs index de44a5ce..1fd5d612 100644 --- a/Oqtane.Shared/Models/Script.cs +++ b/Oqtane.Shared/Models/Script.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Oqtane.Shared; @@ -27,6 +28,13 @@ namespace Oqtane.Models this.Type = Type; } + public Script(string Content, ResourceLoadBehavior LoadBehavior) + { + SetDefaults(); + this.Content = Content; + this.LoadBehavior = LoadBehavior; + } + public Script(string Src, string Integrity, string CrossOrigin) { SetDefaults(); @@ -35,6 +43,22 @@ namespace Oqtane.Models this.CrossOrigin = CrossOrigin; } + public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string RenderMode) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + this.Type = Type; + this.Content = Content; + this.Location = Location; + this.Bundle = Bundle; + this.LoadBehavior = LoadBehavior; + this.DataAttributes = DataAttributes; + this.RenderMode = RenderMode; + } + + [Obsolete("This constructor is deprecated. Use constructor with LoadBehavior parameter instead.", false)] public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, bool Reload, Dictionary DataAttributes, string RenderMode) { SetDefaults(); @@ -45,9 +69,10 @@ namespace Oqtane.Models this.Content = Content; this.Location = Location; this.Bundle = Bundle; - this.Reload = Reload; + this.LoadBehavior = (Reload) ? ResourceLoadBehavior.BlazorPageScript : ResourceLoadBehavior.Once; this.DataAttributes = DataAttributes; this.RenderMode = RenderMode; } + } } From 64a38d6e45f78140bbb41ae3ca4437e6747c2285 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 16 Jan 2025 14:21:52 -0500 Subject: [PATCH 041/118] allow data-reload to support true or always --- Oqtane.Server/wwwroot/js/reload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 66ac9a4c..9c29628d 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -14,7 +14,7 @@ export function onUpdate() { if (enhancedNavigation) { // reload the script if data-reload is always or if the script has not been loaded previously and data-reload is once let dataReload = script.getAttribute('data-reload'); - if (dataReload === 'always' || (!scriptKeys.has(key) && dataReload == 'once')) { + if ((dataReload === 'true' || dataReload === 'always') || (!scriptKeys.has(key) && dataReload == 'once')) { reloadScript(script); } } From 74b72ed9d4adf1d0b2fb0efbbc20dea0be31159a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 16 Jan 2025 15:06:15 -0500 Subject: [PATCH 042/118] reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" --- Oqtane.Server/wwwroot/js/reload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 9c29628d..4e04bda7 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -12,9 +12,9 @@ export function onUpdate() { let key = getKey(script); if (enhancedNavigation) { - // reload the script if data-reload is always or if the script has not been loaded previously and data-reload is once + // reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" let dataReload = script.getAttribute('data-reload'); - if ((dataReload === 'true' || dataReload === 'always') || (!scriptKeys.has(key) && dataReload == 'once')) { + if (dataReload === 'always' || (!scriptKeys.has(key) && (dataReload == 'once' || dataReload === 'true'))) { reloadScript(script); } } From e6cf77e724b441c6558cececaa4a715cad19dba1 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Fri, 17 Jan 2025 13:34:16 +0100 Subject: [PATCH 043/118] Update ActionDialog Add method to ensure consistent button sizing This PR introduces a new private method GetButtonSize() to enhance the consistency of button sizing within our UI. The method specifically checks for the presence of the "btn-sm" class in the Action Button's class list and applies the same sizing to the Cancel Button if found. --- Oqtane.Client/Modules/Controls/ActionDialog.razor | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index b39b4f27..6a1e4fb5 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -24,7 +24,7 @@ { } - +
@@ -71,7 +71,7 @@ else }
- +
@@ -196,7 +196,7 @@ else _openIconSpan = $"{(IconOnly ? "" : " ")}"; _iconSpan = $" "; } - + _permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList; _authorized = IsAuthorized(); @@ -207,6 +207,15 @@ else _visible = (PageState.QueryString["dialog"] == Id); } } + /// + /// Checks the Class that is used for the Action Button, if it is small make the Cancel Button small as well. + /// + /// + /// + private string GetButtonSize() + { + return Class.Contains("btn-sm", StringComparison.OrdinalIgnoreCase) ? "btn-sm" : string.Empty; + } private bool IsAuthorized() { From 4bc26f13c151417dea4e51cc1673ca560cf5787e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 17 Jan 2025 07:54:34 -0500 Subject: [PATCH 044/118] improve error messages --- Oqtane.Server/wwwroot/js/reload.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 4e04bda7..6766e74d 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -12,9 +12,9 @@ export function onUpdate() { let key = getKey(script); if (enhancedNavigation) { - // reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" + // reload the script if data-reload is "always" or "true"... or if the script has not been loaded previously and data-reload is "once" let dataReload = script.getAttribute('data-reload'); - if (dataReload === 'always' || (!scriptKeys.has(key) && (dataReload == 'once' || dataReload === 'true'))) { + if ((dataReload === 'always' || dataReload === 'true') || (!scriptKeys.has(key) && dataReload == 'once')) { reloadScript(script); } } @@ -43,17 +43,13 @@ function reloadScript(script) { replaceScript(script); } } catch (error) { - if (script.src) { - console.error(`Script Reload failed to load external script: ${script.src}`, error); - } else { - console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error); - } + console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error); } } function isValid(script) { if (script.innerHTML.includes('document.write(')) { - console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`); + console.log(`Blazor Script Reload does not support scripts using document.write(): ${script.innerHTML}`); return false; } return true; From 677f68b08d519ae579bb9ad428b80ac5d5082e54 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 17 Jan 2025 11:14:35 -0500 Subject: [PATCH 045/118] allow entry of name during installation --- Oqtane.Client/Installer/Installer.razor | 11 +++++++++-- Oqtane.Client/Resources/Installer/Installer.resx | 6 ++++++ Oqtane.Client/Services/InstallationService.cs | 4 ++-- .../Services/Interfaces/IInstallationService.cs | 3 ++- .../Controllers/InstallationController.cs | 14 +++++++------- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index c0187971..d5bb7dfc 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -98,6 +98,12 @@ +
+ +
+ +
+
@@ -153,6 +159,7 @@ private string _toggleConfirmPassword = string.Empty; private string _confirmPassword = string.Empty; private string _hostEmail = string.Empty; + private string _hostName = string.Empty; private List _templates; private string _template = Constants.DefaultSiteTemplate; private bool _register = true; @@ -236,7 +243,7 @@ } } - if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) + if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@") && !string.IsNullOrEmpty(_hostName)) { var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword); if (result.Succeeded) @@ -256,7 +263,7 @@ HostUsername = _hostUsername, HostPassword = _hostPassword, HostEmail = _hostEmail, - HostName = _hostUsername, + HostName = _hostName, TenantName = TenantNames.Master, IsNewTenant = true, SiteName = Constants.DefaultSite, diff --git a/Oqtane.Client/Resources/Installer/Installer.resx b/Oqtane.Client/Resources/Installer/Installer.resx index a06752d9..cd1c2e6e 100644 --- a/Oqtane.Client/Resources/Installer/Installer.resx +++ b/Oqtane.Client/Resources/Installer/Installer.resx @@ -186,4 +186,10 @@ The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits. + + Full Name: + + + Provide the full name of the host user + \ No newline at end of file diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs index a9e4d3d6..e7fdce81 100644 --- a/Oqtane.Client/Services/InstallationService.cs +++ b/Oqtane.Client/Services/InstallationService.cs @@ -57,9 +57,9 @@ namespace Oqtane.Services await PostAsync($"{ApiUrl}/restart"); } - public async Task RegisterAsync(string email) + public async Task RegisterAsync(string email, string name) { - await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true); + await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}", true); } } } diff --git a/Oqtane.Client/Services/Interfaces/IInstallationService.cs b/Oqtane.Client/Services/Interfaces/IInstallationService.cs index 84790c63..8f5853c8 100644 --- a/Oqtane.Client/Services/Interfaces/IInstallationService.cs +++ b/Oqtane.Client/Services/Interfaces/IInstallationService.cs @@ -39,8 +39,9 @@ namespace Oqtane.Services /// Registers a new /// /// Email of the user to be registered + /// Name of the user to be registered /// - Task RegisterAsync(string email); + Task RegisterAsync(string email, string name); } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 92e5b289..48ddd513 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -60,9 +60,9 @@ namespace Oqtane.Controllers { installation = _databaseManager.Install(config); - if (installation.Success && config.Register) + if (installation.Success) { - await RegisterContact(config.HostEmail); + await RegisterContact(config.HostEmail, config.HostName); } } else @@ -257,7 +257,7 @@ namespace Oqtane.Controllers } } - private async Task RegisterContact(string email) + private async Task RegisterContact(string email, string name) { try { @@ -268,7 +268,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}")).ConfigureAwait(false); + var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}")).ConfigureAwait(false); } } } @@ -278,12 +278,12 @@ namespace Oqtane.Controllers } } - // GET api//register?email=x + // GET api//register?email=x&name=y [HttpPost("register")] [Authorize(Roles = RoleNames.Host)] - public async Task Register(string email) + public async Task Register(string email, string name) { - await RegisterContact(email); + await RegisterContact(email, name); } public struct ClientAssembly From 06ca382bd76072a921a6532938d16ba350b8a2ee Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 17 Jan 2025 12:50:26 -0500 Subject: [PATCH 046/118] remove unused method --- Oqtane.Client/Installer/Installer.razor | 10 +++++----- Oqtane.Client/Services/InstallationService.cs | 5 ----- .../Services/Interfaces/IInstallationService.cs | 9 --------- .../Controllers/InstallationController.cs | 14 +++----------- 4 files changed, 8 insertions(+), 30 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index d5bb7dfc..c00a0d7e 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -71,14 +71,14 @@
- +
- +
@@ -87,7 +87,7 @@
- +
@@ -95,13 +95,13 @@
- +
- +
diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs index e7fdce81..f1fbd057 100644 --- a/Oqtane.Client/Services/InstallationService.cs +++ b/Oqtane.Client/Services/InstallationService.cs @@ -56,10 +56,5 @@ namespace Oqtane.Services { await PostAsync($"{ApiUrl}/restart"); } - - public async Task RegisterAsync(string email, string name) - { - await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}", true); - } } } diff --git a/Oqtane.Client/Services/Interfaces/IInstallationService.cs b/Oqtane.Client/Services/Interfaces/IInstallationService.cs index 8f5853c8..e8a433c7 100644 --- a/Oqtane.Client/Services/Interfaces/IInstallationService.cs +++ b/Oqtane.Client/Services/Interfaces/IInstallationService.cs @@ -34,14 +34,5 @@ namespace Oqtane.Services /// /// internal status/message object Task RestartAsync(); - - /// - /// Registers a new - /// - /// Email of the user to be registered - /// Name of the user to be registered - /// - Task RegisterAsync(string email, string name); - } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 48ddd513..48bbf8b8 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -62,7 +62,7 @@ namespace Oqtane.Controllers if (installation.Success) { - await RegisterContact(config.HostEmail, config.HostName); + await RegisterContact(config.HostEmail, config.HostName, config.Register); } } else @@ -257,7 +257,7 @@ namespace Oqtane.Controllers } } - private async Task RegisterContact(string email, string name) + private async Task RegisterContact(string email, string name, bool register) { try { @@ -268,7 +268,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}")).ConfigureAwait(false); + var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}®ister={register.ToString().ToLower()}")).ConfigureAwait(false); } } } @@ -278,14 +278,6 @@ namespace Oqtane.Controllers } } - // GET api//register?email=x&name=y - [HttpPost("register")] - [Authorize(Roles = RoleNames.Host)] - public async Task Register(string email, string name) - { - await RegisterContact(email, name); - } - public struct ClientAssembly { public ClientAssembly(string filepath, bool hashfilename) From 4793ab4bc99c57257e445dec8bf42c484ad0a6e3 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 17 Jan 2025 12:56:59 -0500 Subject: [PATCH 047/118] update copyright year --- .../Infrastructure/SiteTemplates/DefaultSiteTemplate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index d6f41e91..09b1f30a 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -67,7 +67,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, - Content = "

Copyright (c) 2018-2024 .NET Foundation

" + + Content = "

Copyright (c) 2018-2025 .NET Foundation

" + "

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

" + "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" From d1f78f9048e132eb9c3f4b9c83dce2a6c4df853e Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 17 Jan 2025 12:57:32 -0500 Subject: [PATCH 048/118] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d2b14c41..a8125f20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2024 .NET Foundation +Copyright (c) 2018-2025 .NET Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 1283ec200864c0dc5b7381759bf59eb4a15a90c2 Mon Sep 17 00:00:00 2001 From: sdi2121 <159460782+sdi2121@users.noreply.github.com> Date: Sat, 18 Jan 2025 13:53:04 -0500 Subject: [PATCH 049/118] Update Oqtane.Server.csproj Fixes Azure manual deployment build from local code build. May need an additional fix in MySQL library (Line 24 - MySQLDatabase.cs) --- Oqtane.Server/Oqtane.Server.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index a9060e71..1ed427f7 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -57,7 +57,7 @@ - + From ca7fdaa125ac605e0d711869b6b0702770f66a50 Mon Sep 17 00:00:00 2001 From: David Montesinos <90258222+mdmontesinos@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:17:22 +0100 Subject: [PATCH 050/118] FIX: File server MimeType not updated after image conversion --- Oqtane.Server/Pages/Files.cshtml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index 463534fc..8af671ae 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -253,12 +253,12 @@ namespace Oqtane.Pages if (download) { _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); - return PhysicalFile(filepath, file.GetMimeType(), downloadName); + return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName), downloadName); } else { HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); - return PhysicalFile(filepath, file.GetMimeType()); + return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName)); } } From fe2a883386aa5c87cc18c2f9731e774aa56cdf48 Mon Sep 17 00:00:00 2001 From: RahulKaushik007 Date: Tue, 21 Jan 2025 17:26:47 +0530 Subject: [PATCH 051/118] Fix static file caching headers --- Oqtane.Server/Startup.cs | 1 + Oqtane.Server/appsettings.json | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index d873bd24..7c37cc09 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -205,6 +205,7 @@ namespace Oqtane ServeUnknownFileTypes = true, OnPrepareResponse = (ctx) => { + ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=604800"); var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) .ConfigureAwait(false).GetAwaiter().GetResult(); corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json index 28270bab..128c0cda 100644 --- a/Oqtane.Server/appsettings.json +++ b/Oqtane.Server/appsettings.json @@ -2,10 +2,10 @@ "RenderMode": "Interactive", "Runtime": "Server", "Database": { - "DefaultDBType": "" + "DefaultDBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer" }, "ConnectionStrings": { - "DefaultConnection": "" + "DefaultConnection": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\Oqtane-202501210838.mdf;Initial Catalog=Oqtane-202501210838;Integrated Security=SSPI;Encrypt=false;" }, "Installation": { "DefaultAlias": "", @@ -54,5 +54,6 @@ "LogLevel": { "Default": "Information" } - } -} + }, + "InstallationId": "60faae5c-96ea-4416-9abf-9203fd9e3d8d" +} \ No newline at end of file From 66a05603f741cdad7f4597c84f53043e909238ad Mon Sep 17 00:00:00 2001 From: RahulKaushik007 Date: Tue, 21 Jan 2025 17:42:08 +0530 Subject: [PATCH 052/118] Fix static file caching headers --- Oqtane.Server/appsettings.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json index 128c0cda..54c22b30 100644 --- a/Oqtane.Server/appsettings.json +++ b/Oqtane.Server/appsettings.json @@ -2,10 +2,10 @@ "RenderMode": "Interactive", "Runtime": "Server", "Database": { - "DefaultDBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer" + "DefaultDBType": "" }, "ConnectionStrings": { - "DefaultConnection": "Data Source=(LocalDb)\\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\\Oqtane-202501210838.mdf;Initial Catalog=Oqtane-202501210838;Integrated Security=SSPI;Encrypt=false;" + "DefaultConnection": "" }, "Installation": { "DefaultAlias": "", @@ -54,6 +54,5 @@ "LogLevel": { "Default": "Information" } - }, - "InstallationId": "60faae5c-96ea-4416-9abf-9203fd9e3d8d" + } } \ No newline at end of file From 16477052e2fec5e51c78507d019a78446a556fba Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 21 Jan 2025 12:21:27 -0500 Subject: [PATCH 053/118] fix #4965 - improve user/site management --- Oqtane.Client/Modules/Admin/Roles/Users.razor | 33 +++---- Oqtane.Client/Modules/Admin/Users/Edit.razor | 51 +++++++--- Oqtane.Client/Modules/Admin/Users/Index.razor | 28 ++++-- Oqtane.Client/Modules/Admin/Users/Roles.razor | 24 +++-- .../Resources/Modules/Admin/Users/Edit.resx | 9 ++ .../Resources/Modules/Admin/Users/Index.resx | 6 ++ Oqtane.Client/Resources/SharedResources.resx | 2 +- Oqtane.Server/Controllers/UserController.cs | 2 +- ...taneSiteAuthenticationBuilderExtensions.cs | 95 ++++++++++--------- Oqtane.Server/Managers/UserManager.cs | 42 ++++---- 10 files changed, 187 insertions(+), 105 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Roles/Users.razor b/Oqtane.Client/Modules/Admin/Roles/Users.razor index d0eb0aac..5f8a6e99 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Users.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Users.razor @@ -58,7 +58,7 @@ else @context.EffectiveDate @context.ExpiryDate - + @@ -180,27 +180,28 @@ else private async Task DeleteUserRole(int UserRoleId) { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) + try { - try + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else { await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); - AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); - await GetUserRoles(); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); - AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); } + AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); + await GetUserRoles(); + StateHasChanged(); } - else + catch (Exception ex) { - AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); + AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); } } } diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index a58af948..e4aedaa0 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -51,15 +51,18 @@
-
- -
- + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+ +
+ +
-
+ }
@@ -127,8 +130,11 @@ @SharedLocalizer["Cancel"] -
-
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") + { + + } +

} @@ -226,8 +232,10 @@ user.Password = _password; user.Email = email; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; - - user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + } user = await UserService.UpdateUserAsync(user); if (user != null) @@ -259,6 +267,25 @@ } } + private async Task DeleteUser() + { + try + { + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) + { + var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); + await logger.LogInformation("User Permanently Deleted {User}", user); + NavigationManager.NavigateTo(NavigateUrl()); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); + } + } + private bool ValidateProfiles() { foreach (Profile profile in profiles) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index c6114047..8af3d381 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -35,7 +35,7 @@ else - + @@ -611,19 +611,31 @@ else { try { - var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); - if (user != null) + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); - await logger.LogInformation("User Deleted {User}", UserRole.User); - await LoadUsersAsync(true); - StateHasChanged(); + var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); + if (user != null) + { + user.IsDeleted = true; + await UserService.UpdateUserAsync(user); + await logger.LogInformation("User Soft Deleted {User}", user); + } } + else + { + var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId); + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success); + await LoadUsersAsync(true); + StateHasChanged(); } catch (Exception ex) { await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); - AddModuleMessage(ex.Message, MessageType.Error); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Roles.razor b/Oqtane.Client/Modules/Admin/Users/Roles.razor index 8d65480a..5ce12b62 100644 --- a/Oqtane.Client/Modules/Admin/Users/Roles.razor +++ b/Oqtane.Client/Modules/Admin/Users/Roles.razor @@ -53,17 +53,17 @@ else

- @Localizer["Roles"] - @Localizer["Effective"] - @Localizer["Expiry"] -   + @Localizer["Roles"] + @Localizer["Effective"] + @Localizer["Expiry"] +  
@context.Role.Name @Utilities.UtcAsLocalDate(context.EffectiveDate) @Utilities.UtcAsLocalDate(context.ExpiryDate) - + @@ -171,8 +171,18 @@ else { try { - await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else + { + await UserRoleService.DeleteUserRoleAsync(UserRoleId); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); + } AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success); await GetUserRoles(); StateHasChanged(); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index f56d3798..29aed594 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -195,4 +195,13 @@ Last Login: + + Delete User + + + Delete + + + Are You Sure You Wish To Permanently Delete This User? + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 021788e8..34884fb5 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -501,4 +501,10 @@ Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie. + + User Deleted Successfully + + + Error Deleting User + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index 8c5ae010..b32eb955 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -427,7 +427,7 @@ At Least One Uppercase Letter - Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site. + Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site. {0} Is Not Valid diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 6d146758..92aef5c5 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -217,7 +217,7 @@ namespace Oqtane.Controllers // DELETE api//5?siteid=x [HttpDelete("{id}")] - [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")] + [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")] public async Task Delete(int id, string siteid) { User user = _users.GetUser(id, false); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 0a7b1094..0e58e4b6 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -524,11 +524,6 @@ namespace Oqtane.Extensions // manage user if (user != null) { - // update user - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); - _users.UpdateUser(user); - // manage roles var _userRoles = httpContext.RequestServices.GetRequiredService(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); @@ -588,64 +583,78 @@ namespace Oqtane.Extensions } } - // create claims identity - identityuser = await _identityUserManager.FindByNameAsync(user.Username); - user.SecurityStamp = identityuser.SecurityStamp; - identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); - identity.Label = ExternalLoginStatus.Success; - - // user profile claims - if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) + var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered); + if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate)) { - var _settings = httpContext.RequestServices.GetRequiredService(); - var _profiles = httpContext.RequestServices.GetRequiredService(); - var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); - foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) + // update user + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); + _users.UpdateUser(user); + + // create claims identity + identityuser = await _identityUserManager.FindByNameAsync(user.Username); + user.SecurityStamp = identityuser.SecurityStamp; + identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); + identity.Label = ExternalLoginStatus.Success; + + // user profile claims + if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) { - if (mapping.Contains(":")) + var _settings = httpContext.RequestServices.GetRequiredService(); + var _profiles = httpContext.RequestServices.GetRequiredService(); + var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); + foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) { - var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); - if (claim != null) + if (mapping.Contains(":")) { - var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); - if (profile != null) + var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); + if (claim != null) { - if (!string.IsNullOrEmpty(claim.Value)) + var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); + if (profile != null) { - var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); - if (setting != null) + if (!string.IsNullOrEmpty(claim.Value)) { - setting.SettingValue = claim.Value; - _settings.UpdateSetting(setting); - } - else - { - setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; - _settings.AddSetting(setting); + var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); + if (setting != null) + { + setting.SettingValue = claim.Value; + _settings.UpdateSetting(setting); + } + else + { + setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; + _settings.AddSetting(setting); + } } } + else + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); } } - else - { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); - } } + + var _syncManager = httpContext.RequestServices.GetRequiredService(); + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); + } + else + { + identity.Label = ExternalLoginStatus.AccessDenied; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External User Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId); } - - var _syncManager = httpContext.RequestServices.GetRequiredService(); - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); } } else // claims invalid diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index c1a91f5d..694bfd03 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -363,28 +363,36 @@ namespace Oqtane.Managers } else { - user = _users.GetUser(identityuser.UserName); - if (user != null) + if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) { - if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) + user = GetUser(identityuser.UserName, alias.SiteId); + if (user != null) { - user.IsAuthenticated = true; - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = LastIPAddress; - _users.UpdateUser(user); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); - - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - if (setCookie) + // ensure user is registered for site + if (user.Roles.Contains(RoleNames.Registered)) { - await _identitySignInManager.SignInAsync(identityuser, isPersistent); + user.IsAuthenticated = true; + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = LastIPAddress; + _users.UpdateUser(user); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); + + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId); } } - else - { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); - } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); } } } From 90d2e0a40be785e3e4cb451f290c61ca6ae6c9a2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 21 Jan 2025 15:57:48 -0500 Subject: [PATCH 054/118] fix #4976 - manage hierarchical path updates and page deletion --- .../Modules/Admin/RecycleBin/Index.razor | 154 ++++++++++-------- .../Modules/Admin/RecycleBin/Index.resx | 21 +++ Oqtane.Client/Resources/SharedResources.resx | 3 + Oqtane.Server/Controllers/PageController.cs | 55 +++++-- 4 files changed, 152 insertions(+), 81 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor index 4e4e2607..4b672b61 100644 --- a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor +++ b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor @@ -13,71 +13,71 @@ } else { - - - @if (!_pages.Where(item => item.IsDeleted).Any()) - { -
-

@Localizer["NoPage.Deleted"]

- } - else - { - -
-   -   - @SharedLocalizer["Name"] - @Localizer["DeletedBy"] - @Localizer["DeletedOn"] -
- + + + @if (!_pages.Where(item => item.IsDeleted).Any()) + { +
+

@Localizer["NoPage.Deleted"]

+ } + else + { + +
+   +   + @SharedLocalizer["Path"] + @Localizer["DeletedBy"] + @Localizer["DeletedOn"] +
+ - - @context.Name - @context.DeletedBy - @context.DeletedOn - -
-
- - } -
- - @if (!_modules.Where(item => item.IsDeleted).Any()) - { -
-

@Localizer["NoModule.Deleted"]

- } - else - { - -
-   -   - @Localizer["Page"] - @Localizer["Module"] - @Localizer["DeletedBy"] - @Localizer["DeletedOn"] -
- - - - @_pages.Find(item => item.PageId == context.PageId).Name - @context.Title - @context.DeletedBy - @context.DeletedOn - -
-
- - } -
-
+ + @context.Path + @context.DeletedBy + @context.DeletedOn +
+
+
+ + } +
+ + @if (!_modules.Where(item => item.IsDeleted).Any()) + { +
+

@Localizer["NoModule.Deleted"]

+ } + else + { + +
+   +   + @Localizer["Page"] + @Localizer["Module"] + @Localizer["DeletedBy"] + @Localizer["DeletedOn"] +
+ + + + @_pages.Find(item => item.PageId == context.PageId).Name + @context.Title + @context.DeletedBy + @context.DeletedOn + +
+
+ + } +
+
} @code { - private List _pages; - private List _modules; + private List _pages; + private List _modules; private int _pagePage = 1; private int _pageModule = 1; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -105,12 +105,25 @@ else { try { - page.IsDeleted = false; - await PageService.UpdatePageAsync(page); - await logger.LogInformation("Page Restored {Page}", page); - await Load(); - StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); + var validated = true; + if (page.ParentId != null) + { + var parent = _pages.Find(item => item.PageId == page.ParentId); + validated = !parent.IsDeleted; + } + if (validated) + { + page.IsDeleted = false; + await PageService.UpdatePageAsync(page); + await logger.LogInformation("Page Restored {Page}", page); + AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success); + await Load(); + StateHasChanged(); + } + else + { + AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning); + } } catch (Exception ex) { @@ -125,9 +138,9 @@ else { await PageService.DeletePageAsync(page.PageId); await logger.LogInformation("Page Permanently Deleted {Page}", page); + AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success); await Load(); StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); } catch (Exception ex) { @@ -148,10 +161,10 @@ else } await logger.LogInformation("Pages Permanently Deleted"); + AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success); await Load(); HideProgressIndicator(); StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); } catch (Exception ex) { @@ -169,6 +182,7 @@ else pagemodule.IsDeleted = false; await PageModuleService.UpdatePageModuleAsync(pagemodule); await logger.LogInformation("Module Restored {Module}", module); + AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success); await Load(); StateHasChanged(); } @@ -185,6 +199,7 @@ else { await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await logger.LogInformation("Module Permanently Deleted {Module}", module); + AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success); await Load(); StateHasChanged(); } @@ -205,6 +220,7 @@ else await PageModuleService.DeletePageModuleAsync(module.PageModuleId); } await logger.LogInformation("Modules Permanently Deleted"); + AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success); await Load(); HideProgressIndicator(); StateHasChanged(); diff --git a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx index c45ee304..0e212ab8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx @@ -195,4 +195,25 @@ Modules + + You Cannot Restore A Page If Its Parent Is Deleted + + + Page Restored Successfully + + + Page Deleted Successfully + + + All Pages Deleted Successfully + + + Module Restored Successfully + + + Module Deleted Successfully + + + All Modules Deleted Successfully + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index b32eb955..a0078ea5 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -474,4 +474,7 @@ User + + Path + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 97c303ac..e57091c7 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -9,6 +9,8 @@ using System.Net; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; +using System.Xml.Linq; +using Microsoft.AspNetCore.Diagnostics; namespace Oqtane.Controllers { @@ -279,18 +281,14 @@ namespace Oqtane.Controllers // get current page permissions var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList(); - page = _pages.UpdatePage(page); + // preserve new path and deleted status + var newPath = page.Path; + var deleted = page.IsDeleted; + page.Path = currentPage.Path; + page.IsDeleted = currentPage.IsDeleted; - // save url mapping if page path changed - if (currentPage.Path != page.Path) - { - var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path); - if (urlMapping != null) - { - urlMapping.MappedUrl = page.Path; - _urlMappings.UpdateUrlMapping(urlMapping); - } - } + // update page + UpdatePage(page, page.Path, newPath, deleted); // get differences between current and new page permissions var added = GetPermissionsDifferences(page.PermissionList, currentPermissions); @@ -320,6 +318,7 @@ namespace Oqtane.Controllers }); } } + // permissions removed foreach (Permission permission in removed) { @@ -343,7 +342,6 @@ namespace Oqtane.Controllers } } - _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); // personalized page @@ -378,6 +376,39 @@ namespace Oqtane.Controllers return page; } + private void UpdatePage(Page page, string oldPath, string newPath, bool deleted) + { + var update = false; + if (oldPath != newPath) + { + var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, page.Path); + if (urlMapping != null) + { + urlMapping.MappedUrl = newPath + page.Path.Substring(oldPath.Length); + _urlMappings.UpdateUrlMapping(urlMapping); + } + + page.Path = newPath + page.Path.Substring(oldPath.Length); + update = true; + } + if (deleted != page.IsDeleted) + { + page.IsDeleted = deleted; + update = true; + } + if (update) + { + _pages.UpdatePage(page); + _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update); + } + + // update any children + foreach (var _page in _pages.GetPages(page.SiteId).Where(item => item.ParentId == page.PageId)) + { + UpdatePage(_page, oldPath, newPath, deleted); + } + } + private List GetPermissionsDifferences(List permissions1, List permissions2) { var differences = new List(); From 950d90badb7bfca5e45af6d31b9edb4bf0881a2f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 21 Jan 2025 16:55:02 -0500 Subject: [PATCH 055/118] fix #4964 - use bearer token if it already exists --- Oqtane.Server/Components/App.razor | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index a8a9fefb..e487eecf 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -174,7 +174,7 @@ // get jwt token for downstream APIs if (Context.User.Identity.IsAuthenticated) { - CreateJwtToken(alias); + GetJwtToken(alias); } // includes resources @@ -441,13 +441,23 @@ } } - private void CreateJwtToken(Alias alias) + private void GetJwtToken(Alias alias) { - var sitesettings = Context.GetSiteSettings(); - var secret = sitesettings.GetValue("JwtOptions:Secret", ""); - if (!string.IsNullOrEmpty(secret)) + _authorizationToken = Context.Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(_authorizationToken)) { - _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); + // bearer token was provided by remote Identity Provider and was persisted using SaveTokens + _authorizationToken = _authorizationToken.Replace("Bearer ", ""); + } + else + { + // generate bearer token if a secret has been configured in User Settings + var sitesettings = Context.GetSiteSettings(); + var secret = sitesettings.GetValue("JwtOptions:Secret", ""); + if (!string.IsNullOrEmpty(secret)) + { + _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); + } } } From af6ed78b8e3e1954485c4b35aaee1ab822b5dfa4 Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 22 Jan 2025 07:57:52 +0100 Subject: [PATCH 056/118] Update ActionDialog.razor --- .../Modules/Controls/ActionDialog.razor | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index 6a1e4fb5..09e93225 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -22,9 +22,9 @@
@@ -66,12 +66,12 @@ else {
- +
}
- +
@@ -128,6 +128,12 @@ else [Parameter] public string Class { get; set; } // optional + [Parameter] + public string ConfirmClass { get; set; } // optional - for Confirm modal button + + [Parameter] + public string CancelClass { get; set; } // optional - for Cancel modal button + [Parameter] public bool Disabled { get; set; } // optional @@ -168,6 +174,16 @@ else Class = "btn btn-success"; } + if (string.IsNullOrEmpty(ConfirmClass)) + { + ConfirmClass = Class; + } + + if (string.IsNullOrEmpty(CancelClass)) + { + CancelClass = "btn btn-secondary"; + } + if (!string.IsNullOrEmpty(EditMode)) { _editmode = bool.Parse(EditMode); From 935983c02a62e57459047d8912db9aa1bb976d5e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 22 Jan 2025 07:43:23 -0500 Subject: [PATCH 057/118] remove GetButtonSize method --- Oqtane.Client/Modules/Controls/ActionDialog.razor | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index 09e93225..83948efe 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -223,15 +223,6 @@ else _visible = (PageState.QueryString["dialog"] == Id); } } - /// - /// Checks the Class that is used for the Action Button, if it is small make the Cancel Button small as well. - /// - /// - /// - private string GetButtonSize() - { - return Class.Contains("btn-sm", StringComparison.OrdinalIgnoreCase) ? "btn-sm" : string.Empty; - } private bool IsAuthorized() { From 0ef24ebc3ff1f5eeda2b9e81845e8a649f858caa Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 23 Jan 2025 09:08:02 -0500 Subject: [PATCH 058/118] allow packages to be managed across installations --- .../Modules/Admin/SystemInfo/Index.razor | 17 ++++++++--- .../Modules/Admin/SystemInfo/Index.resx | 10 +++++-- .../Controllers/PackageController.cs | 30 ++++++++++++++++--- Oqtane.Server/Controllers/SystemController.cs | 1 + 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index eb3cb283..3485d814 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -133,11 +133,17 @@
- +
+
+ +
+ +
+


  @@ -180,8 +186,9 @@ private string _notificationlevel = string.Empty; private string _swagger = string.Empty; private string _packageregistryurl = string.Empty; + private string _packageregistryemail = string.Empty; - private string _log = string.Empty; + private string _log = string.Empty; protected override async Task OnInitializedAsync() { @@ -211,7 +218,8 @@ _notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString(); _swagger = systeminfo["UseSwagger"].ToString(); _packageregistryurl = systeminfo["PackageRegistryUrl"].ToString(); - } + _packageregistryemail = systeminfo["PackageRegistryEmail"].ToString(); + } systeminfo = await SystemService.GetSystemInfoAsync("log"); if (systeminfo != null) @@ -230,7 +238,8 @@ settings.Add("Logging:LogLevel:Notify", _notificationlevel); settings.Add("UseSwagger", _swagger); settings.Add("PackageRegistryUrl", _packageregistryurl); - await SystemService.UpdateSystemInfoAsync(settings); + settings.Add("PackageRegistryEmail", _packageregistryemail); + await SystemService.UpdateSystemInfoAsync(settings); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); } catch (Exception ex) diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index 76747197..48525017 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -220,10 +220,10 @@ You Have Been Successfully Registered For Updates - Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation. + Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation. - Package Manager: + Package Manager Url: Specify If Swagger Is Enabled For Your Server API @@ -294,4 +294,10 @@ Process: + + Package Manager Email: + + + Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations. + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs index 795eb525..f1aba718 100644 --- a/Oqtane.Server/Controllers/PackageController.cs +++ b/Oqtane.Server/Controllers/PackageController.cs @@ -12,6 +12,8 @@ using Oqtane.Infrastructure; using Oqtane.Enums; using System.Net.Http.Headers; using System.Text.Json; +using Oqtane.Managers; +using System.Net; // ReSharper disable PartialTypeWithSinglePart namespace Oqtane.Controllers @@ -20,13 +22,15 @@ namespace Oqtane.Controllers public class PackageController : Controller { private readonly IInstallationManager _installationManager; + private readonly IUserManager _userManager; private readonly IWebHostEnvironment _environment; private readonly IConfigManager _configManager; private readonly ILogManager _logger; - public PackageController(IInstallationManager installationManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger) + public PackageController(IInstallationManager installationManager, IUserManager userManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger) { _installationManager = installationManager; + _userManager = userManager; _environment = environment; _configManager = configManager; _logger = logger; @@ -45,7 +49,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - packages = await GetJson>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}"); + packages = await GetJson>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}"); } } return packages; @@ -64,7 +68,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - packages = await GetJson>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}"); + packages = await GetJson>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}"); } } return packages; @@ -83,7 +87,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - package = await GetJson(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}"); + package = await GetJson(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}"); } if (package != null) @@ -117,6 +121,24 @@ namespace Oqtane.Controllers return package; } + private string GetPackageRegistryEmail() + { + var email = _configManager.GetSetting("PackageRegistryEmail", ""); + if (string.IsNullOrEmpty(email)) + { + if (User.Identity.IsAuthenticated) + { + var user = _userManager.GetUser(User.Identity.Name, -1); + if (user != null) + { + email = user.Email; + _configManager.AddOrUpdateSetting("PackageRegistryEmail", email, false); + } + } + } + return email; + } + private async Task GetJson(HttpClient httpClient, string url) { try diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs index b34611f4..74aa9a27 100644 --- a/Oqtane.Server/Controllers/SystemController.cs +++ b/Oqtane.Server/Controllers/SystemController.cs @@ -54,6 +54,7 @@ namespace Oqtane.Controllers systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl)); + systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", "")); break; case "log": string log = ""; From 7a9c637e0308afc5bf3145047adbc57752a44b39 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 24 Jan 2025 14:29:23 -0500 Subject: [PATCH 059/118] fix #5014 - page content scripts not loading on initial page request in Interactive rendering --- Oqtane.Client/UI/ThemeBuilder.razor | 7 +++---- Oqtane.Server/Controllers/PageController.cs | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index fc567833..6981b983 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -11,8 +11,6 @@ RenderFragment DynamicComponent { get; set; } - private string lastPagePath = ""; - protected override void OnParametersSet() { // handle page redirection @@ -92,8 +90,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { - if (!firstRender && PageState.Page.Path != lastPagePath) + if (!firstRender) { + // site content if (!string.IsNullOrEmpty(PageState.Site.HeadContent) && PageState.Site.HeadContent.Contains(" item.ParentId == page.PageId)) { - UpdatePage(_page, oldPath, newPath, deleted); + UpdatePage(_page, pageId, oldPath, newPath, deleted); } } From 056ef7a3d5e6504010062f8b25967a7e9a50247a Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:00:08 -0800 Subject: [PATCH 060/118] Update Package Dependencies to 9.0.1 --- Oqtane.Server/Oqtane.Server.csproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 1ed427f7..9e06b595 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -34,17 +34,17 @@
- - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + From 9bd36931ffefec4b2ddaf81bf2f2733f5af48639 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:02:10 -0800 Subject: [PATCH 061/118] Update Package Dependencies to 9.0.1 --- Oqtane.Client/Oqtane.Client.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index c08c9181..fe970ff9 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -22,10 +22,10 @@ - - - - + + + + From 72ddf27504482dd8abf33e4835eb9c4bf358103a Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:03:03 -0800 Subject: [PATCH 062/118] Update Package Dependency to 9.2.0 --- Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index d73c96ad..59428c93 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -33,7 +33,7 @@ - + From 36f50118acd39d6ae0e57f49449cf1a66795ea33 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:03:46 -0800 Subject: [PATCH 063/118] Update Package Dependencies to 9.0.1 --- Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index 2e66cbfc..fb90faa1 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -34,8 +34,8 @@ - - + + From ed9929963c0a0f36aa538e52dc192ea32288d7ed Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:04:34 -0800 Subject: [PATCH 064/118] Update Package Dependencies to 9.0.1 --- Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index 4212d9fe..d358b88d 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -33,7 +33,7 @@ - + From 86ec25d4deb29f7190fae39330cfc52126620fea Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:05:42 -0800 Subject: [PATCH 065/118] Update Package Dependencies to 9.0.1 --- Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 2fad42e3..c4f90077 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -33,7 +33,7 @@ - + From 2bb76564e9f30921b50495cd55d3ae9727c7e8ae Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:06:46 -0800 Subject: [PATCH 066/118] Update Package Dependencies to 9.0.1 --- Oqtane.Shared/Oqtane.Shared.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 1c72ab77..d1e7c0b7 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -19,11 +19,11 @@ - - - + + + - + From e0e32b0199e79d419b5184634a7aa5ce770cf7bc Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:09:19 -0800 Subject: [PATCH 067/118] Update Package Dependencies to 9.0.1 and 9.0.30 --- Oqtane.Maui/Oqtane.Maui.csproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 6b696214..09f24894 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -67,14 +67,14 @@ - - - - - - - - + + + + + + + + From 8441c95a5cd87dbdccfbc03a38d2892d301ed5fa Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:11:49 -0800 Subject: [PATCH 068/118] Update Package Dependencies to 9.0.1 --- .../External/Client/[Owner].Theme.[Theme].Client.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index a40bfb11..ec578462 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@ - - - + + + From c66a5d028f95a1801af62d3e8022815228a4f956 Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:13:01 -0800 Subject: [PATCH 069/118] Update Package Dependencies to 9.0.1 --- .../Client/[Owner].Module.[Module].Client.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 0bc7b7c7..dce1e10e 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@ - - - - - + + + + + From 26b88f1a224ec14349e3e7015736a1c3b98d160c Mon Sep 17 00:00:00 2001 From: Cody Date: Mon, 27 Jan 2025 05:13:31 -0800 Subject: [PATCH 070/118] Update Package Dependencies to 9.0.1 --- .../External/Server/[Owner].Module.[Module].Server.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 550e7e32..57bbbf94 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + From 153a689bdbb5da18128af08782e7053553e894c1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 27 Jan 2025 16:34:47 -0500 Subject: [PATCH 071/118] fix #5005 - adds versioning (ie. fingerprinting) for static assets - core, modules, and themes. --- Oqtane.Client/UI/SiteRouter.razor | 18 +++++----- Oqtane.Server/Components/App.razor | 35 ++++++++++--------- .../Controllers/InstallationController.cs | 22 +----------- .../Controllers/PackageController.cs | 2 +- Oqtane.Server/Infrastructure/ConfigManager.cs | 6 ++++ .../Infrastructure/DatabaseManager.cs | 12 ++++++- .../Master/06000201_AddThemeVersion.cs | 28 +++++++++++++++ .../Repository/ModuleDefinitionRepository.cs | 1 + Oqtane.Server/Repository/ThemeRepository.cs | 9 +++++ Oqtane.Server/Services/SiteService.cs | 7 +++- Oqtane.Shared/Models/ModuleDefinition.cs | 5 ++- Oqtane.Shared/Models/Resource.cs | 17 ++++++++- Oqtane.Shared/Models/Site.cs | 9 ++++- Oqtane.Shared/Models/Theme.cs | 15 ++++++-- Oqtane.Shared/Shared/Utilities.cs | 19 ++++++---- 15 files changed, 145 insertions(+), 60 deletions(-) create mode 100644 Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 68996c8b..da50e350 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -391,7 +391,7 @@ if (themetype != null) { // get resources for theme (ITheme) - page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); + page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash); var themeobject = Activator.CreateInstance(themetype) as IThemeControl; if (themeobject != null) @@ -401,7 +401,7 @@ panes = themeobject.Panes; } // get resources for theme control - page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace); + page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Hash); } } // theme settings components are dynamically loaded within the framework Page Management module @@ -411,7 +411,7 @@ if (settingsType != null) { var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace); + page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash); } } @@ -455,7 +455,7 @@ if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) { - page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); + page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) @@ -504,7 +504,7 @@ module.RenderMode = moduleobject.RenderMode; module.Prerender = moduleobject.Prerender; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash); // settings components are dynamically loaded within the framework Settings module if (action.ToLower() == "settings" && module.ModuleDefinition != null) @@ -525,7 +525,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash); } // container settings component @@ -536,7 +536,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash); } } } @@ -595,7 +595,7 @@ return (page, modules); } - private List ManagePageResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name) + private List ManagePageResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string version) { if (resources != null) { @@ -615,7 +615,7 @@ // ensure resource does not exist already if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) { - pageresources.Add(resource.Clone(level, name)); + pageresources.Add(resource.Clone(level, name, version)); } } } diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index e487eecf..cce1ab33 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -39,7 +39,7 @@ - + @if (_scripts.Contains("PWA Manifest")) { @@ -70,15 +70,15 @@ } - - - + + + @((MarkupString)_scripts) @((MarkupString)_bodyResources) @if (_renderMode == RenderModes.Static) { - + } } else @@ -94,6 +94,7 @@ private string _renderMode = RenderModes.Interactive; private string _runtime = Runtimes.Server; private bool _prerender = true; + private string _hash = ""; private int _visitorId = -1; private string _antiForgeryToken = ""; private string _remoteIPAddress = ""; @@ -136,6 +137,8 @@ _renderMode = site.RenderMode; _runtime = site.Runtime; _prerender = site.Prerender; + _hash = site.Hash; + var modules = new List(); Route route = new Route(url, alias.Path); @@ -605,13 +608,13 @@ var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); if (theme != null) { - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode); } else { // fallback to default Oqtane theme theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode); } var type = Type.GetType(themeType); if (type != null) @@ -619,7 +622,7 @@ var obj = Activator.CreateInstance(type) as IThemeControl; if (obj != null) { - resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode); + resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Hash, site.RenderMode); } } // theme settings components are dynamically loaded within the framework Page Management module @@ -629,7 +632,7 @@ if (settingsType != null) { var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; - resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode); + resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash, site.RenderMode); } } @@ -638,7 +641,7 @@ var typename = ""; if (module.ModuleDefinition != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); + resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) @@ -684,7 +687,7 @@ var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; if (moduleobject != null) { - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode); // settings components are dynamically loaded within the framework Settings module if (action.ToLower() == "settings" && module.ModuleDefinition != null) @@ -705,7 +708,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode); } // container settings component @@ -715,7 +718,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash, site.RenderMode); } } } @@ -731,7 +734,7 @@ { if (module.ModuleDefinition?.Resources != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); + resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode); } } } @@ -739,7 +742,7 @@ return resources; } - private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string rendermode) + private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string version, string rendermode) { if (resources != null) { @@ -759,7 +762,7 @@ // ensure resource does not exist already if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) { - pageresources.Add(resource.Clone(level, name)); + pageresources.Add(resource.Clone(level, name, version)); } } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 48bbf8b8..207d082c 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -286,7 +286,7 @@ namespace Oqtane.Controllers DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath); if (hashfilename) { - HashedName = GetDeterministicHashCode(filepath).ToString("X8") + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); + HashedName = Utilities.GenerateSimpleHash(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); } else { @@ -297,25 +297,5 @@ namespace Oqtane.Controllers public string FilePath { get; private set; } public string HashedName { get; private set; } } - - private static int GetDeterministicHashCode(string value) - { - unchecked - { - int hash1 = (5381 << 16) + 5381; - int hash2 = hash1; - - for (int i = 0; i < value.Length; i += 2) - { - hash1 = ((hash1 << 5) + hash1) ^ value[i]; - if (i == value.Length - 1) - break; - hash2 = ((hash2 << 5) + hash2) ^ value[i + 1]; - } - - return hash1 + (hash2 * 1566083941); - } - } - } } diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs index f1aba718..c0b943bc 100644 --- a/Oqtane.Server/Controllers/PackageController.cs +++ b/Oqtane.Server/Controllers/PackageController.cs @@ -132,7 +132,7 @@ namespace Oqtane.Controllers if (user != null) { email = user.Email; - _configManager.AddOrUpdateSetting("PackageRegistryEmail", email, false); + _configManager.AddOrUpdateSetting("PackageRegistryEmail", email, true); } } } diff --git a/Oqtane.Server/Infrastructure/ConfigManager.cs b/Oqtane.Server/Infrastructure/ConfigManager.cs index 8b96569d..d6b0d8d9 100644 --- a/Oqtane.Server/Infrastructure/ConfigManager.cs +++ b/Oqtane.Server/Infrastructure/ConfigManager.cs @@ -175,6 +175,12 @@ namespace Oqtane.Infrastructure installationid = Guid.NewGuid().ToString(); AddOrUpdateSetting("InstallationId", installationid, true); } + var version = GetSetting("InstallationVersion", ""); + if (version != Constants.Version) + { + AddOrUpdateSetting("InstallationVersion", Constants.Version, true); + AddOrUpdateSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"), true); + } return installationid; } } diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 3509eafa..2273cded 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -265,6 +265,7 @@ namespace Oqtane.Infrastructure var installation = IsInstalled(); try { + UpdateInstallation(); UpdateConnectionString(install.ConnectionString); UpdateDatabaseType(install.DatabaseType); @@ -491,6 +492,7 @@ namespace Oqtane.Infrastructure moduleDefinition.Categories = moduledef.Categories; // update version moduleDefinition.Version = versions[versions.Length - 1]; + moduleDefinition.ModifiedOn = DateTime.UtcNow; db.Entry(moduleDefinition).State = EntityState.Modified; db.SaveChanges(); } @@ -666,6 +668,11 @@ namespace Oqtane.Infrastructure return connectionString; } + public void UpdateInstallation() + { + _config.GetInstallationId(); + } + public void UpdateConnectionString(string connectionString) { connectionString = DenormalizeConnectionString(connectionString); @@ -677,7 +684,10 @@ namespace Oqtane.Infrastructure public void UpdateDatabaseType(string databaseType) { - _configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true); + if (_config.GetSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", "") != databaseType) + { + _configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true); + } } public void AddEFMigrationsHistory(ISqlRepository sql, string connectionString, string databaseType, string version, bool isMaster) diff --git a/Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs b/Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs new file mode 100644 index 00000000..def80a48 --- /dev/null +++ b/Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Master +{ + [DbContext(typeof(MasterDBContext))] + [Migration("Master.06.00.02.01")] + public class AddThemeVersion : MultiDatabaseMigration + { + public AddThemeVersion(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase); + themeEntityBuilder.AddStringColumn("Version", 50, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 15eadf9e..cae8cf15 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -101,6 +101,7 @@ namespace Oqtane.Repository ModuleDefinition.Resources = moduleDefinition.Resources; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.PackageName = moduleDefinition.PackageName; + ModuleDefinition.Hash = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm")); } return ModuleDefinition; diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 2802f8f0..22af55ee 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using System.Reflection.Metadata; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; @@ -87,6 +88,7 @@ namespace Oqtane.Repository Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.PackageName = theme.PackageName; + Theme.Hash = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); Themes.Add(Theme); } @@ -126,6 +128,13 @@ namespace Oqtane.Repository } else { + if (theme.Version != Theme.Version) + { + // update theme version + theme.Version = Theme.Version; + _db.SaveChanges(); + } + // override user customizable property values Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name; diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 2a12b38b..dbf65656 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -29,12 +29,13 @@ namespace Oqtane.Services private readonly ISettingRepository _settings; private readonly ITenantManager _tenantManager; private readonly ISyncManager _syncManager; + private readonly IConfigManager _configManager; private readonly ILogManager _logger; private readonly IMemoryCache _cache; private readonly IHttpContextAccessor _accessor; private readonly string _private = "[PRIVATE]"; - public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) + public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) { _sites = sites; _pages = pages; @@ -46,6 +47,7 @@ namespace Oqtane.Services _settings = settings; _tenantManager = tenantManager; _syncManager = syncManager; + _configManager = configManager; _logger = logger; _cache = cache; _accessor = accessor; @@ -143,6 +145,9 @@ namespace Oqtane.Services // themes site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); + + // installation date used for fingerprinting static assets + site.Hash = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); } else { diff --git a/Oqtane.Shared/Models/ModuleDefinition.cs b/Oqtane.Shared/Models/ModuleDefinition.cs index 5c374667..d5553675 100644 --- a/Oqtane.Shared/Models/ModuleDefinition.cs +++ b/Oqtane.Shared/Models/ModuleDefinition.cs @@ -65,7 +65,7 @@ namespace Oqtane.Models public string Categories { get; set; } /// - /// Version information of this Module based on the DLL / NuGet package. + /// Version information of this Module based on the information stored in its assembly /// public string Version { get; set; } @@ -144,6 +144,9 @@ namespace Oqtane.Models [NotMapped] public bool IsPortable { get; set; } + [NotMapped] + public string Hash { get; set; } + #region Deprecated Properties [Obsolete("The Permissions property is deprecated. Use PermissionList instead", false)] diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index f3ee3349..5f5ab968 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -83,7 +83,21 @@ namespace Oqtane.Models /// public string Namespace { get; set; } - public Resource Clone(ResourceLevel level, string name) + /// + /// The version of the theme or module that declared the resource - only used in SiteRouter + /// + public string Version + { + set + { + if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(Url) && !Url.Contains("?")) + { + Url += "?v=" + value; + } + } + } + + public Resource Clone(ResourceLevel level, string name, string version) { var resource = new Resource(); resource.ResourceType = ResourceType; @@ -106,6 +120,7 @@ namespace Oqtane.Models } resource.Level = level; resource.Namespace = name; + resource.Version = version; return resource; } diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index b108cea2..c45cfe4e 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -187,6 +187,12 @@ namespace Oqtane.Models [NotMapped] public List Themes { get; set; } + /// + /// hash code for static assets + /// + [NotMapped] + public string Hash { get; set; } + public Site Clone() { return new Site @@ -227,7 +233,8 @@ namespace Oqtane.Models Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value), Pages = Pages.ConvertAll(page => page.Clone()), Languages = Languages.ConvertAll(language => language.Clone()), - Themes = Themes + Themes = Themes, + Hash = Hash }; } diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index 4ff9fe2f..95715c47 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -40,10 +40,13 @@ namespace Oqtane.Models /// public string Name { get; set; } - // additional ITheme properties - [NotMapped] + /// + /// Version information of this Theme based on the information stored in its assembly + /// public string Version { get; set; } + // additional ITheme properties + [NotMapped] public string Owner { get; set; } @@ -78,17 +81,25 @@ namespace Oqtane.Models // internal properties [NotMapped] public int SiteId { get; set; } + [NotMapped] public bool IsEnabled { get; set; } + [NotMapped] public string AssemblyName { get; set; } + [NotMapped] public List Themes { get; set; } + [NotMapped] public List Containers { get; set; } + [NotMapped] public string Template { get; set; } + [NotMapped] + public string Hash { get; set; } + #region Obsolete Properties [Obsolete("This property is obsolete. Use Themes instead.", false)] diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 6e8f3933..826fac70 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -575,7 +575,6 @@ namespace Oqtane.Shared } else if (expiryDate.HasValue) { - // Include equality check here return currentUtcTime <= expiryDate.Value; } else @@ -586,32 +585,40 @@ namespace Oqtane.Shared public static bool ValidateEffectiveExpiryDates(DateTime? effectiveDate, DateTime? expiryDate) { - // Treat DateTime.MinValue as null effectiveDate ??= DateTime.MinValue; expiryDate ??= DateTime.MinValue; - // Check if both effectiveDate and expiryDate have values if (effectiveDate != DateTime.MinValue && expiryDate != DateTime.MinValue) { return effectiveDate <= expiryDate; } - // Check if only effectiveDate has a value else if (effectiveDate != DateTime.MinValue) { return true; } - // Check if only expiryDate has a value else if (expiryDate != DateTime.MinValue) { return true; } - // If neither effectiveDate nor expiryDate has a value, consider the page/module visible else { return true; } } + public static string GenerateSimpleHash(string text) + { + unchecked // prevent overflow exception + { + int hash = 23; + foreach (char c in text) + { + hash = hash * 31 + c; + } + return hash.ToString("X8"); + } + } + [Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)] public static string ContentUrl(Alias alias, int fileId) { From 37afd1aec94ba60b5255eddad94c55f5db5e8adb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 08:27:10 -0500 Subject: [PATCH 072/118] add ThemeState property to ThemeBase --- Oqtane.Client/Themes/ThemeBase.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 62bc44cc..91091dbf 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -84,6 +84,16 @@ namespace Oqtane.Themes } } + // property for obtaining theme information about this theme component + public Theme ThemeState + { + get + { + var type = GetType().Namespace + ", " + GetType().Assembly.GetName().Name; + return PageState?.Site.Themes.FirstOrDefault(item => item.ThemeName == type); + } + } + // path method public string ThemePath() From bfb4b4431b96171ed8f389313a72c20f2cbead0a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 08:34:15 -0500 Subject: [PATCH 073/118] improve terminology --- Oqtane.Client/Modules/Admin/SystemInfo/Index.razor | 2 +- Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index 3485d814..f6f65b40 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -147,7 +147,7 @@

  - @Localizer["Access.ApiFramework"]  + @Localizer["Swagger"]  diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index 48525017..f81b0663 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -117,8 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Access Swagger API + + Access Swagger UI Framework Version From ed981c67b76f2be8fbf99bea46f2592787a1825c Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 08:43:48 -0500 Subject: [PATCH 074/118] add Fingerprint property to ModuleBase and ThemeBase --- Oqtane.Client/Modules/ModuleBase.cs | 9 +++++++++ Oqtane.Client/Themes/ThemeBase.cs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index ee326db8..09a515b3 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -132,6 +132,15 @@ namespace Oqtane.Modules return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/"; } + // fingerprint hash code for static assets + public string Fingerprint + { + get + { + return ModuleState.ModuleDefinition.Hash; + } + } + // url methods // navigate url diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 91091dbf..fa4e842c 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -101,6 +101,15 @@ namespace Oqtane.Themes return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/"; } + // fingerprint hash code for static assets + public string Fingerprint + { + get + { + return ThemeState.Hash; + } + } + // url methods // navigate url From a996a88fc4b0f2c29a6ec499b974733c6c38f0de Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 08:56:05 -0500 Subject: [PATCH 075/118] improve terminology consistency --- Oqtane.Client/Modules/ModuleBase.cs | 2 +- Oqtane.Client/Themes/ThemeBase.cs | 2 +- Oqtane.Client/UI/SiteRouter.razor | 18 +++++++-------- Oqtane.Server/Components/App.razor | 22 +++++++++---------- .../Repository/ModuleDefinitionRepository.cs | 2 +- Oqtane.Server/Repository/ThemeRepository.cs | 2 +- Oqtane.Shared/Models/ModuleDefinition.cs | 2 +- Oqtane.Shared/Models/Resource.cs | 9 ++++---- Oqtane.Shared/Models/Theme.cs | 2 +- 9 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 09a515b3..e2f89acc 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -137,7 +137,7 @@ namespace Oqtane.Modules { get { - return ModuleState.ModuleDefinition.Hash; + return ModuleState.ModuleDefinition.Fingerprint; } } diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index fa4e842c..b56d958f 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -106,7 +106,7 @@ namespace Oqtane.Themes { get { - return ThemeState.Hash; + return ThemeState.Fingerprint; } } diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index da50e350..9f2b7e99 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -391,7 +391,7 @@ if (themetype != null) { // get resources for theme (ITheme) - page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash); + page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint); var themeobject = Activator.CreateInstance(themetype) as IThemeControl; if (themeobject != null) @@ -401,7 +401,7 @@ panes = themeobject.Panes; } // get resources for theme control - page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Hash); + page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace, theme.Fingerprint); } } // theme settings components are dynamically loaded within the framework Page Management module @@ -411,7 +411,7 @@ if (settingsType != null) { var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash); + page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint); } } @@ -455,7 +455,7 @@ if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) { - page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash); + page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) @@ -504,7 +504,7 @@ module.RenderMode = moduleobject.RenderMode; module.Prerender = moduleobject.Prerender; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint); // settings components are dynamically loaded within the framework Settings module if (action.ToLower() == "settings" && module.ModuleDefinition != null) @@ -525,7 +525,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint); } // container settings component @@ -536,7 +536,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash); + page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint); } } } @@ -595,7 +595,7 @@ return (page, modules); } - private List ManagePageResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string version) + private List ManagePageResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint) { if (resources != null) { @@ -615,7 +615,7 @@ // ensure resource does not exist already if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) { - pageresources.Add(resource.Clone(level, name, version)); + pageresources.Add(resource.Clone(level, name, fingerprint)); } } } diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index cce1ab33..a6edd53e 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -608,13 +608,13 @@ var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); if (theme != null) { - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode); } else { // fallback to default Oqtane theme theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Hash, site.RenderMode); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode); } var type = Type.GetType(themeType); if (type != null) @@ -622,7 +622,7 @@ var obj = Activator.CreateInstance(type) as IThemeControl; if (obj != null) { - resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Hash, site.RenderMode); + resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Fingerprint, site.RenderMode); } } // theme settings components are dynamically loaded within the framework Page Management module @@ -632,7 +632,7 @@ if (settingsType != null) { var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; - resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Hash, site.RenderMode); + resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint, site.RenderMode); } } @@ -641,7 +641,7 @@ var typename = ""; if (module.ModuleDefinition != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode); + resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) @@ -687,7 +687,7 @@ var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; if (moduleobject != null) { - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode); // settings components are dynamically loaded within the framework Settings module if (action.ToLower() == "settings" && module.ModuleDefinition != null) @@ -708,7 +708,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Hash, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode); } // container settings component @@ -718,7 +718,7 @@ if (moduletype != null) { moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; - resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Hash, site.RenderMode); + resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint, site.RenderMode); } } } @@ -734,7 +734,7 @@ { if (module.ModuleDefinition?.Resources != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Hash, site.RenderMode); + resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode); } } } @@ -742,7 +742,7 @@ return resources; } - private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string version, string rendermode) + private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint, string rendermode) { if (resources != null) { @@ -762,7 +762,7 @@ // ensure resource does not exist already if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) { - pageresources.Add(resource.Clone(level, name, version)); + pageresources.Add(resource.Clone(level, name, fingerprint)); } } } diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index cae8cf15..204578e4 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -101,7 +101,7 @@ namespace Oqtane.Repository ModuleDefinition.Resources = moduleDefinition.Resources; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.PackageName = moduleDefinition.PackageName; - ModuleDefinition.Hash = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm")); + ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm")); } return ModuleDefinition; diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index 22af55ee..fd010b8c 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -88,7 +88,7 @@ namespace Oqtane.Repository Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.PackageName = theme.PackageName; - Theme.Hash = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); + Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); Themes.Add(Theme); } diff --git a/Oqtane.Shared/Models/ModuleDefinition.cs b/Oqtane.Shared/Models/ModuleDefinition.cs index d5553675..ce344d09 100644 --- a/Oqtane.Shared/Models/ModuleDefinition.cs +++ b/Oqtane.Shared/Models/ModuleDefinition.cs @@ -145,7 +145,7 @@ namespace Oqtane.Models public bool IsPortable { get; set; } [NotMapped] - public string Hash { get; set; } + public string Fingerprint { get; set; } #region Deprecated Properties diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 5f5ab968..42ecb5f9 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -84,12 +84,13 @@ namespace Oqtane.Models public string Namespace { get; set; } /// - /// The version of the theme or module that declared the resource - only used in SiteRouter + /// Unique identifier of the version of the theme or module that declared the resource - for cache busting - only used in SiteRouter /// - public string Version + public string Fingerprint { set { + // add the fingerprint to the url if it does not contain a querystring already if (!string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(Url) && !Url.Contains("?")) { Url += "?v=" + value; @@ -97,7 +98,7 @@ namespace Oqtane.Models } } - public Resource Clone(ResourceLevel level, string name, string version) + public Resource Clone(ResourceLevel level, string name, string fingerprint) { var resource = new Resource(); resource.ResourceType = ResourceType; @@ -120,7 +121,7 @@ namespace Oqtane.Models } resource.Level = level; resource.Namespace = name; - resource.Version = version; + resource.Fingerprint = fingerprint; return resource; } diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs index 95715c47..a67a2d97 100644 --- a/Oqtane.Shared/Models/Theme.cs +++ b/Oqtane.Shared/Models/Theme.cs @@ -98,7 +98,7 @@ namespace Oqtane.Models public string Template { get; set; } [NotMapped] - public string Hash { get; set; } + public string Fingerprint { get; set; } #region Obsolete Properties From 2a06304a2ce802e53bfd4e2b14f7485675777bba Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 12:47:23 -0500 Subject: [PATCH 076/118] add caching support for folders --- Oqtane.Client/Modules/Admin/Files/Edit.razor | 29 ++++++++++++------- .../Resources/Modules/Admin/Files/Edit.resx | 8 ++++- .../Tenant/06000201_AddFolderCacheControl.cs | 28 ++++++++++++++++++ Oqtane.Server/Pages/Files.cshtml.cs | 4 +++ Oqtane.Shared/Models/Folder.cs | 5 ++++ 5 files changed, 63 insertions(+), 11 deletions(-) create mode 100644 Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 3ad60467..81262d50 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -49,18 +49,24 @@ } -
- -
- -
-
+
+ +
+ +
+
+
+ +
+ +
+
@if (PageState.QueryString.ContainsKey("id")) { @@ -100,8 +106,9 @@ private int _parentId = -1; private string _name; private string _type = FolderTypes.Private; - private string _imagesizes = string.Empty; private string _capacity = "0"; + private string _cachecontrol = string.Empty; + private string _imagesizes = string.Empty; private bool _isSystem; private List _permissions = null; private string _createdBy; @@ -132,8 +139,9 @@ _parentId = folder.ParentId ?? -1; _name = folder.Name; _type = folder.Type; - _imagesizes = folder.ImageSizes; _capacity = folder.Capacity.ToString(); + _cachecontrol = folder.CacheControl; + _imagesizes = folder.ImageSizes; _isSystem = folder.IsSystem; _permissions = folder.PermissionList; _createdBy = folder.CreatedBy; @@ -193,7 +201,7 @@ { folder.ParentId = _parentId; } - + // check for duplicate folder names if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId)) { @@ -204,8 +212,9 @@ folder.SiteId = PageState.Site.SiteId; folder.Name = _name; folder.Type = _type; - folder.ImageSizes = _imagesizes; folder.Capacity = int.Parse(_capacity); + folder.CacheControl = _cachecontrol; + folder.ImageSizes = _imagesizes; folder.IsSystem = _isSystem; folder.PermissionList = _permissionGrid.GetPermissionList(); diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index 4fa73338..1371214d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -175,7 +175,7 @@ Capacity:
- Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes. + Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes. Image Sizes: @@ -198,4 +198,10 @@ Settings + + Caching: + + + Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=604800' indicates that files in this folder should be cached for 1 week. + \ No newline at end of file diff --git a/Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs b/Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs new file mode 100644 index 00000000..7e566bea --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.06.00.02.01")] + public class AddFolderCacheControl : MultiDatabaseMigration + { + public AddFolderCacheControl(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); + folderEntityBuilder.AddStringColumn("CacheControl", 50, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index 8af671ae..a2bb860b 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -257,6 +257,10 @@ namespace Oqtane.Pages } else { + if (!string.IsNullOrEmpty(file.Folder.CacheControl)) + { + HttpContext.Response.Headers.Append(HeaderNames.CacheControl, value: file.Folder.CacheControl); + } HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName)); } diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs index ef20d937..da875219 100644 --- a/Oqtane.Shared/Models/Folder.cs +++ b/Oqtane.Shared/Models/Folder.cs @@ -62,6 +62,11 @@ namespace Oqtane.Models /// public bool IsSystem { get; set; } + /// + /// An HTTP Caching Cache-Control directive + /// + public string CacheControl { get; set; } + /// /// Deprecated /// Note that this property still exists in the database because columns cannot be dropped in SQLite From c5a16fbbc1493473c19d54d91be5f03c4bffcc2f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 13:18:18 -0500 Subject: [PATCH 077/118] fix localization text --- Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index 1371214d..e673f16b 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -175,7 +175,7 @@ Capacity: - Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes. + Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended). Image Sizes: From 65f171f701b32831e57c80b9c9ed1365704c0141 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 14:29:58 -0500 Subject: [PATCH 078/118] provides options to control caching for static assets --- Oqtane.Client/Modules/Admin/SystemInfo/Index.razor | 13 +++++++++++-- .../Resources/Modules/Admin/SystemInfo/Index.resx | 6 ++++++ Oqtane.Server/Controllers/SystemController.cs | 1 + Oqtane.Server/Startup.cs | 14 +++++++++++--- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index f6f65b40..335800cb 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -132,6 +132,12 @@ +
+ +
+ +
+
@@ -185,6 +191,7 @@ private string _logginglevel = string.Empty; private string _notificationlevel = string.Empty; private string _swagger = string.Empty; + private string _cachecontrol = string.Empty; private string _packageregistryurl = string.Empty; private string _packageregistryemail = string.Empty; @@ -216,7 +223,8 @@ _detailederrors = systeminfo["DetailedErrors"].ToString(); _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString(); _notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString(); - _swagger = systeminfo["UseSwagger"].ToString(); + _swagger = systeminfo["UseSwagger"].ToString(); + _cachecontrol = systeminfo["CacheControl"].ToString(); _packageregistryurl = systeminfo["PackageRegistryUrl"].ToString(); _packageregistryemail = systeminfo["PackageRegistryEmail"].ToString(); } @@ -237,7 +245,8 @@ settings.Add("Logging:LogLevel:Default", _logginglevel); settings.Add("Logging:LogLevel:Notify", _notificationlevel); settings.Add("UseSwagger", _swagger); - settings.Add("PackageRegistryUrl", _packageregistryurl); + settings.Add("CacheControl", _cachecontrol); + settings.Add("PackageRegistryUrl", _packageregistryurl); settings.Add("PackageRegistryEmail", _packageregistryemail); await SystemService.UpdateSystemInfoAsync(settings); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index f81b0663..7c4eca2e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -300,4 +300,10 @@ Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations. + + Static Asset Caching: + + + Provide a Cache-Control directive for static assets. For example 'public, max-age=604800' indicates that static assets should be cached for 1 week. A blank value indicates no caching. + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs index 74aa9a27..240a75eb 100644 --- a/Oqtane.Server/Controllers/SystemController.cs +++ b/Oqtane.Server/Controllers/SystemController.cs @@ -53,6 +53,7 @@ namespace Oqtane.Controllers systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); + systeminfo.Add("CacheControl", _configManager.GetSetting("CacheControl", "public, max-age=604800")); systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl)); systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", "")); break; diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 7c37cc09..33e09e80 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -23,6 +23,7 @@ using OqtaneSSR.Extensions; using Microsoft.AspNetCore.Components.Authorization; using Oqtane.Providers; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Net.Http.Headers; namespace Oqtane { @@ -98,7 +99,7 @@ namespace Oqtane { options.HeaderName = Constants.AntiForgeryTokenHeaderName; options.Cookie.Name = Constants.AntiForgeryTokenCookieName; - options.Cookie.SameSite = SameSiteMode.Strict; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.HttpOnly = true; }); @@ -171,7 +172,7 @@ namespace Oqtane } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ILogger logger) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, IConfigManager configManager, ILogger logger) { if (!string.IsNullOrEmpty(_configureServicesErrors)) { @@ -205,7 +206,14 @@ namespace Oqtane ServeUnknownFileTypes = true, OnPrepareResponse = (ctx) => { - ctx.Context.Response.Headers.Append("Cache-Control", "public, max-age=604800"); + if (!env.IsDevelopment()) + { + var cachecontrol = configManager.GetSetting("CacheControl", "public, max-age=604800"); + if (!string.IsNullOrEmpty(cachecontrol)) + { + ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol); + } + } var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) .ConfigureAwait(false).GetAwaiter().GetResult(); corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); From 37de18c6703fcbe2f2ad496d4bb1037c87b4fcf9 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 14:39:30 -0500 Subject: [PATCH 079/118] remove Oqtane.Server.staticwebassets.endpoints.json from release packages --- Oqtane.Package/release.cmd | 1 + 1 file changed, 1 insertion(+) diff --git a/Oqtane.Package/release.cmd b/Oqtane.Package/release.cmd index 3aa117db..0452d806 100644 --- a/Oqtane.Package/release.cmd +++ b/Oqtane.Package/release.cmd @@ -36,6 +36,7 @@ if "%%~nxi" == "%%j" set /A found=1 ) if not !found! == 1 rmdir /Q/S "%%i" ) +del "..\Oqtane.Server\bin\Release\net9.0\publish\Oqtane.Server.staticwebassets.endpoints.json" del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json" ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json" C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1" From 188be2fa8c8d56db0faac9f9e1cffd4c672408d4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 15:55:40 -0500 Subject: [PATCH 080/118] use deterministic hash in file server image generation --- Oqtane.Server/Pages/Files.cshtml.cs | 24 +++++++++-------------- Oqtane.Shared/Interfaces/IImageService.cs | 6 ------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index a2bb860b..e2e3c140 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -131,8 +131,7 @@ namespace Oqtane.Pages string downloadName = file.Name; string filepath = _files.GetFilePath(file); - var etagValue = file.ModifiedOn.Ticks ^ file.Size; - + // evaluate any querystring parameters bool isRequestingImageManipulation = false; int width = 0; @@ -140,39 +139,34 @@ namespace Oqtane.Pages if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0) { isRequestingImageManipulation = true; - etagValue ^= (width * 31); } if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0) { isRequestingImageManipulation = true; - etagValue ^= (height * 17); } Request.Query.TryGetValue("mode", out var mode); Request.Query.TryGetValue("position", out var position); Request.Query.TryGetValue("background", out var background); - if (width > 0 || height > 0) - { - if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode(); - if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode(); - if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode(); - } - int rotate; if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0) { isRequestingImageManipulation = true; - etagValue ^= (rotate * 13); } - if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString())) { isRequestingImageManipulation = true; - etagValue ^= format.ToString().GetHashCode(); } - etag = Convert.ToString(etagValue, 16); + if (isRequestingImageManipulation) + { + etag = Utilities.GenerateSimpleHash(Request.QueryString.Value); + } + else + { + etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16); + } var header = ""; if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) diff --git a/Oqtane.Shared/Interfaces/IImageService.cs b/Oqtane.Shared/Interfaces/IImageService.cs index f872b72d..fdd79a56 100644 --- a/Oqtane.Shared/Interfaces/IImageService.cs +++ b/Oqtane.Shared/Interfaces/IImageService.cs @@ -1,9 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace Oqtane.Services { public interface IImageService From db24ed8b551665a6fe198f97130ddedd4cd9f89b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 28 Jan 2025 16:30:49 -0500 Subject: [PATCH 081/118] fix #5018 - redirect file download to login page --- Oqtane.Server/Pages/Files.cshtml.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index e2e3c140..5af6737a 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -122,9 +122,16 @@ namespace Oqtane.Pages if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return BrokenFile(); + if (!User.Identity.IsAuthenticated && download) + { + return Redirect(Utilities.NavigateUrl(_alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(Request.Path))); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return BrokenFile(); + } } string etag; From 527509732c693ab62e1d569d955f95d72b2f811b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 08:22:21 -0500 Subject: [PATCH 082/118] use Configuration service as it already exists --- Oqtane.Server/Startup.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 33e09e80..eeee71cd 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -172,7 +172,7 @@ namespace Oqtane } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, IConfigManager configManager, ILogger logger) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ILogger logger) { if (!string.IsNullOrEmpty(_configureServicesErrors)) { @@ -208,10 +208,10 @@ namespace Oqtane { if (!env.IsDevelopment()) { - var cachecontrol = configManager.GetSetting("CacheControl", "public, max-age=604800"); - if (!string.IsNullOrEmpty(cachecontrol)) + var cachecontrol = Configuration.GetSection("CacheControl"); + if (!string.IsNullOrEmpty(cachecontrol.Value)) { - ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol); + ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); } } var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) From db2e86e84c4d5c29a2c947973c9dc44913fb188e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 10:22:56 -0500 Subject: [PATCH 083/118] performance improvement when loading files within a folder --- Oqtane.Server/Repository/FileRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs index a589e8c9..fedbdc6e 100644 --- a/Oqtane.Server/Repository/FileRepository.cs +++ b/Oqtane.Server/Repository/FileRepository.cs @@ -46,10 +46,10 @@ namespace Oqtane.Repository files = db.File.AsNoTracking().Where(item => item.FolderId == folderId).Include(item => item.Folder).ToList(); } + var alias = _tenants.GetAlias(); foreach (var file in files) { file.Folder.PermissionList = permissions.ToList(); - var alias = _tenants.GetAlias(); file.Url = GetFileUrl(file, alias); } return files; From 24cd090c617e4105d34d6d9ff33b9c24cab11d8a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 10:42:19 -0500 Subject: [PATCH 084/118] use fingerprint term consistently --- Oqtane.Server/Components/App.razor | 14 +++++++------- Oqtane.Server/Services/SiteService.cs | 2 +- Oqtane.Shared/Models/Site.cs | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index a6edd53e..9b17e701 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -39,7 +39,7 @@ - + @if (_scripts.Contains("PWA Manifest")) { @@ -70,15 +70,15 @@ } - - - + + + @((MarkupString)_scripts) @((MarkupString)_bodyResources) @if (_renderMode == RenderModes.Static) { - + } } else @@ -94,7 +94,7 @@ private string _renderMode = RenderModes.Interactive; private string _runtime = Runtimes.Server; private bool _prerender = true; - private string _hash = ""; + private string _fingerprint = ""; private int _visitorId = -1; private string _antiForgeryToken = ""; private string _remoteIPAddress = ""; @@ -137,7 +137,7 @@ _renderMode = site.RenderMode; _runtime = site.Runtime; _prerender = site.Prerender; - _hash = site.Hash; + _fingerprint = site.Fingerprint; var modules = new List(); diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index dbf65656..52f1f4d1 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -147,7 +147,7 @@ namespace Oqtane.Services site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); // installation date used for fingerprinting static assets - site.Hash = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); + site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); } else { diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index c45cfe4e..aeb6e37b 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -188,10 +188,10 @@ namespace Oqtane.Models public List Themes { get; set; } /// - /// hash code for static assets + /// fingerprint for framework static assets /// [NotMapped] - public string Hash { get; set; } + public string Fingerprint { get; set; } public Site Clone() { @@ -234,7 +234,7 @@ namespace Oqtane.Models Pages = Pages.ConvertAll(page => page.Clone()), Languages = Languages.ConvertAll(language => language.Clone()), Themes = Themes, - Hash = Hash + Fingerprint = Fingerprint }; } From ae5f70a7391c462535190979eabe9402345c2625 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 12:27:42 -0500 Subject: [PATCH 085/118] remove Environment.IsDevelopment logic --- Oqtane.Server/Startup.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index eeee71cd..8ab42dd8 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -203,17 +203,15 @@ namespace Oqtane app.UseHttpsRedirection(); app.UseStaticFiles(new StaticFileOptions { - ServeUnknownFileTypes = true, OnPrepareResponse = (ctx) => { - if (!env.IsDevelopment()) + // static asset caching + var cachecontrol = Configuration.GetSection("CacheControl"); + if (!string.IsNullOrEmpty(cachecontrol.Value)) { - var cachecontrol = Configuration.GetSection("CacheControl"); - if (!string.IsNullOrEmpty(cachecontrol.Value)) - { - ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); - } + ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); } + // CORS headers for .NET MAUI clients var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) .ConfigureAwait(false).GetAwaiter().GetResult(); corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); From 8562a68306176f1cc143dbdcfe2187370a24d12d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 12:39:47 -0500 Subject: [PATCH 086/118] improve asset caching help text --- Oqtane.Client/Modules/Admin/Files/Edit.razor | 2 +- Oqtane.Client/Modules/Admin/SystemInfo/Index.razor | 2 +- Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx | 2 +- Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx | 2 +- Oqtane.Server/Controllers/SystemController.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 81262d50..ec8881b5 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -56,7 +56,7 @@
- +
diff --git a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor index 335800cb..8195b656 100644 --- a/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor +++ b/Oqtane.Client/Modules/Admin/SystemInfo/Index.razor @@ -133,7 +133,7 @@
- +
diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index e673f16b..c35e179e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -202,6 +202,6 @@ Caching: - Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=604800' indicates that files in this folder should be cached for 1 week. + Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI. \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx index 7c4eca2e..6a2cafee 100644 --- a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx @@ -304,6 +304,6 @@ Static Asset Caching: - Provide a Cache-Control directive for static assets. For example 'public, max-age=604800' indicates that static assets should be cached for 1 week. A blank value indicates no caching. + Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled. \ No newline at end of file diff --git a/Oqtane.Server/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs index 240a75eb..ce067c2e 100644 --- a/Oqtane.Server/Controllers/SystemController.cs +++ b/Oqtane.Server/Controllers/SystemController.cs @@ -53,7 +53,7 @@ namespace Oqtane.Controllers systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); - systeminfo.Add("CacheControl", _configManager.GetSetting("CacheControl", "public, max-age=604800")); + systeminfo.Add("CacheControl", _configManager.GetSetting("CacheControl", "")); systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl)); systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", "")); break; From 57879c189138a2c7342ed6061c9a13dd3b2fdf4e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 13:12:46 -0500 Subject: [PATCH 087/118] remove method which was relocated to PageRepository --- Oqtane.Server/Services/SiteService.cs | 40 --------------------------- 1 file changed, 40 deletions(-) diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 52f1f4d1..11c624c3 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -160,46 +160,6 @@ namespace Oqtane.Services return site; } - private static List GetPagesHierarchy(List pages) - { - List hierarchy = new List(); - Action, Page> getPath = null; - getPath = (pageList, page) => - { - IEnumerable children; - int level; - if (page == null) - { - level = -1; - children = pages.Where(item => item.ParentId == null); - } - else - { - level = page.Level; - children = pages.Where(item => item.ParentId == page.PageId); - } - foreach (Page child in children) - { - child.Level = level + 1; - child.HasChildren = pages.Any(item => item.ParentId == child.PageId && !item.IsDeleted && item.IsNavigation); - hierarchy.Add(child); - getPath(pageList, child); - } - }; - pages = pages.OrderBy(item => item.Order).ToList(); - getPath(pages, null); - - // add any non-hierarchical items to the end of the list - foreach (Page page in pages) - { - if (hierarchy.Find(item => item.PageId == page.PageId) == null) - { - hierarchy.Add(page); - } - } - return hierarchy; - } - public Task AddSiteAsync(Site site) { if (_accessor.HttpContext.User.IsInRole(RoleNames.Host)) From 6775edfd66f300dc6ee5b4044f0b7c31c6429db4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 16:02:55 -0500 Subject: [PATCH 088/118] fix logic to retrieve access token --- Oqtane.Server/Components/App.razor | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 9b17e701..712f7145 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -7,6 +7,7 @@ @using Microsoft.AspNetCore.Localization @using Microsoft.Net.Http.Headers @using Microsoft.Extensions.Primitives +@using Microsoft.AspNetCore.Authentication @using Oqtane.Client @using Oqtane.UI @using Oqtane.Repository @@ -177,7 +178,7 @@ // get jwt token for downstream APIs if (Context.User.Identity.IsAuthenticated) { - GetJwtToken(alias); + await GetJwtToken(alias); } // includes resources @@ -444,15 +445,11 @@ } } - private void GetJwtToken(Alias alias) + private async Task GetJwtToken(Alias alias) { - _authorizationToken = Context.Request.Headers[HeaderNames.Authorization]; - if (!string.IsNullOrEmpty(_authorizationToken)) - { - // bearer token was provided by remote Identity Provider and was persisted using SaveTokens - _authorizationToken = _authorizationToken.Replace("Bearer ", ""); - } - else + // bearer token may have been provided by remote Identity Provider and persisted using SaveTokens = true + _authorizationToken = await Context.GetTokenAsync("access_token"); + if (string.IsNullOrEmpty(_authorizationToken)) { // generate bearer token if a secret has been configured in User Settings var sitesettings = Context.GetSiteSettings(); From b5aa206670f4ba6d14bbdc670bbb8c250f7281ff Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 29 Jan 2025 19:05:05 -0500 Subject: [PATCH 089/118] change ResourceLoadBehavior Never to None --- Oqtane.Shared/Enums/ResourceLoadBehavior.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Shared/Enums/ResourceLoadBehavior.cs b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs index 091a014f..6751208e 100644 --- a/Oqtane.Shared/Enums/ResourceLoadBehavior.cs +++ b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs @@ -4,7 +4,7 @@ namespace Oqtane.Shared { Once, Always, - Never, + None, BlazorPageScript } } From 5a77c83e6881eeb49b801d932b6c7b4ce47a2e31 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 30 Jan 2025 08:08:16 -0500 Subject: [PATCH 090/118] update version to 6.1.0 --- Oqtane.Client/Oqtane.Client.csproj | 4 ++-- Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj | 4 ++-- .../Oqtane.Database.PostgreSQL.csproj | 4 ++-- .../Oqtane.Database.SqlServer.csproj | 4 ++-- Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj | 4 ++-- Oqtane.Maui/Oqtane.Maui.csproj | 6 +++--- Oqtane.Package/Oqtane.Client.nuspec | 4 ++-- Oqtane.Package/Oqtane.Framework.nuspec | 6 +++--- Oqtane.Package/Oqtane.Server.nuspec | 4 ++-- Oqtane.Package/Oqtane.Shared.nuspec | 4 ++-- Oqtane.Package/Oqtane.Updater.nuspec | 4 ++-- Oqtane.Package/install.ps1 | 2 +- Oqtane.Package/upgrade.ps1 | 2 +- Oqtane.Server/Infrastructure/UpgradeManager.cs | 8 ++++---- ...201_AddThemeVersion.cs => 06010001_AddThemeVersion.cs} | 2 +- ...rCacheControl.cs => 06010001_AddFolderCacheControl.cs} | 2 +- Oqtane.Server/Oqtane.Server.csproj | 4 ++-- Oqtane.Shared/Oqtane.Shared.csproj | 4 ++-- Oqtane.Shared/Shared/Constants.cs | 4 ++-- Oqtane.Updater/Oqtane.Updater.csproj | 4 ++-- 20 files changed, 40 insertions(+), 40 deletions(-) rename Oqtane.Server/Migrations/Master/{06000201_AddThemeVersion.cs => 06010001_AddThemeVersion.cs} (95%) rename Oqtane.Server/Migrations/Tenant/{06000201_AddFolderCacheControl.cs => 06010001_AddFolderCacheControl.cs} (95%) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index fe970ff9..4439f011 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net9.0 Exe Debug;Release - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index 59428c93..df6c506a 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index fb90faa1..f936db4a 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index d358b88d..2cc6f942 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index c4f90077..7d1d72e2 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net9.0 - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 09f24894..98ae9384 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -30,7 +30,7 @@ com.oqtane.maui - 6.0.2 + 6.1.0 1 diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 1d82676d..ae243057 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 6.0.2 + 6.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index a268e043..085cad61 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 6.0.2 + 6.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v6.0.2/Oqtane.Framework.6.0.2.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/download/v6.1.0/Oqtane.Framework.6.1.0.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 8e0a0fcc..2f5380a9 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 6.0.2 + 6.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 1b8db074..92b94496 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 6.0.2 + 6.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index ea5a8715..c0e5e5db 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 6.0.2 + 6.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index b94436f3..7336d3b4 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.2.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 5fbb0191..80e44c4d 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.2.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.0.Upgrade.zip" -Force diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index b4e8aedf..b32adaeb 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -74,8 +74,8 @@ namespace Oqtane.Infrastructure case "6.0.1": Upgrade_6_0_1(tenant, scope); break; - case "6.0.2": - Upgrade_6_0_2(tenant, scope); + case "6.1.0": + Upgrade_6_1_0(tenant, scope); break; } } @@ -484,14 +484,14 @@ namespace Oqtane.Infrastructure RemoveAssemblies(assemblies, "6.0.1"); } - private void Upgrade_6_0_2(Tenant tenant, IServiceScope scope) + private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope) { // remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql) string[] assemblies = { "MySql.EntityFrameworkCore.dll" }; - RemoveAssemblies(assemblies, "6.0.2"); + RemoveAssemblies(assemblies, "6.1.0"); } private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) diff --git a/Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs b/Oqtane.Server/Migrations/Master/06010001_AddThemeVersion.cs similarity index 95% rename from Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs rename to Oqtane.Server/Migrations/Master/06010001_AddThemeVersion.cs index def80a48..d88f55c1 100644 --- a/Oqtane.Server/Migrations/Master/06000201_AddThemeVersion.cs +++ b/Oqtane.Server/Migrations/Master/06010001_AddThemeVersion.cs @@ -7,7 +7,7 @@ using Oqtane.Repository; namespace Oqtane.Migrations.Master { [DbContext(typeof(MasterDBContext))] - [Migration("Master.06.00.02.01")] + [Migration("Master.06.01.00.01")] public class AddThemeVersion : MultiDatabaseMigration { public AddThemeVersion(IDatabase database) : base(database) diff --git a/Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs b/Oqtane.Server/Migrations/Tenant/06010001_AddFolderCacheControl.cs similarity index 95% rename from Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs rename to Oqtane.Server/Migrations/Tenant/06010001_AddFolderCacheControl.cs index 7e566bea..d2cd810b 100644 --- a/Oqtane.Server/Migrations/Tenant/06000201_AddFolderCacheControl.cs +++ b/Oqtane.Server/Migrations/Tenant/06010001_AddFolderCacheControl.cs @@ -7,7 +7,7 @@ using Oqtane.Repository; namespace Oqtane.Migrations.Tenant { [DbContext(typeof(TenantDBContext))] - [Migration("Tenant.06.00.02.01")] + [Migration("Tenant.06.01.00.01")] public class AddFolderCacheControl : MultiDatabaseMigration { public AddFolderCacheControl(IDatabase database) : base(database) diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 9e06b595..5c1852df 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index d1e7c0b7..91f13025 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 04d3bda7..447da2fa 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "6.0.2"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.0.2"; + public static readonly string Version = "6.1.0"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 79332b79..58cd963e 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net9.0 Exe - 6.0.2 + 6.1.0 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.2 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0 https://github.com/oqtane/oqtane.framework Git Oqtane From f7cf25c4bb78662fc97da33bb225e28dca968528 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 30 Jan 2025 08:39:49 -0500 Subject: [PATCH 091/118] fix upgrade issue which can occur in development environments --- .../Infrastructure/UpgradeManager.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index b32adaeb..7d386eb6 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Oqtane.Models; using Oqtane.Repository; @@ -507,18 +508,22 @@ namespace Oqtane.Infrastructure private void RemoveAssemblies(string[] assemblies, string version) { - foreach (var assembly in assemblies) + // in a development environment assemblies cannot be removed as the debugger runs fron /bin folder and locks the files + if (!_environment.IsDevelopment()) { - try + foreach (var assembly in assemblies) { - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - var filepath = Path.Combine(binFolder, assembly); - if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); - } - catch (Exception ex) - { - // error deleting asesmbly - _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: {version} Upgrade Error Removing {assembly} - {ex}")); + try + { + var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var filepath = Path.Combine(binFolder, assembly); + if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath); + } + catch (Exception ex) + { + // error deleting asesmbly + _filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: {version} Upgrade Error Removing {assembly} - {ex}")); + } } } } From a87af264eb80953140f54ef4412e908eb6e549d5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 31 Jan 2025 08:42:36 -0500 Subject: [PATCH 092/118] added a ScriptsLoaded property in ModuleBase and ThemeBase for flow control in Interactive rendering scenarios --- Oqtane.Client/Modules/ModuleBase.cs | 10 ++++++++++ Oqtane.Client/Themes/ThemeBase.cs | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index e2f89acc..fe40ddf6 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -18,6 +18,7 @@ namespace Oqtane.Modules private Logger _logger; private string _urlparametersstate; private Dictionary _urlparameters; + private bool _scriptsloaded = false; protected Logger logger => _logger ?? (_logger = new Logger(this)); @@ -117,6 +118,7 @@ namespace Oqtane.Modules await interop.IncludeScripts(scripts.ToArray()); } } + _scriptsloaded = true; } } @@ -125,6 +127,14 @@ namespace Oqtane.Modules return PageState?.RenderId == ModuleState?.RenderId; } + public bool ScriptsLoaded + { + get + { + return _scriptsloaded; + } + } + // path method public string ModulePath() diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index b56d958f..aa5eaf5b 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -15,6 +15,8 @@ namespace Oqtane.Themes { public abstract class ThemeBase : ComponentBase, IThemeControl { + private bool _scriptsloaded = false; + [Inject] protected ILogService LoggingService { get; set; } @@ -82,6 +84,15 @@ namespace Oqtane.Themes } } } + _scriptsloaded = true; + } + + public bool ScriptsLoaded + { + get + { + return _scriptsloaded; + } } // property for obtaining theme information about this theme component From 1fd2aedf968f44c7309cc414d2399215bedfa826 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 31 Jan 2025 09:18:44 -0500 Subject: [PATCH 093/118] make Kestrel the default profile in launchjSettings.json --- Oqtane.Client/Properties/launchSettings.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Oqtane.Client/Properties/launchSettings.json b/Oqtane.Client/Properties/launchSettings.json index 4899842e..073f481f 100644 --- a/Oqtane.Client/Properties/launchSettings.json +++ b/Oqtane.Client/Properties/launchSettings.json @@ -8,20 +8,20 @@ } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Oqtane.Client": { + "Oqtane": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:44358/" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file From de6c57a7ee13d688b4824d448c6a2cd91aeb90c5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 31 Jan 2025 11:14:13 -0500 Subject: [PATCH 094/118] add user impersonation --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 26 ++++++ .../Resources/Modules/Admin/Users/Edit.resx | 6 ++ Oqtane.Server/Managers/UserManager.cs | 3 +- Oqtane.Server/Pages/Impersonate.cshtml | 3 + Oqtane.Server/Pages/Impersonate.cshtml.cs | 79 +++++++++++++++++++ Oqtane.Server/Pages/Login.cshtml.cs | 18 ++++- Oqtane.Shared/Security/UserSecurity.cs | 7 +- 7 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 Oqtane.Server/Pages/Impersonate.cshtml create mode 100644 Oqtane.Server/Pages/Impersonate.cshtml.cs diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index e4aedaa0..13bff401 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject IFileService FileService +@inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -130,6 +131,10 @@ @SharedLocalizer["Cancel"] + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) + { + + } @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") { @@ -152,6 +157,7 @@ private string isdeleted; private string lastlogin; private string lastipaddress; + private bool ishost = false; private List profiles; private Dictionary userSettings; @@ -186,6 +192,7 @@ isdeleted = user.IsDeleted.ToString(); lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); lastipaddress = user.LastIPAddress; + ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); userSettings = user.Settings; createdby = user.CreatedBy; @@ -267,6 +274,25 @@ } } + private async Task ImpersonateUser() + { + try + { + await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username); + + // post back to the server so that the cookies are set correctly + var interop = new Interop(JSRuntime); + var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path }; + string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/"); + await interop.SubmitForm(url, fields); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message); + AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error); + } + } + private async Task DeleteUser() { try diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index 29aed594..3dbd1d1e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -204,4 +204,10 @@ Are You Sure You Wish To Permanently Delete This User? + + Impersonate + + + Unable To Impersonate User + \ No newline at end of file diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 694bfd03..84679d23 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -12,6 +12,7 @@ using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; +using Oqtane.Security; using Oqtane.Shared; namespace Oqtane.Managers @@ -369,7 +370,7 @@ namespace Oqtane.Managers if (user != null) { // ensure user is registered for site - if (user.Roles.Contains(RoleNames.Registered)) + if (UserSecurity.ContainsRole(user.Roles, RoleNames.Registered)) { user.IsAuthenticated = true; user.LastLoginOn = DateTime.UtcNow; diff --git a/Oqtane.Server/Pages/Impersonate.cshtml b/Oqtane.Server/Pages/Impersonate.cshtml new file mode 100644 index 00000000..169e999e --- /dev/null +++ b/Oqtane.Server/Pages/Impersonate.cshtml @@ -0,0 +1,3 @@ +@page "/pages/impersonate" +@namespace Oqtane.Pages +@model Oqtane.Pages.ImpersonateModel diff --git a/Oqtane.Server/Pages/Impersonate.cshtml.cs b/Oqtane.Server/Pages/Impersonate.cshtml.cs new file mode 100644 index 00000000..951c1a80 --- /dev/null +++ b/Oqtane.Server/Pages/Impersonate.cshtml.cs @@ -0,0 +1,79 @@ +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; +using Oqtane.Extensions; +using Oqtane.Infrastructure; +using Oqtane.Managers; +using Oqtane.Security; +using Oqtane.Shared; + +namespace Oqtane.Pages +{ + public class ImpersonateModel : PageModel + { + private readonly UserManager _identityUserManager; + private readonly SignInManager _identitySignInManager; + private readonly IUserManager _userManager; + private readonly ILogManager _logger; + + public ImpersonateModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) + { + _identityUserManager = identityUserManager; + _identitySignInManager = identitySignInManager; + _userManager = userManager; + _logger = logger; + } + + public async Task OnPostAsync(string username, string returnurl) + { + if (User.IsInRole(RoleNames.Admin) && !string.IsNullOrEmpty(username)) + { + bool validuser = false; + IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username); + if (identityuser != null) + { + var alias = HttpContext.GetAlias(); + var user = _userManager.GetUser(identityuser.UserName, alias.SiteId); + if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered) && !UserSecurity.ContainsRole(user.Roles, RoleNames.Host)) + { + validuser = true; + } + } + + if (validuser) + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Successfully Impersonated By Administrator {Administrator}", username, User.Identity.Name); + + // note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync + await _identitySignInManager.SignInAsync(identityuser, false); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Impersonation By Administrator {Administrator} Failed For User {Username} ", User.Identity.Name, username); + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Impersonate User {Username} By User {User}", username, User.Identity.Name); + } + + if (returnurl == null) + { + returnurl = ""; + } + else + { + returnurl = WebUtility.UrlDecode(returnurl); + } + if (!returnurl.StartsWith("/")) + { + returnurl = "/" + returnurl; + } + + return LocalRedirect(Url.Content("~" + returnurl)); + } + } +} diff --git a/Oqtane.Server/Pages/Login.cshtml.cs b/Oqtane.Server/Pages/Login.cshtml.cs index 87fceedb..b1b01201 100644 --- a/Oqtane.Server/Pages/Login.cshtml.cs +++ b/Oqtane.Server/Pages/Login.cshtml.cs @@ -4,8 +4,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Enums; using Oqtane.Extensions; +using Oqtane.Infrastructure; using Oqtane.Managers; +using Oqtane.Security; using Oqtane.Shared; namespace Oqtane.Pages @@ -16,12 +19,14 @@ namespace Oqtane.Pages private readonly UserManager _identityUserManager; private readonly SignInManager _identitySignInManager; private readonly IUserManager _userManager; + private readonly ILogManager _logger; - public LoginModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager) + public LoginModel(UserManager identityUserManager, SignInManager identitySignInManager, IUserManager userManager, ILogManager logger) { _identityUserManager = identityUserManager; _identitySignInManager = identitySignInManager; _userManager = userManager; + _logger = logger; } public async Task OnPostAsync(string username, string password, bool remember, string returnurl) @@ -37,7 +42,7 @@ namespace Oqtane.Pages { var alias = HttpContext.GetAlias(); var user = _userManager.GetUser(identityuser.UserName, alias.SiteId); - if (user != null && !user.IsDeleted) + if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered)) { validuser = true; } @@ -48,7 +53,16 @@ namespace Oqtane.Pages { // note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync await _identitySignInManager.SignInAsync(identityuser, remember); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Successful For User {Username}", username); } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Failed For User {Username}", username); + } + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Login User {Username}", username); } if (returnurl == null) diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index 4d954037..7503238d 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -72,6 +72,11 @@ namespace Oqtane.Security return isAuthorized; } + public static bool ContainsRole(string roles, string roleName) + { + return roles.Split(';', StringSplitOptions.RemoveEmptyEntries).Contains(roleName); + } + public static bool ContainsRole(List permissions, string permissionName, string roleName) { return permissions.Any(item => item.PermissionName == permissionName && item.RoleName == roleName); @@ -101,7 +106,7 @@ namespace Oqtane.Security identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString())); identity.AddClaim(new Claim(Constants.SiteKeyClaimType, alias.SiteKey)); - if (user.Roles.Contains(RoleNames.Host)) + if (ContainsRole(user.Roles, RoleNames.Host)) { // host users are site admins by default identity.AddClaim(new Claim(ClaimTypes.Role, RoleNames.Host)); From 0dd075271036a786896235bfe52349884fc240a6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 31 Jan 2025 14:50:54 -0500 Subject: [PATCH 095/118] modify button spacing --- Oqtane.Client/Modules/Admin/Users/Edit.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 13bff401..d011c699 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -133,7 +133,7 @@ @SharedLocalizer["Cancel"] @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) { - + } @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") { From 534353ce13dae218e27895de99e004bafa79b4b1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 3 Feb 2025 10:35:17 -0500 Subject: [PATCH 096/118] add additional Script class constructors --- Oqtane.Shared/Models/Script.cs | 40 ++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/Oqtane.Shared/Models/Script.cs b/Oqtane.Shared/Models/Script.cs index 1fd5d612..1bcdceb6 100644 --- a/Oqtane.Shared/Models/Script.cs +++ b/Oqtane.Shared/Models/Script.cs @@ -13,14 +13,41 @@ namespace Oqtane.Models { this.ResourceType = ResourceType.Script; this.Location = ResourceLocation.Body; + this.LoadBehavior = ResourceLoadBehavior.Once; } + // external script constructors + public Script(string Src) { SetDefaults(); this.Url = Src; } + public Script(string Src, string Integrity, string CrossOrigin) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + } + + public Script(string Src, string Integrity, string CrossOrigin, ResourceLocation Location, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string Type, string Bundle, string RenderMode) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + this.Location = Location; + this.LoadBehavior = LoadBehavior; + this.DataAttributes = DataAttributes; + this.Type = Type; + this.Bundle = Bundle; + this.RenderMode = RenderMode; + } + + // inline script constructors + public Script(string Content, string Type) { SetDefaults(); @@ -35,14 +62,19 @@ namespace Oqtane.Models this.LoadBehavior = LoadBehavior; } - public Script(string Src, string Integrity, string CrossOrigin) + public Script(string Content, ResourceLocation Location, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string Type, string RenderMode) { SetDefaults(); - this.Url = Src; - this.Integrity = Integrity; - this.CrossOrigin = CrossOrigin; + this.Content = Content; + this.Location = Location; + this.LoadBehavior = LoadBehavior; + this.DataAttributes = DataAttributes; + this.Type = Type; + this.RenderMode = RenderMode; } + // general constructor + public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string RenderMode) { SetDefaults(); From f3dbeae28efbe0d4d396bd28cda14eae53fe801d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 3 Feb 2025 11:00:22 -0500 Subject: [PATCH 097/118] fix #5044 - improve file part removal logic --- Oqtane.Server/Controllers/FileController.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 70d17d1d..847cdfa8 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -510,10 +510,10 @@ namespace Oqtane.Controllers filename = Path.GetFileNameWithoutExtension(filename); // base filename string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts - // if all of the file parts exist ( note that file parts can arrive out of order ) + // if all of the file parts exist (note that file parts can arrive out of order) if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) { - // merge file parts into temp file ( in case another user is trying to get the file ) + // merge file parts into temp file (in case another user is trying to get the file) bool success = true; using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) { @@ -536,17 +536,23 @@ namespace Oqtane.Controllers // clean up file parts foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) { - // file name matches part or is more than 2 hours old (ie. a prior file upload failed) - if (fileparts.Contains(file) || System.IO.File.GetCreationTime(file).ToUniversalTime() < DateTime.UtcNow.AddHours(-2)) + if (fileparts.Contains(file)) { - System.IO.File.Delete(file); + try + { + System.IO.File.Delete(file); + } + catch + { + // unable to delete part - ignore + } } } // rename temp file if (success) { - // remove file if it already exists (as well as any thumbnails) + // remove file if it already exists (as well as any thumbnails which may exist) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) { if (Path.GetExtension(file) != ".tmp") From 9dd6dc75231f4e1a5e3d620cdfda4a52c558d604 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 5 Feb 2025 16:48:34 -0500 Subject: [PATCH 098/118] file upload improvements --- .../Modules/Controls/FileManager.razor | 50 +--- Oqtane.Client/UI/Interop.cs | 13 +- Oqtane.Server/Controllers/FileController.cs | 18 +- Oqtane.Server/wwwroot/js/interop.js | 249 +++++++++++++----- 4 files changed, 204 insertions(+), 126 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 509cef2d..71c64dc0 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -157,6 +157,9 @@ [Parameter] public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false + [Parameter] + public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB + [Parameter] public EventCallback OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded @@ -383,51 +386,8 @@ StateHasChanged(); } - await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt); - - // uploading is asynchronous so we need to poll to determine if uploads are completed - var success = true; - int upload = 0; - while (upload < uploads.Length && success) - { - success = false; - var filename = uploads[upload].Split(':')[0]; - - var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes - var megabits = (size / 1048576.0) * 8; // binary conversion - var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload - var uploadtime = (megabits / uploadspeed); // seconds - var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds) - var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds - - int attempts = 0; - while (attempts < maxattempts && !success) - { - attempts += 1; - Thread.Sleep(sleep); - - if (Folder == Constants.PackagesFolder) - { - var files = await FileService.GetFilesAsync(folder); - if (files != null && files.Any(item => item.Name == filename)) - { - success = true; - } - } - else - { - var file = await FileService.GetFileAsync(int.Parse(folder), filename); - if (file != null) - { - success = true; - } - } - } - if (success) - { - upload++; - } - } + // upload files + var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, ChunkSize); // reset progress indicators if (ShowProgress) diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 8183aff5..bda75505 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -209,17 +209,22 @@ namespace Oqtane.UI } public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) + { + UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1); + return Task.CompletedTask; + } + + public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize) { try { - _jsRuntime.InvokeVoidAsync( + return _jsRuntime.InvokeAsync( "Oqtane.Interop.uploadFiles", - posturl, folder, id, antiforgerytoken, jwt); - return Task.CompletedTask; + posturl, folder, id, antiforgerytoken, jwt, chunksize); } catch { - return Task.CompletedTask; + return new ValueTask(Task.FromResult(false)); } } diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 847cdfa8..c754b689 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -21,6 +21,7 @@ using System.Net.Http; using Microsoft.AspNetCore.Cors; using System.IO.Compression; using Oqtane.Services; +using Microsoft.Extensions.Primitives; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -427,7 +428,7 @@ namespace Oqtane.Controllers // POST api//upload [EnableCors(Constants.MauiCorsPolicy)] [HttpPost("upload")] - public async Task UploadFile(string folder, IFormFile formfile) + public async Task UploadFile([FromForm] string folder, IFormFile formfile) { if (formfile == null || formfile.Length <= 0) { @@ -435,13 +436,20 @@ namespace Oqtane.Controllers } // ensure filename is valid - string token = ".part_"; - if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token)))) + if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) { _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); return NoContent(); } + // ensure headers exist + if (!Request.Headers.TryGetValue("PartCount", out StringValues partCount) || !Request.Headers.TryGetValue("TotalParts", out StringValues totalParts)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Request Is Missing Required Headers"); + return NoContent(); + } + + string fileName = formfile.FileName + ".part_" + int.Parse(partCount).ToString("000") + "_" + int.Parse(totalParts).ToString("000"); string folderPath = ""; int FolderId; @@ -465,12 +473,12 @@ namespace Oqtane.Controllers if (!string.IsNullOrEmpty(folderPath)) { CreateDirectory(folderPath); - using (var stream = new FileStream(Path.Combine(folderPath, formfile.FileName), FileMode.Create)) + using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create)) { await formfile.CopyToAsync(stream); } - string upload = await MergeFile(folderPath, formfile.FileName); + string upload = await MergeFile(folderPath, fileName); if (upload != "" && FolderId != -1) { var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload)); diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index ee81109c..130ee74b 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -308,97 +308,202 @@ Oqtane.Interop = { } return files; }, - uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { + uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { + var success = true; var fileinput = document.getElementById('FileInput_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id); var progressbar = document.getElementById('ProgressBar_' + id); + var totalSize = 0; + for (var i = 0; i < fileinput.files.length; i++) { + totalSize += fileinput.files[i].size; + } + let uploadSize = 0; + + if (!chunksize) { + chunksize = 1; // 1 MB default + } + if (progressinfo !== null && progressbar !== null) { + progressinfo.setAttribute('style', 'display: inline;'); + if (fileinput.files.length > 1) { + progressinfo.innerHTML = fileinput.files[0].name + ', ...'; + } + else { + progressinfo.innerHTML = fileinput.files[0].name; + } + progressbar.setAttribute('style', 'width: 100%; display: inline;'); + progressbar.value = 0; + } + + const uploadFiles = Array.from(fileinput.files).map(file => { + const uploadFile = () => { + const chunkSize = chunksize * (1024 * 1024); + const totalParts = Math.ceil(file.size / chunkSize); + let partCount = 0; + + const uploadPart = () => { + const start = partCount * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + return new Promise((resolve, reject) => { + let formdata = new FormData(); + formdata.append('__RequestVerificationToken', antiforgerytoken); + formdata.append('folder', folder); + formdata.append('formfile', chunk, file.name); + + var credentials = 'same-origin'; + var headers = new Headers(); + headers.append('PartCount', partCount + 1); + headers.append('TotalParts', totalParts); + if (jwt !== "") { + headers.append('Authorization', 'Bearer ' + jwt); + credentials = 'include'; + } + + return fetch(posturl, { + method: 'POST', + headers: headers, + credentials: credentials, + body: formdata + }) + .then(response => { + if (!response.ok) { + if (progressinfo !== null) { + progressinfo.innerHTML = 'Error: ' + response.statusText; + } + throw new Error('Failed'); + } + return; + }) + .then(data => { + partCount++; + if (progressbar !== null) { + uploadSize += chunk.size; + var percent = Math.ceil((uploadSize / totalSize) * 100); + progressbar.value = (percent / 100); + } + if (partCount < totalParts) { + uploadPart().then(resolve).catch(reject); + } + else { + resolve(data); + } + }) + .catch(error => { + reject(error); + }); + }); + }; + + return uploadPart(); + }; + + return uploadFile(); + }); + + try { + await Promise.all(uploadFiles); + } catch (error) { + success = false; + } + + fileinput.value = ''; + return success; + }, + uploadFile: function (posturl, folder, id, antiforgerytoken, jwt, chunksize, totalsize, file) { + var fileinput = document.getElementById('FileInput_' + id); + var progressinfo = document.getElementById('ProgressInfo_' + id); + var progressbar = document.getElementById('ProgressBar_' + id); + + if (file === null && fileinput !== null) { + file = fileinput.files[0]; + totalsize = file.size; + } + + if (progressinfo !== null && progressbar !== null && fileinput.files.length === 1) { progressinfo.setAttribute("style", "display: inline;"); - progressinfo.innerHTML = ''; + progressinfo.innerHTML = file.name; progressbar.setAttribute("style", "width: 100%; display: inline;"); progressbar.value = 0; } - var files = fileinput.files; - var totalSize = 0; - for (var i = 0; i < files.length; i++) { - totalSize = totalSize + files[i].size; + if (!chunksize) { + chunksize = 1; // 1 MB default } + const chunkSize = chunksize * (1024 * 1024); + let uploadSize = 0; + const totalParts = Math.ceil(file.size / chunkSize); + let partCount = 0; + const maxThreads = 1; + let threadCount = 1; - var maxChunkSizeMB = 1; - var bufferChunkSize = maxChunkSizeMB * (1024 * 1024); - var uploadedSize = 0; + const uploadPart = () => { + const start = partCount * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); - for (var i = 0; i < files.length; i++) { - var fileChunk = []; - var file = files[i]; - var fileStreamPos = 0; - var endPos = bufferChunkSize; - - while (fileStreamPos < file.size) { - fileChunk.push(file.slice(fileStreamPos, endPos)); - fileStreamPos = endPos; - endPos = fileStreamPos + bufferChunkSize; + while (threadCount > maxThreads) { + // wait for thread to become available } + threadCount++; - var totalParts = fileChunk.length; - var partCount = 0; + return new Promise((resolve, reject) => { + let formdata = new FormData(); + formdata.append('__RequestVerificationToken', antiforgerytoken); + formdata.append('folder', folder); + formdata.append('formfile', chunk, file.name); - while (chunk = fileChunk.shift()) { - partCount++; - var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0'); - - var data = new FormData(); - data.append('__RequestVerificationToken', antiforgerytoken); - data.append('folder', folder); - data.append('formfile', chunk, fileName); - var request = new XMLHttpRequest(); - request.open('POST', posturl, true); + var credentials = 'same-origin'; + var headers = new Headers(); + headers.append('PartCount', partCount + 1); + headers.append('TotalParts', totalParts); if (jwt !== "") { - request.setRequestHeader('Authorization', 'Bearer ' + jwt); - request.withCredentials = true; + headers.append('Authorization', 'Bearer ' + jwt); + credentials = 'include'; } - request.upload.onloadstart = function (e) { - if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') { - if (files.length === 1) { - progressinfo.innerHTML = file.name; - } - else { - progressinfo.innerHTML = file.name + ", ..."; - } - } - }; - request.upload.onprogress = function (e) { - if (progressinfo !== null && progressbar !== null) { - var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100); - progressbar.value = (percent / 100); - } - }; - request.upload.onloadend = function (e) { - if (progressinfo !== null && progressbar !== null) { - uploadedSize = uploadedSize + e.total; - var percent = Math.ceil((uploadedSize / totalSize) * 100); - progressbar.value = (percent / 100); - } - }; - request.upload.onerror = function() { - if (progressinfo !== null && progressbar !== null) { - if (files.length === 1) { - progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; - } - else { - progressinfo.innerHTML = ' Error: ' + request.statusText; - } - } - }; - request.send(data); - } - if (i === files.length - 1) { - fileinput.value = ''; - } - } + return fetch(posturl, { + method: 'POST', + headers: headers, + credentials: credentials, + body: formdata + }) + .then(response => { + if (!response.ok) { + if (progressinfo !== null) { + progressinfo.innerHTML = ' Error: ' + response.statusText; + } + throw new Error('Failed'); + } + if (progressbar !== null) { + progressbar.value = 1; + } + return; + }) + .then(data => { + partCount++; + if (progressbar !== null) { + uploadSize += chunk.size; + var percent = Math.ceil((uploadSize / totalsize) * 100); + progressbar.value = (percent / 100); + } + threadCount--; + if (partCount < totalParts) { + uploadPart().then(resolve).catch(reject); + } + else { + resolve(data); + } + }) + .catch(error => { + reject(error); + }); + }); + }; + + return uploadPart(); }, refreshBrowser: function (verify, wait) { async function attemptReload (verify) { From e2af4f74c3b449994dc4f74985f80bf25e23fb68 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 5 Feb 2025 17:08:32 -0500 Subject: [PATCH 099/118] remove uploadFile() method as it is not used --- Oqtane.Server/wwwroot/js/interop.js | 93 ----------------------------- 1 file changed, 93 deletions(-) diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 130ee74b..b7e60d5c 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -412,99 +412,6 @@ Oqtane.Interop = { fileinput.value = ''; return success; }, - uploadFile: function (posturl, folder, id, antiforgerytoken, jwt, chunksize, totalsize, file) { - var fileinput = document.getElementById('FileInput_' + id); - var progressinfo = document.getElementById('ProgressInfo_' + id); - var progressbar = document.getElementById('ProgressBar_' + id); - - if (file === null && fileinput !== null) { - file = fileinput.files[0]; - totalsize = file.size; - } - - if (progressinfo !== null && progressbar !== null && fileinput.files.length === 1) { - progressinfo.setAttribute("style", "display: inline;"); - progressinfo.innerHTML = file.name; - progressbar.setAttribute("style", "width: 100%; display: inline;"); - progressbar.value = 0; - } - - if (!chunksize) { - chunksize = 1; // 1 MB default - } - const chunkSize = chunksize * (1024 * 1024); - let uploadSize = 0; - const totalParts = Math.ceil(file.size / chunkSize); - let partCount = 0; - const maxThreads = 1; - let threadCount = 1; - - const uploadPart = () => { - const start = partCount * chunkSize; - const end = Math.min(start + chunkSize, file.size); - const chunk = file.slice(start, end); - - while (threadCount > maxThreads) { - // wait for thread to become available - } - threadCount++; - - return new Promise((resolve, reject) => { - let formdata = new FormData(); - formdata.append('__RequestVerificationToken', antiforgerytoken); - formdata.append('folder', folder); - formdata.append('formfile', chunk, file.name); - - var credentials = 'same-origin'; - var headers = new Headers(); - headers.append('PartCount', partCount + 1); - headers.append('TotalParts', totalParts); - if (jwt !== "") { - headers.append('Authorization', 'Bearer ' + jwt); - credentials = 'include'; - } - - return fetch(posturl, { - method: 'POST', - headers: headers, - credentials: credentials, - body: formdata - }) - .then(response => { - if (!response.ok) { - if (progressinfo !== null) { - progressinfo.innerHTML = ' Error: ' + response.statusText; - } - throw new Error('Failed'); - } - if (progressbar !== null) { - progressbar.value = 1; - } - return; - }) - .then(data => { - partCount++; - if (progressbar !== null) { - uploadSize += chunk.size; - var percent = Math.ceil((uploadSize / totalsize) * 100); - progressbar.value = (percent / 100); - } - threadCount--; - if (partCount < totalParts) { - uploadPart().then(resolve).catch(reject); - } - else { - resolve(data); - } - }) - .catch(error => { - reject(error); - }); - }); - }; - - return uploadPart(); - }, refreshBrowser: function (verify, wait) { async function attemptReload (verify) { if (verify) { From dec0c0649c9e8096fc58758de92977c52ae06236 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 5 Feb 2025 19:09:55 -0500 Subject: [PATCH 100/118] modified file upload error message to reflect new behavior --- Oqtane.Client/Modules/Controls/FileManager.razor | 2 +- Oqtane.Client/Resources/Modules/Controls/FileManager.resx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 71c64dc0..57c3d19b 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -412,7 +412,7 @@ } else { - await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads); + await logger.LogInformation("File Upload Failed {Files}", uploads); _message = Localizer["Error.File.Upload"]; _messagetype = MessageType.Error; } diff --git a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx index c4e84b7b..c2ccac05 100644 --- a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx +++ b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx @@ -127,7 +127,7 @@ Error Loading Files - File Upload Failed Or Is Still In Progress + File Upload Failed You Have Not Selected A File To Upload From 8c83a18f931239caaa0a0e9a080c8868cde6ba35 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Feb 2025 08:19:57 -0500 Subject: [PATCH 101/118] improve file upload validation and error handling on server --- Oqtane.Server/Controllers/FileController.cs | 123 +++++++++++--------- 1 file changed, 70 insertions(+), 53 deletions(-) diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index c754b689..4bdb8a90 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Cors; using System.IO.Compression; using Oqtane.Services; using Microsoft.Extensions.Primitives; +using Microsoft.AspNetCore.Http.HttpResults; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -430,80 +431,96 @@ namespace Oqtane.Controllers [HttpPost("upload")] public async Task UploadFile([FromForm] string folder, IFormFile formfile) { + if (string.IsNullOrEmpty(folder)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A Folder"); + return StatusCode((int)HttpStatusCode.Forbidden); + } + if (formfile == null || formfile.Length <= 0) { - return NoContent(); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A File"); + return StatusCode((int)HttpStatusCode.Forbidden); } // ensure filename is valid if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); - return NoContent(); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); + return StatusCode((int)HttpStatusCode.Forbidden); } // ensure headers exist - if (!Request.Headers.TryGetValue("PartCount", out StringValues partCount) || !Request.Headers.TryGetValue("TotalParts", out StringValues totalParts)) + if (!Request.Headers.TryGetValue("PartCount", out StringValues partcount) || !int.TryParse(partcount, out int partCount) || partCount <= 0 || + !Request.Headers.TryGetValue("TotalParts", out StringValues totalparts) || !int.TryParse(totalparts, out int totalParts) || totalParts <= 0) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Request Is Missing Required Headers"); - return NoContent(); + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Is Missing Required Headers"); + return StatusCode((int)HttpStatusCode.Forbidden); } - string fileName = formfile.FileName + ".part_" + int.Parse(partCount).ToString("000") + "_" + int.Parse(totalParts).ToString("000"); + // create file name using header values + string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); string folderPath = ""; - int FolderId; - if (int.TryParse(folder, out FolderId)) + try { - Folder Folder = _folders.GetFolder(FolderId); - if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList)) + int FolderId; + if (int.TryParse(folder, out FolderId)) { - folderPath = _folders.GetFolderPath(Folder); - } - } - else - { - FolderId = -1; - if (User.IsInRole(RoleNames.Host)) - { - folderPath = GetFolderPath(folder); - } - } - - if (!string.IsNullOrEmpty(folderPath)) - { - CreateDirectory(folderPath); - using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create)) - { - await formfile.CopyToAsync(stream); - } - - string upload = await MergeFile(folderPath, fileName); - if (upload != "" && FolderId != -1) - { - var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload)); - if (file != null) + Folder Folder = _folders.GetFolder(FolderId); + if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList)) { - if (file.FileId == 0) - { - file = _files.AddFile(file); - } - else - { - file = _files.UpdateFile(file); - } - _logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload)); - _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create); + folderPath = _folders.GetFolderPath(Folder); + } + } + else + { + FolderId = -1; + if (User.IsInRole(RoleNames.Host)) + { + folderPath = GetFolderPath(folder); } } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - } - return NoContent(); + if (!string.IsNullOrEmpty(folderPath)) + { + CreateDirectory(folderPath); + using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create)) + { + await formfile.CopyToAsync(stream); + } + + string upload = await MergeFile(folderPath, fileName); + if (upload != "" && FolderId != -1) + { + var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload)); + if (file != null) + { + if (file.FileId == 0) + { + file = _files.AddFile(file); + } + else + { + file = _files.UpdateFile(file); + } + _logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload)); + _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create); + } + } + return NoContent(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName); + return StatusCode((int)HttpStatusCode.Forbidden); + } + } + catch (Exception ex) + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Upload Attempt Failed {Folder} {File}", folder, formfile.FileName); + return StatusCode((int)HttpStatusCode.InternalServerError); + } } private async Task MergeFile(string folder, string filename) From 1ebc8ebff3876a9dd78140bebd7eadb57c20e057 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Feb 2025 11:16:30 -0500 Subject: [PATCH 102/118] fix #5061 - configure Page Management for personalizable pages --- Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index bbfe9eb9..90b351ef 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -266,6 +266,7 @@ namespace Oqtane.SiteTemplates PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.View, RoleNames.Registered, true), // required to support personalized pages new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List @@ -276,6 +277,7 @@ namespace Oqtane.SiteTemplates PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.View, RoleNames.Registered, true), // required to support personalized pages new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, Content = "" From 620c768e058a107a736efbdb75de8126ea345bcb Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Feb 2025 12:21:34 -0500 Subject: [PATCH 103/118] fix LogLevel for file upload error --- Oqtane.Client/Modules/Controls/FileManager.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 57c3d19b..9a1f6d85 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -412,7 +412,7 @@ } else { - await logger.LogInformation("File Upload Failed {Files}", uploads); + await logger.LogError("File Upload Failed {Files}", uploads); _message = Localizer["Error.File.Upload"]; _messagetype = MessageType.Error; } From 1a7656d8ee17bdfc40f7a828b1d7444a5781536f Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Thu, 6 Feb 2025 19:21:51 +0100 Subject: [PATCH 104/118] fix #5058: ensure sequential file and chunk uploads to avoid overload --- Oqtane.Server/wwwroot/js/interop.js | 120 ++++++++++++++-------------- 1 file changed, 59 insertions(+), 61 deletions(-) diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index b7e60d5c..719eb63e 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -320,7 +320,7 @@ Oqtane.Interop = { } let uploadSize = 0; - if (!chunksize) { + if (!chunksize || chunksize < 1) { chunksize = 1; // 1 MB default } @@ -336,75 +336,73 @@ Oqtane.Interop = { progressbar.value = 0; } - const uploadFiles = Array.from(fileinput.files).map(file => { - const uploadFile = () => { - const chunkSize = chunksize * (1024 * 1024); - const totalParts = Math.ceil(file.size / chunkSize); - let partCount = 0; + const uploadFile = (file) => { + const chunkSize = chunksize * (1024 * 1024); + const totalParts = Math.ceil(file.size / chunkSize); + let partCount = 0; - const uploadPart = () => { - const start = partCount * chunkSize; - const end = Math.min(start + chunkSize, file.size); - const chunk = file.slice(start, end); + const uploadPart = () => { + const start = partCount * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); - return new Promise((resolve, reject) => { - let formdata = new FormData(); - formdata.append('__RequestVerificationToken', antiforgerytoken); - formdata.append('folder', folder); - formdata.append('formfile', chunk, file.name); + return new Promise((resolve, reject) => { + let formdata = new FormData(); + formdata.append('__RequestVerificationToken', antiforgerytoken); + formdata.append('folder', folder); + formdata.append('formfile', chunk, file.name); - var credentials = 'same-origin'; - var headers = new Headers(); - headers.append('PartCount', partCount + 1); - headers.append('TotalParts', totalParts); - if (jwt !== "") { - headers.append('Authorization', 'Bearer ' + jwt); - credentials = 'include'; - } + var credentials = 'same-origin'; + var headers = new Headers(); + headers.append('PartCount', partCount + 1); + headers.append('TotalParts', totalParts); + if (jwt !== "") { + headers.append('Authorization', 'Bearer ' + jwt); + credentials = 'include'; + } - return fetch(posturl, { - method: 'POST', - headers: headers, - credentials: credentials, - body: formdata + return fetch(posturl, { + method: 'POST', + headers: headers, + credentials: credentials, + body: formdata + }) + .then(response => { + if (!response.ok) { + if (progressinfo !== null) { + progressinfo.innerHTML = 'Error: ' + response.statusText; + } + throw new Error('Failed'); + } + return; }) - .then(response => { - if (!response.ok) { - if (progressinfo !== null) { - progressinfo.innerHTML = 'Error: ' + response.statusText; - } - throw new Error('Failed'); - } - return; - }) - .then(data => { - partCount++; - if (progressbar !== null) { - uploadSize += chunk.size; - var percent = Math.ceil((uploadSize / totalSize) * 100); - progressbar.value = (percent / 100); - } - if (partCount < totalParts) { - uploadPart().then(resolve).catch(reject); - } - else { - resolve(data); - } - }) - .catch(error => { - reject(error); - }); - }); - }; - - return uploadPart(); + .then(data => { + partCount++; + if (progressbar !== null) { + uploadSize += chunk.size; + var percent = Math.ceil((uploadSize / totalSize) * 100); + progressbar.value = (percent / 100); + } + if (partCount < totalParts) { + uploadPart().then(resolve).catch(reject); + } + else { + resolve(data); + } + }) + .catch(error => { + reject(error); + }); + }); }; - return uploadFile(); - }); + return uploadPart(); + }; try { - await Promise.all(uploadFiles); + for (const file of fileinput.files) { + await uploadFile(file); + } } catch (error) { success = false; } From 0fbae8d7dafe2d431bfa101e3655252618be7a53 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Feb 2025 14:16:53 -0500 Subject: [PATCH 105/118] fix #5067 - add support for Guid data types --- .../EntityBuilders/BaseEntityBuilder.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs index cfb58b72..7202921e 100644 --- a/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs +++ b/Oqtane.Server/Migrations/EntityBuilders/BaseEntityBuilder.cs @@ -67,6 +67,7 @@ namespace Oqtane.Migrations.EntityBuilders return ActiveDatabase.AddAutoIncrementColumn(table, RewriteName(name)); } + // boolean public void AddBooleanColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -87,6 +88,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // datetime public void AddDateTimeColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -107,6 +109,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // datetimeoffset public void AddDateTimeOffsetColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -127,6 +130,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // dateonly public void AddDateOnlyColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -147,6 +151,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // timeonly public void AddTimeOnlyColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -167,6 +172,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // btye public void AddByteColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -187,6 +193,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + // integer public void AddIntegerColumn(string name, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); @@ -207,6 +214,8 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); } + + // maxstring public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, schema: Schema); @@ -227,6 +236,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue); } + // string public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, schema: Schema); @@ -247,6 +257,7 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue); } + // decimal public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) { _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, schema: Schema); @@ -267,6 +278,28 @@ namespace Oqtane.Migrations.EntityBuilders return table.Column(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue); } + // guid + public void AddGuidColumn(string name, bool nullable = false) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); + } + + public void AddGuidColumn(string name, bool nullable, Guid defaultValue) + { + _migrationBuilder.AddColumn(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema); + } + + protected OperationBuilder AddGuidColumn(ColumnsBuilder table, string name, bool nullable = false) + { + return table.Column(name: RewriteName(name), nullable: nullable); + } + + protected OperationBuilder AddGuidColumn(ColumnsBuilder table, string name, bool nullable, Guid defaultValue) + { + return table.Column(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); + } + + // alter string public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true, string index = "") { if (index != "") @@ -283,6 +316,7 @@ namespace Oqtane.Migrations.EntityBuilders ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode, index); } + // drop column public void DropColumn(string name) { ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName)); From 05a767c7be8a7532020f005e6cc84ee36887487f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 6 Feb 2025 15:10:14 -0500 Subject: [PATCH 106/118] moved file setting to File Management and added Max Chunk Size --- Oqtane.Client/Modules/Admin/Files/Index.razor | 152 +++++++++++++----- Oqtane.Client/Modules/Admin/Site/Index.razor | 17 -- .../Modules/Controls/FileManager.razor | 11 +- .../Resources/Modules/Admin/Files/Index.resx | 27 ++++ .../Resources/Modules/Admin/Site/Index.resx | 12 -- 5 files changed, 146 insertions(+), 73 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Files/Index.razor b/Oqtane.Client/Modules/Admin/Files/Index.razor index 504a888f..06f7e61c 100644 --- a/Oqtane.Client/Modules/Admin/Files/Index.razor +++ b/Oqtane.Client/Modules/Admin/Files/Index.razor @@ -3,54 +3,92 @@ @inject NavigationManager NavigationManager @inject IFolderService FolderService @inject IFileService FileService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -@if (_files != null) +@if (_files == null) { -
-
- -
-
-
- @Localizer["Folder"]: - -   +

+ @SharedLocalizer["Loading"] +

+} +else +{ + + +
+
+ +
+
+
+ @Localizer["Folder"]: + +   +
+
+
+ +
-
-
- -
-
- -
-   -   - @SharedLocalizer["Name"] - @Localizer["Modified"] - @Localizer["Type"] - @Localizer["Size"] -
- - - - @context.Name - @context.ModifiedOn - @context.Extension.ToUpper() @SharedLocalizer["File"] - @string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB - -
- @if (_files.Count == 0) - { -
@Localizer["NoFiles"]
- } + @if (_files.Count != 0) + { + +
+   +   + @SharedLocalizer["Name"] + @Localizer["Modified"] + @Localizer["Type"] + @Localizer["Size"] +
+ + + + @context.Name + @context.ModifiedOn + @context.Extension.ToUpper() @SharedLocalizer["File"] + @string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB + +
+ } + else + { +
@Localizer["NoFiles"]
+ } + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ } @code { @@ -58,6 +96,10 @@ private int _folderId = -1; private List _files; + private string _imageFiles = string.Empty; + private string _uploadableFiles = string.Empty; + private int _maxChunkSize = 1; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; protected override async Task OnParametersSetAsync() @@ -71,6 +113,13 @@ _folderId = _folders[0].FolderId; await GetFiles(); } + + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + _imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); + _imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; + _uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles); + _uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles; + _maxChunkSize = int.Parse(SettingService.GetSetting(settings, "MaxChunkSize", "1")); } catch (Exception ex) { @@ -115,4 +164,23 @@ AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error); } } + + private async Task SaveSiteSettings() + { + try + { + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false); + settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false); + settings = SettingService.SetSetting(settings, "MaxChunkSize", _maxChunkSize.ToString(), false); + await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); + AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message); + AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error); + } + } + } diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 1db3bc96..c23f9732 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -144,18 +144,6 @@
-
- -
- -
-
-
- -
- -
-
@@ -431,7 +419,6 @@ private Dictionary _textEditors = new Dictionary(); private string _textEditor = ""; private string _imageFiles = string.Empty; - private string _uploadableFiles = string.Empty; private string _headcontent = string.Empty; private string _bodycontent = string.Empty; @@ -528,8 +515,6 @@ _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); _imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; - _uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles); - _uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles; // page content _headcontent = site.HeadContent; @@ -734,8 +719,6 @@ // functionality settings = SettingService.SetSetting(settings, "TextEditor", _textEditor); - settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false); - settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 9a1f6d85..009eca34 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -3,8 +3,8 @@ @inherits ModuleControlBase @inject IFolderService FolderService @inject IFileService FileService -@inject ISettingService SettingService @inject IUserService UserService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -380,6 +380,13 @@ } } + var chunksize = ChunkSize; + if (chunksize == 1) + { + // if ChunkSize parameter is not overridden use the site setting + chunksize = int.Parse(SettingService.GetSetting(PageState.Site.Settings, "MaxChunkSize", "1")); + } + if (!ShowProgress) { _uploading = true; @@ -387,7 +394,7 @@ } // upload files - var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, ChunkSize); + var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize); // reset progress indicators if (ShowProgress) diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Index.resx index 949ce2a3..a4d7177e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Index.resx @@ -165,4 +165,31 @@ Upload Files + + Files + + + Image Extensions: + + + Enter a comma separated list of image file extensions + + + Uploadable File Extensions: + + + Enter a comma separated list of uploadable file extensions + + + Max Upload Chunk Size (MB): + + + Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks). + + + Settings Saved Successfully + + + Error Saving Settings + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 670a4cba..0551fb8f 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -402,18 +402,6 @@ Retention (Days): - - Enter a comma separated list of image file extensions - - - Image Extensions: - - - Enter a comma separated list of uploadable file extensions - - - Uploadable File Extensions: - Specifies if the site can be integrated with an external .NET MAUI hybrid application From fc81bae9b76ee098630e41afd204da4d94a0b934 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 6 Feb 2025 15:16:50 -0500 Subject: [PATCH 107/118] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb5e7da8..de03e405 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Oqtane is an open source Content Management System (CMS) and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on modern .NET. -Oqtane allows you to "Build Applications, Not Infrastructure" which means that you can focus your investment on solving your unique business challenges rather than wasting time and effort on building general infrastructure. +Oqtane allows you to "Build Applications, Not Infrastructure" which means that you can focus your efforts on solving your unique business challenges rather than wasting time and effort on building general infrastructure. Oqtane is "Rocket Fuel for Blazor" as it provides powerful capabilities to accelerate your Blazor development experience, providing scalable services and a composable UI which can be hosted on Static Blazor, Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI). From 0741ce2197cbd875794b4c6b6f07b485421dcbd0 Mon Sep 17 00:00:00 2001 From: David Montesinos Date: Fri, 7 Feb 2025 09:11:52 +0100 Subject: [PATCH 108/118] fix #5074: generate cancellation token for file upload --- Oqtane.Client/Modules/Controls/FileManager.razor | 8 +++++++- Oqtane.Client/UI/Interop.cs | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 009eca34..6a2b979d 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -362,6 +362,8 @@ } if (restricted == "") { + CancellationTokenSource tokenSource = new CancellationTokenSource(); + try { // upload the files @@ -394,7 +396,7 @@ } // upload files - var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize); + var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token); // reset progress indicators if (ShowProgress) @@ -449,6 +451,10 @@ _message = Localizer["Error.File.Upload"]; _messagetype = MessageType.Error; _uploading = false; + await tokenSource.CancelAsync(); + } + finally { + tokenSource.Dispose(); } } diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index bda75505..575889b3 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Text.Json; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace Oqtane.UI { @@ -214,12 +215,12 @@ namespace Oqtane.UI return Task.CompletedTask; } - public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize) + public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default) { try { return _jsRuntime.InvokeAsync( - "Oqtane.Interop.uploadFiles", + "Oqtane.Interop.uploadFiles", cancellationToken, posturl, folder, id, antiforgerytoken, jwt, chunksize); } catch From f30f1e5c1f916edaf4734de888b7227a2b49c915 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 7 Feb 2025 07:52:43 -0500 Subject: [PATCH 109/118] synchronize interop script changes with .NET MAUI --- Oqtane.Maui/wwwroot/js/interop.js | 244 ++++++++++++++++-------------- 1 file changed, 131 insertions(+), 113 deletions(-) diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js index 675cebca..719eb63e 100644 --- a/Oqtane.Maui/wwwroot/js/interop.js +++ b/Oqtane.Maui/wwwroot/js/interop.js @@ -120,13 +120,22 @@ Oqtane.Interop = { this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); } }, - includeScript: function (id, src, integrity, crossorigin, type, content, location) { + includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) { var script; if (src !== "") { script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); } else { - script = document.getElementById(id); + if (id !== "") { + script = document.getElementById(id); + } else { + const scripts = document.querySelectorAll("script:not([src])"); + for (let i = 0; i < scripts.length; i++) { + if (scripts[i].textContent.includes(content)) { + script = scripts[i]; + } + } + } } if (script !== null) { script.remove(); @@ -152,37 +161,36 @@ Oqtane.Interop = { else { script.innerHTML = content; } - script.async = false; - this.addScript(script, location) - .then(() => { - if (src !== "") { - console.log(src + ' loaded'); - } - else { - console.log(id + ' loaded'); - } - }) - .catch(() => { - if (src !== "") { - console.error(src + ' failed'); - } - else { - console.error(id + ' failed'); - } - }); + if (dataAttributes !== null) { + for (var key in dataAttributes) { + script.setAttribute(key, dataAttributes[key]); + } + } + + try { + this.addScript(script, location); + } catch (error) { + if (src !== "") { + console.error("Failed to load external script: ${src}", error); + } else { + console.error("Failed to load inline script: ${content}", error); + } + } } }, addScript: function (script, location) { - if (location === 'head') { - document.head.appendChild(script); - } - if (location === 'body') { - document.body.appendChild(script); - } + return new Promise((resolve, reject) => { + script.async = false; + script.defer = false; - return new Promise((res, rej) => { - script.onload = res(); - script.onerror = rej(); + script.onload = () => resolve(); + script.onerror = (error) => reject(error); + + if (location === 'head') { + document.head.appendChild(script); + } else { + document.body.appendChild(script); + } }); }, includeScripts: async function (scripts) { @@ -222,10 +230,10 @@ Oqtane.Interop = { if (scripts[s].crossorigin !== '') { element.crossOrigin = scripts[s].crossorigin; } - if (scripts[s].es6module === true) { - element.type = "module"; + if (scripts[s].type !== '') { + element.type = scripts[s].type; } - if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) { + if (scripts[s].dataAttributes !== null) { for (var key in scripts[s].dataAttributes) { element.setAttribute(key, scripts[s].dataAttributes[key]); } @@ -300,97 +308,107 @@ Oqtane.Interop = { } return files; }, - uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { + uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { + var success = true; var fileinput = document.getElementById('FileInput_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id); var progressbar = document.getElementById('ProgressBar_' + id); + var totalSize = 0; + for (var i = 0; i < fileinput.files.length; i++) { + totalSize += fileinput.files[i].size; + } + let uploadSize = 0; + + if (!chunksize || chunksize < 1) { + chunksize = 1; // 1 MB default + } + if (progressinfo !== null && progressbar !== null) { - progressinfo.setAttribute("style", "display: inline;"); - progressinfo.innerHTML = ''; - progressbar.setAttribute("style", "width: 100%; display: inline;"); + progressinfo.setAttribute('style', 'display: inline;'); + if (fileinput.files.length > 1) { + progressinfo.innerHTML = fileinput.files[0].name + ', ...'; + } + else { + progressinfo.innerHTML = fileinput.files[0].name; + } + progressbar.setAttribute('style', 'width: 100%; display: inline;'); progressbar.value = 0; } - var files = fileinput.files; - var totalSize = 0; - for (var i = 0; i < files.length; i++) { - totalSize = totalSize + files[i].size; + const uploadFile = (file) => { + const chunkSize = chunksize * (1024 * 1024); + const totalParts = Math.ceil(file.size / chunkSize); + let partCount = 0; + + const uploadPart = () => { + const start = partCount * chunkSize; + const end = Math.min(start + chunkSize, file.size); + const chunk = file.slice(start, end); + + return new Promise((resolve, reject) => { + let formdata = new FormData(); + formdata.append('__RequestVerificationToken', antiforgerytoken); + formdata.append('folder', folder); + formdata.append('formfile', chunk, file.name); + + var credentials = 'same-origin'; + var headers = new Headers(); + headers.append('PartCount', partCount + 1); + headers.append('TotalParts', totalParts); + if (jwt !== "") { + headers.append('Authorization', 'Bearer ' + jwt); + credentials = 'include'; + } + + return fetch(posturl, { + method: 'POST', + headers: headers, + credentials: credentials, + body: formdata + }) + .then(response => { + if (!response.ok) { + if (progressinfo !== null) { + progressinfo.innerHTML = 'Error: ' + response.statusText; + } + throw new Error('Failed'); + } + return; + }) + .then(data => { + partCount++; + if (progressbar !== null) { + uploadSize += chunk.size; + var percent = Math.ceil((uploadSize / totalSize) * 100); + progressbar.value = (percent / 100); + } + if (partCount < totalParts) { + uploadPart().then(resolve).catch(reject); + } + else { + resolve(data); + } + }) + .catch(error => { + reject(error); + }); + }); + }; + + return uploadPart(); + }; + + try { + for (const file of fileinput.files) { + await uploadFile(file); + } + } catch (error) { + success = false; } - var maxChunkSizeMB = 1; - var bufferChunkSize = maxChunkSizeMB * (1024 * 1024); - var uploadedSize = 0; - - for (var i = 0; i < files.length; i++) { - var fileChunk = []; - var file = files[i]; - var fileStreamPos = 0; - var endPos = bufferChunkSize; - - while (fileStreamPos < file.size) { - fileChunk.push(file.slice(fileStreamPos, endPos)); - fileStreamPos = endPos; - endPos = fileStreamPos + bufferChunkSize; - } - - var totalParts = fileChunk.length; - var partCount = 0; - - while (chunk = fileChunk.shift()) { - partCount++; - var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0'); - - var data = new FormData(); - data.append('__RequestVerificationToken', antiforgerytoken); - data.append('folder', folder); - data.append('formfile', chunk, fileName); - var request = new XMLHttpRequest(); - request.open('POST', posturl, true); - if (jwt !== "") { - request.setRequestHeader('Authorization', 'Bearer ' + jwt); - request.withCredentials = true; - } - request.upload.onloadstart = function (e) { - if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') { - if (files.length === 1) { - progressinfo.innerHTML = file.name; - } - else { - progressinfo.innerHTML = file.name + ", ..."; - } - } - }; - request.upload.onprogress = function (e) { - if (progressinfo !== null && progressbar !== null) { - var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100); - progressbar.value = (percent / 100); - } - }; - request.upload.onloadend = function (e) { - if (progressinfo !== null && progressbar !== null) { - uploadedSize = uploadedSize + e.total; - var percent = Math.ceil((uploadedSize / totalSize) * 100); - progressbar.value = (percent / 100); - } - }; - request.upload.onerror = function() { - if (progressinfo !== null && progressbar !== null) { - if (files.length === 1) { - progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; - } - else { - progressinfo.innerHTML = ' Error: ' + request.statusText; - } - } - }; - request.send(data); - } - - if (i === files.length - 1) { - fileinput.value = ''; - } - } + fileinput.value = ''; + return success; }, refreshBrowser: function (verify, wait) { async function attemptReload (verify) { From c80910f355849e9299cda5266479bd6d3c8c683b Mon Sep 17 00:00:00 2001 From: Ben Date: Sat, 8 Feb 2025 11:51:38 +0800 Subject: [PATCH 110/118] Fix #5079: remove the records limit. --- Oqtane.Server/Repository/UrlMappingRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs index 71c80e7c..63332dc6 100644 --- a/Oqtane.Server/Repository/UrlMappingRepository.cs +++ b/Oqtane.Server/Repository/UrlMappingRepository.cs @@ -22,11 +22,11 @@ namespace Oqtane.Repository using var db = _dbContextFactory.CreateDbContext(); if (isMapped) { - return db.UrlMapping.Where(item => item.SiteId == siteId && !string.IsNullOrEmpty(item.MappedUrl)).Take(200).ToList(); + return db.UrlMapping.Where(item => item.SiteId == siteId && !string.IsNullOrEmpty(item.MappedUrl)).ToList(); } else { - return db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl)).Take(200).ToList(); + return db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl)).ToList(); } } From 4f4258d5328035997d0bf42c3c6e4d90f901e8e2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 9 Feb 2025 12:34:17 -0500 Subject: [PATCH 111/118] fix #5072 - administrators should be allowed to send system notifications --- Oqtane.Server/Controllers/NotificationController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs index 8e439fd2..8eb06c88 100644 --- a/Oqtane.Server/Controllers/NotificationController.cs +++ b/Oqtane.Server/Controllers/NotificationController.cs @@ -223,7 +223,7 @@ namespace Oqtane.Controllers private bool IsAuthorized(int? userid) { - bool authorized = false; + bool authorized = User.IsInRole(RoleNames.Admin); if (userid != null) { authorized = (_userPermissions.GetUser(User).UserId == userid); From ed353461dad78bab6b0e004d27bdeaabc47d1b0d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sun, 9 Feb 2025 13:02:07 -0500 Subject: [PATCH 112/118] enhance purge job to trim broken urls based on retention policy --- .../Modules/Admin/UrlMappings/Index.razor | 22 ++++++++++++++++--- .../Modules/Admin/UrlMappings/Index.resx | 6 +++++ Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 17 ++++++++++++++ .../Interfaces/IUrlMappingRepository.cs | 1 + .../Repository/UrlMappingRepository.cs | 19 ++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor index bae495fb..75a4c845 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor @@ -3,6 +3,7 @@ @inject NavigationManager NavigationManager @inject IUrlMappingService UrlMappingService @inject ISiteService SiteService +@inject ISettingService SettingService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -62,7 +63,13 @@ else - +
+ +
+ +
+
+
@@ -73,6 +80,7 @@ else private bool _mapped = true; private List _urlMappings; private string _capturebrokenurls; + private int _retention = 30; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -80,7 +88,10 @@ else { await GetUrlMappings(); _capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString(); - } + + var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + _retention = int.Parse(SettingService.GetSetting(settings, "UrlMappingRetention", "30")); + } private async void MappedChanged(ChangeEventArgs e) { @@ -124,7 +135,12 @@ else var site = PageState.Site; site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls); await SiteService.UpdateSiteAsync(site); - AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); + + var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); + settings = SettingService.SetSetting(settings, "UrlMappingRetention", _retention.ToString(), true); + await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); + + AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); } catch (Exception ex) { diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx index 188c09e3..49b517db 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx @@ -162,4 +162,10 @@ Edit + + Retention (Days): + + + Number of days of broken urls to retain + \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 28a13387..07504f1e 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure var logRepository = provider.GetRequiredService(); var visitorRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); + var urlMappingRepository = provider.GetRequiredService(); var installationManager = provider.GetRequiredService(); // iterate through sites for current tenant @@ -95,6 +96,22 @@ namespace Oqtane.Infrastructure { log += $"Error Purging Notifications - {ex.Message}
"; } + + // purge broken urls + retention = 30; // 30 days + if (settings.ContainsKey("UrlMappingRetention") && !string.IsNullOrEmpty(settings["UrlMappingRetention"])) + { + retention = int.Parse(settings["UrlMappingRetention"]); + } + try + { + count = urlMappingRepository.DeleteUrlMappings(site.SiteId, retention); + log += count.ToString() + " Broken Urls Purged
"; + } + catch (Exception ex) + { + log += $"Error Purging Broken Urls - {ex.Message}
"; + } } // register assemblies diff --git a/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs b/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs index f954d87b..ca056484 100644 --- a/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IUrlMappingRepository.cs @@ -13,5 +13,6 @@ namespace Oqtane.Repository UrlMapping GetUrlMapping(int urlMappingId, bool tracking); UrlMapping GetUrlMapping(int siteId, string url); void DeleteUrlMapping(int urlMappingId); + int DeleteUrlMappings(int siteId, int age); } } diff --git a/Oqtane.Server/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs index 71c80e7c..a4086b32 100644 --- a/Oqtane.Server/Repository/UrlMappingRepository.cs +++ b/Oqtane.Server/Repository/UrlMappingRepository.cs @@ -101,5 +101,24 @@ namespace Oqtane.Repository db.UrlMapping.Remove(urlMapping); db.SaveChanges(); } + + public int DeleteUrlMappings(int siteId, int age) + { + using var db = _dbContextFactory.CreateDbContext(); + // delete notifications in batches of 100 records + var count = 0; + var purgedate = DateTime.UtcNow.AddDays(-age); + var urlMappings = db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl) && item.RequestedOn < purgedate) + .OrderBy(item => item.RequestedOn).Take(100).ToList(); + while (urlMappings.Count > 0) + { + count += urlMappings.Count; + db.UrlMapping.RemoveRange(urlMappings); + db.SaveChanges(); + urlMappings = db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl) && item.RequestedOn < purgedate) + .OrderBy(item => item.RequestedOn).Take(100).ToList(); + } + return count; + } } } From d4a4d7c34698afe73f81438c5988d72214e55945 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Feb 2025 08:20:58 -0500 Subject: [PATCH 113/118] fix comment --- Oqtane.Server/Repository/UrlMappingRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs index a4086b32..1351d72e 100644 --- a/Oqtane.Server/Repository/UrlMappingRepository.cs +++ b/Oqtane.Server/Repository/UrlMappingRepository.cs @@ -105,7 +105,7 @@ namespace Oqtane.Repository public int DeleteUrlMappings(int siteId, int age) { using var db = _dbContextFactory.CreateDbContext(); - // delete notifications in batches of 100 records + // delete in batches of 100 records var count = 0; var purgedate = DateTime.UtcNow.AddDays(-age); var urlMappings = db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl) && item.RequestedOn < purgedate) From 324e9852479343db6c8550efca14e4fbd8628eb5 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Feb 2025 09:50:21 -0500 Subject: [PATCH 114/118] improve notification add and update methods --- .../Controllers/NotificationController.cs | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs index 8eb06c88..9a68cd68 100644 --- a/Oqtane.Server/Controllers/NotificationController.cs +++ b/Oqtane.Server/Controllers/NotificationController.cs @@ -155,7 +155,7 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Registered)] public Notification Post([FromBody] Notification notification) { - if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId)) + if (ModelState.IsValid && notification.SiteId == _alias.SiteId && (IsAuthorized(notification.FromUserId) || (notification.FromUserId == null && User.IsInRole(RoleNames.Admin)))) { if (!User.IsInRole(RoleNames.Admin)) { @@ -181,17 +181,45 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Registered)] public Notification Put(int id, [FromBody] Notification notification) { - if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId))) + if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null) { - if (!User.IsInRole(RoleNames.Admin) && notification.FromUserId != null) + bool update = false; + if (IsAuthorized(notification.FromUserId)) { - // content must be HTML encoded for non-admins to prevent HTML injection - notification.Subject = WebUtility.HtmlEncode(notification.Subject); - notification.Body = WebUtility.HtmlEncode(notification.Body); + // notification belongs to current authenticated user - update is allowed + if (!User.IsInRole(RoleNames.Admin)) + { + // content must be HTML encoded for non-admins to prevent HTML injection + notification.Subject = WebUtility.HtmlEncode(notification.Subject); + notification.Body = WebUtility.HtmlEncode(notification.Body); + } + update = true; + } + else + { + if (IsAuthorized(notification.ToUserId)) + { + // notification was sent to current authenticated user - only isread and isdeleted properties can be updated + var isread = notification.IsRead; + var isdeleted = notification.IsDeleted; + notification = _notifications.GetNotification(notification.NotificationId); + notification.IsRead = isread; + notification.IsDeleted = isdeleted; + update = true; + } + } + if (update) + { + notification = _notifications.UpdateNotification(notification); + _syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Put Attempt {Notification}", notification); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + notification = null; } - notification = _notifications.UpdateNotification(notification); - _syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update); - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId); } else { @@ -223,7 +251,7 @@ namespace Oqtane.Controllers private bool IsAuthorized(int? userid) { - bool authorized = User.IsInRole(RoleNames.Admin); + bool authorized = false; if (userid != null) { authorized = (_userPermissions.GetUser(User).UserId == userid); From 1a738b358eb70f52172c8c0b8c693d3944431d54 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Feb 2025 10:46:44 -0500 Subject: [PATCH 115/118] update Provider property to Pomelo --- Oqtane.Database.MySQL/MySQLDatabase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Database.MySQL/MySQLDatabase.cs b/Oqtane.Database.MySQL/MySQLDatabase.cs index 32759f21..a3b8d307 100644 --- a/Oqtane.Database.MySQL/MySQLDatabase.cs +++ b/Oqtane.Database.MySQL/MySQLDatabase.cs @@ -21,7 +21,7 @@ namespace Oqtane.Database.MySQL public MySQLDatabase() :base(_name, _friendlyName) { } - public override string Provider => "MySql.EntityFrameworkCore"; + public override string Provider => "Pomelo.EntityFrameworkCore.MySql"; public override OperationBuilder AddAutoIncrementColumn(ColumnsBuilder table, string name) { From e3118c6e996a73ac5292b15e59096018493f4b2f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Feb 2025 16:27:05 -0500 Subject: [PATCH 116/118] modify RemoveAssemblies method so that it only runs once - not for every tenant --- Oqtane.Server/Infrastructure/UpgradeManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 7d386eb6..8116cfce 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -482,7 +482,7 @@ namespace Oqtane.Infrastructure "System.Text.Json.dll" }; - RemoveAssemblies(assemblies, "6.0.1"); + RemoveAssemblies(tenant, assemblies, "6.0.1"); } private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope) @@ -492,7 +492,7 @@ namespace Oqtane.Infrastructure "MySql.EntityFrameworkCore.dll" }; - RemoveAssemblies(assemblies, "6.1.0"); + RemoveAssemblies(tenant, assemblies, "6.1.0"); } private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) @@ -506,10 +506,10 @@ namespace Oqtane.Infrastructure } } - private void RemoveAssemblies(string[] assemblies, string version) + private void RemoveAssemblies(Tenant tenant, string[] assemblies, string version) { // in a development environment assemblies cannot be removed as the debugger runs fron /bin folder and locks the files - if (!_environment.IsDevelopment()) + if (tenant.Name == TenantNames.Master && !_environment.IsDevelopment()) { foreach (var assembly in assemblies) { From a1ac81e907b5f0cca0745cd969dd1a2c0c8e83c9 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 10 Feb 2025 16:53:22 -0500 Subject: [PATCH 117/118] fix #5089 - remove upgrade cleanup logic as .NET 9.0.1 moves assemblies back to /bin folder --- .../Infrastructure/UpgradeManager.cs | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 8116cfce..210961c4 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -72,9 +72,6 @@ namespace Oqtane.Infrastructure case "5.2.1": Upgrade_5_2_1(tenant, scope); break; - case "6.0.1": - Upgrade_6_0_1(tenant, scope); - break; case "6.1.0": Upgrade_6_1_0(tenant, scope); break; @@ -450,41 +447,6 @@ namespace Oqtane.Infrastructure AddPagesToSites(scope, tenant, pageTemplates); } - private void Upgrade_6_0_1(Tenant tenant, IServiceScope scope) - { - // assemblies which have been relocated to the bin/refs folder in .NET 9 - string[] assemblies = { - "Microsoft.AspNetCore.Authorization.dll", - "Microsoft.AspNetCore.Components.Authorization.dll", - "Microsoft.AspNetCore.Components.dll", - "Microsoft.AspNetCore.Components.Forms.dll", - "Microsoft.AspNetCore.Components.Web.dll", - "Microsoft.AspNetCore.Cryptography.Internal.dll", - "Microsoft.AspNetCore.Cryptography.KeyDerivation.dll", - "Microsoft.AspNetCore.Metadata.dll", - "Microsoft.Extensions.Caching.Memory.dll", - "Microsoft.Extensions.Configuration.Binder.dll", - "Microsoft.Extensions.Configuration.FileExtensions.dll", - "Microsoft.Extensions.Configuration.Json.dll", - "Microsoft.Extensions.DependencyInjection.Abstractions.dll", - "Microsoft.Extensions.DependencyInjection.dll", - "Microsoft.Extensions.Diagnostics.Abstractions.dll", - "Microsoft.Extensions.Diagnostics.dll", - "Microsoft.Extensions.Http.dll", - "Microsoft.Extensions.Identity.Core.dll", - "Microsoft.Extensions.Identity.Stores.dll", - "Microsoft.Extensions.Localization.Abstractions.dll", - "Microsoft.Extensions.Localization.dll", - "Microsoft.Extensions.Logging.Abstractions.dll", - "Microsoft.Extensions.Logging.dll", - "Microsoft.Extensions.Options.dll", - "Microsoft.JSInterop.dll", - "System.Text.Json.dll" - }; - - RemoveAssemblies(tenant, assemblies, "6.0.1"); - } - private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope) { // remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql) From 1214a11704da3a4e0c6bf8f82d90aad5e7b86e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ton=C4=87i=20Vatavuk?= Date: Tue, 11 Feb 2025 14:35:13 +0100 Subject: [PATCH 118/118] Minor fix in ThemeController.cs update `SharedReference` to use "Oqtane.Shared" for PackageReference code --- Oqtane.Server/Controllers/ThemeController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index f42fc01b..c4bafe6a 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -280,7 +280,7 @@ namespace Oqtane.Controllers { { "FrameworkVersion", theme.Version }, { "ClientReference", $"" }, - { "SharedReference", $"" }, + { "SharedReference", $"" }, }; }); }