From a49b8728fd3cc6ab1071a763816216f248d6d563 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 08:56:21 -0400 Subject: [PATCH 01/21] improve module export so that content can be saved to a file --- .../Modules/Admin/Modules/Export.razor | 71 ++++++++++++++++--- .../Modules/Admin/Modules/Export.resx | 20 +++++- .../Services/Interfaces/IModuleService.cs | 12 +++- Oqtane.Client/Services/ModuleService.cs | 7 +- Oqtane.Server/Controllers/ModuleController.cs | 66 ++++++++++++++++- Oqtane.Shared/Models/Result.cs | 2 + 6 files changed, 164 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index d2e90193..ab714221 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -5,24 +5,45 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -
-
- -
- + + +
+
+ +
+ +
+
-
-
+
+ + @SharedLocalizer["Cancel"] + + +
+
+ +
+ +
+
+
+
+ + @SharedLocalizer["Cancel"] +
+ + - -@SharedLocalizer["Cancel"] @code { private string _content = string.Empty; + private FileManager _filemanager; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; - private async Task ExportModule() + private async Task ExportText() { try { @@ -35,4 +56,34 @@ AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); } } + + private async Task ExportFile() + { + try + { + var folderid = _filemanager.GetFolderId(); + if (folderid != -1) + { + var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid); + if (result.Success) + { + AddModuleMessage(string.Format(Localizer["Success.Export.File"], result.Message), MessageType.Success); + } + else + { + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + } diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 3ed705f8..90e614c2 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -121,7 +121,7 @@ Export - The Exported Module Content + Select the Export option and you will be able to view the module content Content: @@ -135,4 +135,22 @@ Export Content + + Text + + + File + + + Folder: + + + Select a folder where you wish to save the exported content + + + Please Select A Folder Before Choosing Export + + + Content Was Successfully Exported To Specified Folder With Filename {0} + \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index 46777fdf..34a9c1b1 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -56,7 +56,17 @@ namespace Oqtane.Services /// Exports a given module /// /// - /// module in JSON + /// + /// module content in JSON format Task ExportModuleAsync(int moduleId, int pageId); + + /// + /// Exports a given module + /// + /// + /// + /// + /// success/failure + Task ExportModuleAsync(int moduleId, int pageId, int folderId); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index e1914004..1e688610 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -47,8 +47,13 @@ namespace Oqtane.Services } public async Task ExportModuleAsync(int moduleId, int pageId) -{ + { return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } + + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId) + { + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}", null); + } } } diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 02f6f8c6..78aae764 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -9,6 +9,10 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using System.Net; +using System.IO; +using System; +using static System.Net.WebRequestMethods; +using System.Net.Http; namespace Oqtane.Controllers { @@ -20,18 +24,22 @@ namespace Oqtane.Controllers private readonly IPageRepository _pages; private readonly IModuleDefinitionRepository _moduleDefinitions; private readonly ISettingRepository _settings; + private readonly IFolderRepository _folders; + private readonly IFileRepository _files; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IFolderRepository folders, IFileRepository files, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) { _modules = modules; _pageModules = pageModules; _pages = pages; _moduleDefinitions = moduleDefinitions; _settings = settings; + _folders = folders; + _files = files; _userPermissions = userPermissions; _syncManager = syncManager; _logger = logger; @@ -248,6 +256,62 @@ namespace Oqtane.Controllers return content; } + // POST api//export?moduleid=x&pageid=y&folderid=z + [HttpPost("export")] + [Authorize(Roles = RoleNames.Registered)] + public Result Export(int moduleid, int pageid, int folderid) + { + var result = new Result(false); + var module = _modules.GetModule(moduleid); + if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && + _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit)) + { + // get content + var content = _modules.ExportModule(moduleid); + + // get folder + var folder = _folders.GetFolder(folderid, false); + string folderPath = _folders.GetFolderPath(folder); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + // create text file + var filename = Utilities.GetTypeNameLastSegment(module.ModuleDefinitionName, 0) + moduleid.ToString() + ".json"; + string filepath = Path.Combine(folderPath, filename); + if (System.IO.File.Exists(filepath)) + { + System.IO.File.Delete(filepath); + } + System.IO.File.WriteAllText(filepath, content); + + // register file + var file = _files.GetFile(folderid, filename); + if (file == null) + { + file = new Models.File { FolderId = folderid, Name = filename, Extension = "txt", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; + _files.AddFile(file); + } + else + { + file.Size = (int)new FileInfo(filepath).Length; + _files.UpdateFile(file); + } + + result.Success = true; + result.Message = filename; + + _logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + return result; + } + // POST api//import?moduleid=x&pageid=y [HttpPost("import")] [Authorize(Roles = RoleNames.Registered)] diff --git a/Oqtane.Shared/Models/Result.cs b/Oqtane.Shared/Models/Result.cs index d550f2c4..8500d3ad 100644 --- a/Oqtane.Shared/Models/Result.cs +++ b/Oqtane.Shared/Models/Result.cs @@ -6,6 +6,8 @@ namespace Oqtane.Models public string Message { get; set; } + public Result() {} + public Result(bool success) { Success = success; From 51ba3a01f5ce4eeae207c3d90e1707988a2c678e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 09:34:19 -0400 Subject: [PATCH 02/21] allow module import from a file --- .../Modules/Admin/Modules/Import.razor | 19 ++++++++++++++++--- Oqtane.Server/Controllers/ModuleController.cs | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Import.razor b/Oqtane.Client/Modules/Admin/Modules/Import.razor index 04b2557a..eacec260 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Import.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Import.razor @@ -2,20 +2,27 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IModuleService ModuleService +@inject IFileService FileService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer
- + +
+ +
+
+
+
+
-
- +
@SharedLocalizer["Cancel"]
@@ -28,6 +35,12 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Import Content"; + private async Task OnSelectFile(int fileId) + { + var bytes = await FileService.DownloadFileAsync(fileId); + _content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + private async Task ImportModule() { validated = true; diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 78aae764..e075e5e5 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -290,7 +290,7 @@ namespace Oqtane.Controllers var file = _files.GetFile(folderid, filename); if (file == null) { - file = new Models.File { FolderId = folderid, Name = filename, Extension = "txt", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; + file = new Models.File { FolderId = folderid, Name = filename, Extension = "json", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; _files.AddFile(file); } else From 5d077e843d2d8a0508509b8ac1fb6eaf30694ee6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 10:58:55 -0400 Subject: [PATCH 03/21] allow filename to be provided during module export --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 13 ++++++++++--- .../Resources/Modules/Admin/Modules/Export.resx | 9 ++++++--- Oqtane.Client/Services/Interfaces/IModuleService.cs | 3 ++- Oqtane.Client/Services/ModuleService.cs | 4 ++-- Oqtane.Server/Controllers/ModuleController.cs | 10 +++++----- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index ab714221..f7fd3a57 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -27,6 +27,12 @@
+
+ +
+ +
+

@@ -39,6 +45,7 @@ @code { private string _content = string.Empty; private FileManager _filemanager; + private string _filename = string.Empty; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; @@ -62,12 +69,12 @@ try { var folderid = _filemanager.GetFolderId(); - if (folderid != -1) + if (folderid != -1 && !string.IsNullOrEmpty(_filename)) { - var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid); + var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); if (result.Success) { - AddModuleMessage(string.Format(Localizer["Success.Export.File"], result.Message), MessageType.Success); + AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); } else { diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 90e614c2..0a365dfd 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -148,9 +148,12 @@ Select a folder where you wish to save the exported content - Please Select A Folder Before Choosing Export + Please Select A Folder And Provide A Filename Before Choosing Export - - Content Was Successfully Exported To Specified Folder With Filename {0} + + Filename: + + + Specify a name for the file (without an extension) \ No newline at end of file diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index 34a9c1b1..a2334a20 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -66,7 +66,8 @@ namespace Oqtane.Services /// /// /// + /// /// success/failure - Task ExportModuleAsync(int moduleId, int pageId, int folderId); + Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index 1e688610..68d1e4fd 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -51,9 +51,9 @@ namespace Oqtane.Services return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } - public async Task ExportModuleAsync(int moduleId, int pageId, int folderId) + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) { - return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}", null); + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); } } } diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index e075e5e5..012f2e11 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -256,15 +256,15 @@ namespace Oqtane.Controllers return content; } - // POST api//export?moduleid=x&pageid=y&folderid=z + // POST api//export?moduleid=x&pageid=y&folderid=z&filename=a [HttpPost("export")] [Authorize(Roles = RoleNames.Registered)] - public Result Export(int moduleid, int pageid, int folderid) + public Result Export(int moduleid, int pageid, int folderid, string filename) { var result = new Result(false); var module = _modules.GetModule(moduleid); if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && - _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit)) + _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) { // get content var content = _modules.ExportModule(moduleid); @@ -277,8 +277,8 @@ namespace Oqtane.Controllers Directory.CreateDirectory(folderPath); } - // create text file - var filename = Utilities.GetTypeNameLastSegment(module.ModuleDefinitionName, 0) + moduleid.ToString() + ".json"; + // create json file + filename = Path.GetFileNameWithoutExtension(filename) + ".json"; string filepath = Path.Combine(folderPath, filename); if (System.IO.File.Exists(filepath)) { From c57c6abb1b66acbaafe3ef48c2c83ab21df11f5e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 15 May 2025 11:06:04 -0400 Subject: [PATCH 04/21] update module export resource info --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 2 +- Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index f7fd3a57..d72ec689 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -6,7 +6,7 @@ @inject IStringLocalizer SharedLocalizer - +
diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 0a365dfd..23378774 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -135,8 +135,8 @@ Export Content - - Text + + Content File From eb5a0dc1c9f121e83ab78bd80fbf03696892e8a1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 08:25:50 -0400 Subject: [PATCH 05/21] improve filename validation in module content export --- Oqtane.Client/Modules/Admin/Modules/Export.razor | 9 +++++++-- .../Services/Interfaces/IModuleService.cs | 4 ++-- Oqtane.Client/Services/ModuleService.cs | 4 ++-- Oqtane.Server/Controllers/FileController.cs | 1 - Oqtane.Server/Controllers/ModuleController.cs | 16 ++++++---------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index d72ec689..10831a56 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -50,6 +50,11 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; + protected override void OnInitialized() + { + _filename = Utilities.GetFriendlyUrl(ModuleState.Title); + } + private async Task ExportText() { try @@ -71,8 +76,8 @@ var folderid = _filemanager.GetFolderId(); if (folderid != -1 && !string.IsNullOrEmpty(_filename)) { - var result = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); - if (result.Success) + var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); + if (fileid != -1) { AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); } diff --git a/Oqtane.Client/Services/Interfaces/IModuleService.cs b/Oqtane.Client/Services/Interfaces/IModuleService.cs index a2334a20..ea6beab3 100644 --- a/Oqtane.Client/Services/Interfaces/IModuleService.cs +++ b/Oqtane.Client/Services/Interfaces/IModuleService.cs @@ -67,7 +67,7 @@ namespace Oqtane.Services /// /// /// - /// success/failure - Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); + /// file id + Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename); } } diff --git a/Oqtane.Client/Services/ModuleService.cs b/Oqtane.Client/Services/ModuleService.cs index 68d1e4fd..ac093bed 100644 --- a/Oqtane.Client/Services/ModuleService.cs +++ b/Oqtane.Client/Services/ModuleService.cs @@ -51,9 +51,9 @@ namespace Oqtane.Services return await GetStringAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}"); } - public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) + public async Task ExportModuleAsync(int moduleId, int pageId, int folderId, string filename) { - return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); + return await PostJsonAsync($"{Apiurl}/export?moduleid={moduleId}&pageid={pageId}&folderid={folderId}&filename={filename}", null); } } } diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 7ead95d7..f0b72f22 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -22,7 +22,6 @@ using Microsoft.AspNetCore.Cors; using System.IO.Compression; using Oqtane.Services; using Microsoft.Extensions.Primitives; -using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.Net.Http.Headers; // ReSharper disable StringIndexOfIsCultureSpecific.1 diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index 012f2e11..61f69f33 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -10,9 +10,6 @@ using Oqtane.Repository; using Oqtane.Security; using System.Net; using System.IO; -using System; -using static System.Net.WebRequestMethods; -using System.Net.Http; namespace Oqtane.Controllers { @@ -259,9 +256,9 @@ namespace Oqtane.Controllers // POST api//export?moduleid=x&pageid=y&folderid=z&filename=a [HttpPost("export")] [Authorize(Roles = RoleNames.Registered)] - public Result Export(int moduleid, int pageid, int folderid, string filename) + public int Export(int moduleid, int pageid, int folderid, string filename) { - var result = new Result(false); + var fileid = -1; var module = _modules.GetModule(moduleid); if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) @@ -278,7 +275,7 @@ namespace Oqtane.Controllers } // create json file - filename = Path.GetFileNameWithoutExtension(filename) + ".json"; + filename = Utilities.GetFriendlyUrl(Path.GetFileNameWithoutExtension(filename)) + ".json"; string filepath = Path.Combine(folderPath, filename); if (System.IO.File.Exists(filepath)) { @@ -298,9 +295,7 @@ namespace Oqtane.Controllers file.Size = (int)new FileInfo(filepath).Length; _files.UpdateFile(file); } - - result.Success = true; - result.Message = filename; + fileid = file.FileId; _logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid); } @@ -309,7 +304,8 @@ namespace Oqtane.Controllers _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid); HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } - return result; + + return fileid; } // POST api//import?moduleid=x&pageid=y From bbd6f13f369d0f3871c31ace90c5bafc62db42cd Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 09:09:07 -0400 Subject: [PATCH 06/21] fix initialization issue related to time zones --- .../Modules/Admin/Register/Index.razor | 143 +++++++++--------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 13920441..712ba186 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -8,88 +8,92 @@ @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService -@if (PageState.Site.AllowRegistration) +@if (_initialized) { - if (!_userCreated) + @if (PageState.Site.AllowRegistration) { - if (PageState.User != null) + if (!_userCreated) { - - } - else - { - -
-
-
- -
- + if (PageState.User != null) + { + + } + else + { + + +
+
+ +
+ +
-
-
- -
-
- - +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
- - - @if (_allowsitelogin) - {
+ + + @if (_allowsitelogin) + { +
-
- @Localizer["Login"] - } - +
+ @Localizer["Login"] + } + + } } - } -} -else -{ - + } + else + { + + } } @code { + private bool _initialized = false; private List _timezones; private string _passwordrequirements; private string _username = string.Empty; @@ -113,6 +117,7 @@ else _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); _timezones = await TimeZoneService.GetTimeZonesAsync(); _timezoneid = PageState.Site.TimeZoneId; + _initialized = true; } protected override void OnParametersSet() From 1f05d12ef55c06edbfd4000191309ad9cf3575d0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 09:39:57 -0400 Subject: [PATCH 07/21] fix spelling mistake --- Oqtane.Server/Controllers/UserController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 7a0f3bfd..37648c4f 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -131,7 +131,7 @@ namespace Oqtane.Controllers filtered.TwoFactorCode = ""; filtered.SecurityStamp = ""; - // include private properties if authenticated user is accessing their own user account os is an administrator + // include private properties if authenticated user is accessing their own user account or is an administrator if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) { filtered.Email = user.Email; From ff6a810ad589126cceee37fa255af9b4f4b22a58 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:13:03 -0400 Subject: [PATCH 08/21] Fix #4789 - allow user email verification to be managed by administrator --- Oqtane.Client/Modules/Admin/Login/Index.razor | 2 +- Oqtane.Client/Modules/Admin/Users/Edit.razor | 28 +++++++++---- .../Resources/Modules/Admin/Login/Index.resx | 4 +- .../Resources/Modules/Admin/Users/Edit.resx | 6 +++ Oqtane.Server/Controllers/UserController.cs | 10 ++++- Oqtane.Server/Managers/UserManager.cs | 39 ++++++++++++------- 6 files changed, 63 insertions(+), 26 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f10c0203..f0a057d0 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -144,7 +144,7 @@ else user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); if (user != null) { - await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username); AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); } else diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index 549059e4..c6401a47 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -18,13 +18,13 @@
- +
- +
@@ -33,7 +33,7 @@
- +
@@ -42,13 +42,22 @@
- +
- + +
+ +
+
+
+
@@ -68,7 +77,7 @@ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
- +
- +
@@ -167,6 +176,7 @@ private string _togglepassword = string.Empty; private string _confirm = string.Empty; private string _email = string.Empty; + private string _confirmed = string.Empty; private string _displayname = string.Empty; private string _timezoneid = string.Empty; private string _isdeleted; @@ -204,6 +214,7 @@ { _username = user.Username; _email = user.Email; + _confirmed = user.EmailConfirmed.ToString(); _displayname = user.DisplayName; _timezoneid = PageState.User.TimeZoneId; _isdeleted = user.IsDeleted.ToString(); @@ -255,6 +266,7 @@ user.Username = _username; user.Password = _password; user.Email = _email; + user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 0b1a8780..f1fb0c8a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -121,10 +121,10 @@ Forgot Password - User Account Verified Successfully. You Can Now Login With Your Username And Password Below. + User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password. - User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions. + User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions. User Account Linked Successfully. You Can Now Login With Your External Login Below. diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index df4ccc95..ff38ec85 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -216,4 +216,10 @@ The user's time zone + + Confirmed? + + + Indicates if the user's email is verified + \ No newline at end of file diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 37648c4f..7789a2d3 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -140,6 +140,7 @@ namespace Oqtane.Controllers filtered.LastLoginOn = user.LastLoginOn; filtered.LastIPAddress = user.LastIPAddress; filtered.TwoFactorRequired = user.TwoFactorRequired; + filtered.EmailConfirmed = user.EmailConfirmed; filtered.Roles = user.Roles; filtered.CreatedBy = user.CreatedBy; filtered.CreatedOn = user.CreatedOn; @@ -200,10 +201,15 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null + var existing = _userManager.GetUser(user.UserId, user.SiteId); + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { - user.EmailConfirmed = User.IsInRole(RoleNames.Admin); + // only administrators can update the email confirmation + if (!User.IsInRole(RoleNames.Admin)) + { + user.EmailConfirmed = existing.EmailConfirmed; + } user = await _userManager.UpdateUser(user); } else diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 84679d23..5e1e6e64 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -65,7 +65,12 @@ namespace Oqtane.Managers { user.SiteId = siteid; user.Roles = GetUserRoles(user.UserId, user.SiteId); - user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp; + var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult(); + if (identityuser != null) + { + user.SecurityStamp = identityuser.SecurityStamp; + user.EmailConfirmed = identityuser.EmailConfirmed; + } user.Settings = _settings.GetSettings(EntityNames.User, user.UserId) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); } @@ -245,22 +250,30 @@ namespace Oqtane.Managers { identityuser.Email = user.Email; await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated - - // if email address changed and it is not confirmed, verification is required for new email address - if (!user.EmailConfirmed) - { - string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; - var notification = new Notification(user.SiteId, user, "User Account Verification", body); - _notifications.AddNotification(notification); - } } if (user.EmailConfirmed) { - var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + if (!identityuser.EmailConfirmed) + { + var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + + string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password."; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); + } + } + else + { + identityuser.EmailConfirmed = false; + await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated + + string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); } user = _users.UpdateUser(user); From 5bde40ec2bd042f27f41c26deb7a1afa4dc4c074 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:46:53 -0400 Subject: [PATCH 09/21] improve messaging --- Oqtane.Client/Resources/Modules/Admin/Login/Index.resx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index f1fb0c8a..c60c0716 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -133,7 +133,7 @@ External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions. - Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. + Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification. Please Provide All Required Fields From fe9f1897341868e997e3b2ed9491f1f5dfe7f57f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 11:53:04 -0400 Subject: [PATCH 10/21] improve comment --- Oqtane.Shared/Models/Folder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs index da875219..47177cdb 100644 --- a/Oqtane.Shared/Models/Folder.cs +++ b/Oqtane.Shared/Models/Folder.cs @@ -43,7 +43,7 @@ namespace Oqtane.Models public string Path { get; set; } /// - /// Sorting order of the folder + /// Sorting order of the folder ** not used as folders are sorted in alphabetical order ** /// public int Order { get; set; } From a4370829526a261f8a1cefaff6608a46ea0ed87b Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 16 May 2025 12:11:03 -0400 Subject: [PATCH 11/21] use consistent authorization method --- Oqtane.Server/Controllers/UserController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 7789a2d3..859bd50c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -205,8 +205,8 @@ namespace Oqtane.Controllers if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { - // only administrators can update the email confirmation - if (!User.IsInRole(RoleNames.Admin)) + // only authorized users can update the email confirmation + if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin)) { user.EmailConfirmed = existing.EmailConfirmed; } From 4b05f7fdad4a204bd52e926e9183479e9252e9e6 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 19 May 2025 15:14:49 -0700 Subject: [PATCH 12/21] imprvoe help text --- Oqtane.Client/Modules/Admin/UrlMappings/Add.razor | 4 ++-- Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor | 4 ++-- Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx | 4 ++-- Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor index 8f43985d..68956c40 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor @@ -8,7 +8,7 @@
- +
@@ -17,7 +17,7 @@
- +
diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor index 78a062f9..c6cb2e0e 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor @@ -8,13 +8,13 @@
- +
- +
diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx index a8de1c59..aeae4b29 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx @@ -121,10 +121,10 @@ Redirect To: - A relative or absolute Url where the user will be redirected. Use "/" for site root path. + A Url where the user will be redirected (absolute or relative). Use '/' for site root path. - An absolute Url for this site + A Url identifying a path to a specific page in the site (absolute or relative) Url: diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx index 0b5d799a..b51c4366 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Edit.resx @@ -121,10 +121,10 @@ Redirect To: - A relative or absolute Url where the user will be redirected. Use "/" for site root path. + A Url where the user will be redirected (absolute or relative). Use '/' for site root path. - A relative Url identifying a path to a specific page in the site + A Url identifying a path to a specific page in the site (absolute or relative) Url: From 2b6ba0f4105ef18d00155275e61a847f4980386d Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 19 May 2025 15:32:27 -0700 Subject: [PATCH 13/21] ensure Content folder is empty when packaging --- Oqtane.Package/release.cmd | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Oqtane.Package/release.cmd b/Oqtane.Package/release.cmd index 0452d806..8bc6518a 100644 --- a/Oqtane.Package/release.cmd +++ b/Oqtane.Package/release.cmd @@ -9,6 +9,8 @@ nuget.exe pack Oqtane.Framework.nuspec del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" > NUL rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release +del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content" > NUL +rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content" del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" > NUL rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" setlocal ENABLEDELAYEDEXPANSION From c098839881e2b50081820e8f1ea87da6d6626418 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 19 May 2025 21:00:35 -0700 Subject: [PATCH 14/21] fix #5205 add support for inheritance when loading Resources from ModuleBase or ThemeBase --- Oqtane.Client/Modules/ModuleBase.cs | 19 +++++++++++-------- Oqtane.Client/Themes/ThemeBase.cs | 19 +++++++++++-------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 49d6b00b..18a8a5e9 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -79,18 +79,21 @@ namespace Oqtane.Modules { List resources = null; var type = GetType(); - if (type.BaseType == typeof(ModuleBase)) + if (type.IsSubclassOf(typeof(ModuleBase))) { - if (PageState.Page.Resources != null) + if (type.IsSubclassOf(typeof(ModuleControlBase))) { - resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList(); + if (Resources != null) + { + resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + } } - } - else // modulecontrolbase - { - if (Resources != null) + else // ModuleBase { - resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + if (PageState.Page.Resources != null) + { + resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList(); + } } } if (resources != null && resources.Any()) diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index aa5eaf5b..4359f48e 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -43,18 +43,21 @@ namespace Oqtane.Themes { List resources = null; var type = GetType(); - if (type.BaseType == typeof(ThemeBase)) + if (type.IsSubclassOf(typeof(ThemeBase))) { - if (PageState.Page.Resources != null) + if (type.IsSubclassOf(typeof(ThemeControlBase)) || type.IsSubclassOf(typeof(ContainerBase))) { - resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Page && item.Namespace == type.Namespace).ToList(); + if (Resources != null) + { + resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + } } - } - else // themecontrolbase, containerbase - { - if (Resources != null) + else // ThemeBase { - resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + if (PageState.Page.Resources != null) + { + resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Page && item.Namespace == type.Namespace).ToList(); + } } } if (resources != null && resources.Any()) From 7fff5c0d18601bc1e0dd359586c3db81abb474eb Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sun, 25 May 2025 10:55:49 +0200 Subject: [PATCH 15/21] Fix for ModuleBase ReplaceTokens #5332 Replaced the ReplaceTokens logic to replace all tokens in the string --- Oqtane.Client/Modules/ModuleBase.cs | 126 ++++++++++++---------------- 1 file changed, 54 insertions(+), 72 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 18a8a5e9..2dac151e 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -1,16 +1,16 @@ -using Microsoft.AspNetCore.Components; -using Oqtane.Shared; -using Oqtane.Models; -using System.Threading.Tasks; -using Oqtane.Services; using System; -using Oqtane.Enums; -using Oqtane.UI; using System.Collections.Generic; -using Microsoft.JSInterop; -using System.Linq; using System.Dynamic; -using System.Reflection; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane.Modules { @@ -424,72 +424,54 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { - var tokens = new List(); - var pos = content.IndexOf("["); - if (pos != -1) - { - if (content.IndexOf("]", pos) != -1) - { - var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1); - if (token.Contains(":")) - { - tokens.Add(token.Substring(1, token.Length - 2)); - } - } - pos = content.IndexOf("[", pos + 1); - } - if (tokens.Count != 0) - { - foreach (string token in tokens) - { - var segments = token.Split(":"); - if (segments.Length >= 2 && segments.Length <= 3) - { - var objectName = string.Join(":", segments, 0, segments.Length - 1); - var propertyName = segments[segments.Length - 1]; - var propertyValue = ""; + // Pattern: [Object:Property] or [Object:SubObject:Property] + var pattern = @"\[(\w+(?::\w+){1,2})\]"; - switch (objectName) - { - case "ModuleState": - propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString(); - break; - case "PageState": - propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString(); - break; - case "PageState:Alias": - propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString(); - break; - case "PageState:Site": - propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString(); - break; - case "PageState:Page": - propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString(); - break; - case "PageState:User": - propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString(); - break; - case "PageState:Route": - propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString(); - break; - default: - if (obj != null && obj.GetType().Name == objectName) - { - propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString(); - } - break; - } - if (propertyValue != null) - { - content = content.Replace("[" + token + "]", propertyValue); - } + return Regex.Replace(content, pattern, match => + { + string token = match.Groups[1].Value; + var segments = token.Split(':'); + if (segments.Length < 2 || segments.Length > 3) + return match.Value; // Leave as is if not a valid token - } + string objectName = string.Join(":", segments, 0, segments.Length - 1); + string propertyName = segments[segments.Length - 1]; + string propertyValue = null; + + switch (objectName) + { + case "ModuleState": + propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null)?.ToString(); + break; + case "PageState": + propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null)?.ToString(); + break; + case "PageState:Alias": + propertyValue = PageState.Alias?.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null)?.ToString(); + break; + case "PageState:Site": + propertyValue = PageState.Site?.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null)?.ToString(); + break; + case "PageState:Page": + propertyValue = PageState.Page?.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null)?.ToString(); + break; + case "PageState:User": + propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null)?.ToString(); + break; + case "PageState:Route": + propertyValue = PageState.Route?.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null)?.ToString(); + break; + default: + if (obj != null && obj.GetType().Name == objectName) + { + propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null)?.ToString(); + } + break; } - } - return content; + + return propertyValue ?? match.Value; // If not found, leave token as is + }); } - // date methods public DateTime? UtcToLocal(DateTime? datetime) { From 543e9339c74da40d2400070822f2c250b53a5e4a Mon Sep 17 00:00:00 2001 From: Cody Date: Sun, 25 May 2025 09:34:09 -0700 Subject: [PATCH 16/21] Update Swashbuckle.AspNetCore Package Dependency to 8.1.2 --- 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 76bb68c8..2068c0ea 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -48,7 +48,7 @@ - + From ef4fbcbb8a8fc5da4ee79fe9d9e8cdeac32476cd Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 28 May 2025 17:30:19 +0200 Subject: [PATCH 17/21] Update ModuleBase.cs This method replaces all tokens in the format [Object:Property] or [Object:SubObject:Property] within a string. Efficient string parsing and reflection ensure flexibility with performance. It supports deeply nested properties, optional default fallback values (e.g. [PageState:User:Email|default@email.com]), and uses caching to optimize repeated token resolution without regex. --- Oqtane.Client/Modules/ModuleBase.cs | 114 +++++++++++++++++----------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 2dac151e..39681067 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; -using System.Text.RegularExpressions; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -424,54 +424,82 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { - // Pattern: [Object:Property] or [Object:SubObject:Property] - var pattern = @"\[(\w+(?::\w+){1,2})\]"; + // Using StringBuilder avoids the performance penalty of repeated string allocations + // that occur with string.Replace or string concatenation inside loops. + var sb = new StringBuilder(); + var cache = new Dictionary(); // Cache to store resolved tokens + int index = 0; - return Regex.Replace(content, pattern, match => + // Loop through content to find and replace all tokens + while (index < content.Length) { - string token = match.Groups[1].Value; - var segments = token.Split(':'); - if (segments.Length < 2 || segments.Length > 3) - return match.Value; // Leave as is if not a valid token - - string objectName = string.Join(":", segments, 0, segments.Length - 1); - string propertyName = segments[segments.Length - 1]; - string propertyValue = null; - - switch (objectName) + int start = content.IndexOf('[', index); // Find start of token + if (start == -1) { - case "ModuleState": - propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null)?.ToString(); - break; - case "PageState": - propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null)?.ToString(); - break; - case "PageState:Alias": - propertyValue = PageState.Alias?.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null)?.ToString(); - break; - case "PageState:Site": - propertyValue = PageState.Site?.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null)?.ToString(); - break; - case "PageState:Page": - propertyValue = PageState.Page?.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null)?.ToString(); - break; - case "PageState:User": - propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null)?.ToString(); - break; - case "PageState:Route": - propertyValue = PageState.Route?.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null)?.ToString(); - break; - default: - if (obj != null && obj.GetType().Name == objectName) - { - propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null)?.ToString(); - } - break; + sb.Append(content, index, content.Length - index); // Append remaining content + break; } - return propertyValue ?? match.Value; // If not found, leave token as is - }); + int end = content.IndexOf(']', start); // Find end of token + if (end == -1) + { + sb.Append(content, index, content.Length - index); // Append unmatched content + break; + } + + sb.Append(content, index, start - index); // Append content before token + + string token = content.Substring(start + 1, end - start - 1); // Extract token without brackets + string[] parts = token.Split('|', 2); // Separate default fallback if present + string key = parts[0]; + string fallback = parts.Length == 2 ? parts[1] : null; + + if (!cache.TryGetValue(token, out string replacement)) // Check cache first + { + replacement = "[" + token + "]"; // Default replacement is original token + string[] segments = key.Split(':'); + + if (segments.Length >= 2) + { + object current = GetTarget(segments[0], obj); // Start from root object + for (int i = 1; i < segments.Length && current != null; i++) + { + var type = current.GetType(); + var prop = type.GetProperty(segments[i]); + current = prop?.GetValue(current); + } + + if (current != null) + { + replacement = current.ToString(); + } + else if (fallback != null) + { + replacement = fallback; // Use fallback if available + } + } + cache[token] = replacement; // Store in cache + } + + sb.Append(replacement); // Append replacement value + index = end + 1; // Move index past token + } + + return sb.ToString(); } + + // Resolve the object instance for a given object name + // Easy to extend with additional object types + private object GetTarget(string name, object obj) + { + return name switch + { + "ModuleState" => ModuleState, + "PageState" => PageState, + _ => (obj != null && obj.GetType().Name == name) ? obj : null // Fallback to obj + }; + } + // date methods public DateTime? UtcToLocal(DateTime? datetime) { From 9c333232e204ad897d4c00a84132159c489c85c1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 May 2025 11:53:14 -0400 Subject: [PATCH 18/21] fix #5329 - clear Options after updating User Settings --- .../Controllers/SettingController.cs | 74 ++++++++++++------- .../OqtaneSiteIdentityBuilderExtensions.cs | 1 - .../Options/SiteOptionsCache.cs | 1 - 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 2579d379..fd8708c1 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -24,26 +24,50 @@ namespace Oqtane.Controllers private readonly IPageModuleRepository _pageModules; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; - private readonly IAliasAccessor _aliasAccessor; - private readonly IOptionsMonitorCache _cookieCache; - private readonly IOptionsMonitorCache _oidcCache; - private readonly IOptionsMonitorCache _oauthCache; - private readonly IOptionsMonitorCache _identityCache; + + private readonly IOptions _cookieOptions; + private readonly IOptionsSnapshot _cookieOptionsSnapshot; + private readonly IOptionsMonitorCache _cookieOptionsMonitorCache; + + private readonly IOptions _oidcOptions; + private readonly IOptionsSnapshot _oidcOptionsSnapshot; + private readonly IOptionsMonitorCache _oidcOptionsMonitorCache; + + private readonly IOptions _oauthOptions; + private readonly IOptionsSnapshot _oauthOptionsSnapshot; + private readonly IOptionsMonitorCache _oauthOptionsMonitorCache; + + private readonly IOptions _identityOptions; + private readonly IOptionsSnapshot _identityOptionsSnapshot; + private readonly IOptionsMonitorCache _identityOptionsMonitorCache; + private readonly ILogManager _logger; private readonly Alias _alias; private readonly string _visitorCookie; - public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IAliasAccessor aliasAccessor, IOptionsMonitorCache cookieCache, IOptionsMonitorCache oidcCache, IOptionsMonitorCache oauthCache, IOptionsMonitorCache identityCache, ILogManager logger) + public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, + IOptions cookieOptions, IOptionsSnapshot cookieOptionsSnapshot, IOptionsMonitorCache cookieOptionsMonitorCache, + IOptions oidcOptions, IOptionsSnapshot oidcOptionsSnapshot, IOptionsMonitorCache oidcOptionsMonitorCache, + IOptions oauthOptions, IOptionsSnapshot oauthOptionsSnapshot, IOptionsMonitorCache oauthOptionsMonitorCache, + IOptions identityOptions, IOptionsSnapshot identityOptionsSnapshot, IOptionsMonitorCache identityOptionsMonitorCache, + ILogManager logger) { _settings = settings; _pageModules = pageModules; _userPermissions = userPermissions; _syncManager = syncManager; - _aliasAccessor = aliasAccessor; - _cookieCache = cookieCache; - _oidcCache = oidcCache; - _oauthCache = oauthCache; - _identityCache = identityCache; + _cookieOptions = cookieOptions; + _cookieOptionsSnapshot = cookieOptionsSnapshot; + _cookieOptionsMonitorCache = cookieOptionsMonitorCache; + _oidcOptions = oidcOptions; + _oidcOptionsSnapshot = oidcOptionsSnapshot; + _oidcOptionsMonitorCache = oidcOptionsMonitorCache; + _oauthOptions = oauthOptions; + _oauthOptionsSnapshot = oauthOptionsSnapshot; + _oauthOptionsMonitorCache = oauthOptionsMonitorCache; + _identityOptions = identityOptions; + _identityOptionsSnapshot = identityOptionsSnapshot; + _identityOptionsMonitorCache = identityOptionsMonitorCache; _logger = logger; _alias = tenantManager.GetAlias(); _visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); @@ -210,21 +234,21 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Admin)] public void Clear() { - // clear SiteOptionsCache for each option type - var cookieCache = new SiteOptionsCache(_aliasAccessor); - cookieCache.Clear(); - var oidcCache = new SiteOptionsCache(_aliasAccessor); - oidcCache.Clear(); - var oauthCache = new SiteOptionsCache(_aliasAccessor); - oauthCache.Clear(); - var identityCache = new SiteOptionsCache(_aliasAccessor); - identityCache.Clear(); + (_cookieOptions as SiteOptionsManager).Reset(); + (_cookieOptionsSnapshot as SiteOptionsManager).Reset(); + _cookieOptionsMonitorCache.Clear(); - // clear IOptionsMonitorCache for each option type - _cookieCache.Clear(); - _oidcCache.Clear(); - _oauthCache.Clear(); - _identityCache.Clear(); + (_oidcOptions as SiteOptionsManager).Reset(); + (_oidcOptionsSnapshot as SiteOptionsManager).Reset(); + _oidcOptionsMonitorCache.Clear(); + + (_oauthOptions as SiteOptionsManager).Reset(); + (_oauthOptionsSnapshot as SiteOptionsManager).Reset(); + _oauthOptionsMonitorCache.Clear(); + + (_identityOptions as SiteOptionsManager).Reset(); + (_identityOptionsSnapshot as SiteOptionsManager).Reset(); + _identityOptionsMonitorCache.Clear(); _logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared"); } diff --git a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs index 6234d2fb..d00c01ae 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Oqtane.Models; using Microsoft.AspNetCore.Identity; using System; diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs index e4737d9a..5c802fa9 100644 --- a/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs +++ b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs @@ -19,7 +19,6 @@ namespace Oqtane.Infrastructure { var cache = map.GetOrAdd(GetKey(), new OptionsCache()); cache.Clear(); - } public TOptions GetOrAdd(string name, Func createOptions) From d5f19d97e2d5c1cc1d660b943a726d1ef718c71a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 May 2025 15:04:12 -0400 Subject: [PATCH 19/21] change id for header/footer --- Oqtane.Client/Modules/Admin/Modules/Settings.razor | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index b7cf4f4c..297a365a 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -101,15 +101,15 @@
- +
- +
- +
- +
From 11150b6a1049da488bab3d34a3da25386bb32a49 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Thu, 29 May 2025 17:03:20 -0400 Subject: [PATCH 20/21] Update README.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a69b4390..10112a56 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.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) was released on April 10, 2025 and is a maintenance release including 41 pull requests by 3 different contributors, pushing the total number of project commits all-time to over 6500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) was released on May 29, 2025 and is a maintenance release including 59 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 6600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -22,11 +22,11 @@ Microsoft's Public Cloud (requires an Azure account) A free ASP.NET hosting account. No hidden fees. No credit card required. [![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) -# Getting Started (Version 6.1.2) +# Getting Started (Version 6) **Installing using source code from the Dev/Master branch:** -- Install **[.NET 9.0.4 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. @@ -92,6 +92,9 @@ 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.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025) +- [x] Stabilization improvements + [6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025) - [x] Stabilization improvements From 985e50d41587c09a618fc07fff678d0c6fd683a4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 29 May 2025 17:04:45 -0400 Subject: [PATCH 21/21] update Azure ARM template to 6.1.3 --- azuredeploy.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredeploy.json b/azuredeploy.json index e57cc1f8..d7970211 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -220,7 +220,7 @@ "apiVersion": "2024-04-01", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "properties": { - "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.2/Oqtane.Framework.6.1.2.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"