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
diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor
index c0187971..c00a0d7e 100644
--- a/Oqtane.Client/Installer/Installer.razor
+++ b/Oqtane.Client/Installer/Installer.razor
@@ -71,14 +71,14 @@
@@ -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/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor
index b39b4f27..83948efe 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);
@@ -196,7 +212,7 @@ else
_openIconSpan = $"{(IconOnly ? "" : " ")}";
_iconSpan = $" ";
}
-
+
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized();
diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor
index 509cef2d..6a2b979d 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
@@ -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
@@ -359,6 +362,8 @@
}
if (restricted == "")
{
+ CancellationTokenSource tokenSource = new CancellationTokenSource();
+
try
{
// upload the files
@@ -377,57 +382,21 @@
}
}
+ 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;
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, tokenSource.Token);
// reset progress indicators
if (ShowProgress)
@@ -452,7 +421,7 @@
}
else
{
- await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads);
+ await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
}
@@ -482,6 +451,10 @@
_message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error;
_uploading = false;
+ await tokenSource.CancelAsync();
+ }
+ finally {
+ tokenSource.Dispose();
}
}
diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs
index b99e7d29..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));
@@ -98,7 +99,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))
{
@@ -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()
@@ -132,6 +142,15 @@ namespace Oqtane.Modules
return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
}
+ // fingerprint hash code for static assets
+ public string Fingerprint
+ {
+ get
+ {
+ return ModuleState.ModuleDefinition.Fingerprint;
+ }
+ }
+
// url methods
// navigate url
diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj
index d0d4769f..4439f011 100644
--- a/Oqtane.Client/Oqtane.Client.csproj
+++ b/Oqtane.Client/Oqtane.Client.csproj
@@ -4,7 +4,7 @@
net9.0ExeDebug;Release
- 6.0.1
+ 6.1.0OqtaneShaun Walker.NET Foundation
@@ -12,7 +12,7 @@
.NET Foundationhttps://www.oqtane.orghttps://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.1.0https://github.com/oqtane/oqtane.frameworkGitOqtane
@@ -22,10 +22,10 @@
-
-
-
-
+
+
+
+
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
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/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx
index 4fa73338..c35e179e 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 (not recommended).Image Sizes:
@@ -198,4 +198,10 @@
Settings
+
+ Caching:
+
+
+ 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/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/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/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/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
diff --git a/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx b/Oqtane.Client/Resources/Modules/Admin/SystemInfo/Index.resx
index 76747197..6a2cafee 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 UIFramework Version
@@ -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,16 @@
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.
+
+
+ Static Asset 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.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.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
index f56d3798..3dbd1d1e 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx
@@ -195,4 +195,19 @@
Last Login:
+
+ Delete User
+
+
+ Delete
+
+
+ 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.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
index 381eff20..34884fb5 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
@@ -495,4 +495,16 @@
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.
+
+
+ User Deleted Successfully
+
+
+ Error Deleting User
+
\ No newline at end of file
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 FailedYou Have Not Selected A File To Upload
diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx
index 8c5ae010..a0078ea5 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
@@ -474,4 +474,7 @@
User
+
+ Path
+
\ No newline at end of file
diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs
index a9e4d3d6..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)
- {
- await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
- }
}
}
diff --git a/Oqtane.Client/Services/Interfaces/IInstallationService.cs b/Oqtane.Client/Services/Interfaces/IInstallationService.cs
index 84790c63..e8a433c7 100644
--- a/Oqtane.Client/Services/Interfaces/IInstallationService.cs
+++ b/Oqtane.Client/Services/Interfaces/IInstallationService.cs
@@ -34,13 +34,5 @@ namespace Oqtane.Services
///
/// internal status/message object
Task RestartAsync();
-
- ///
- /// Registers a new
- ///
- /// Email of the user to be registered
- ///
- Task RegisterAsync(string email);
-
}
}
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/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 = "";
diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs
index 6db6a028..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; }
@@ -62,7 +64,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))
{
@@ -82,6 +84,25 @@ namespace Oqtane.Themes
}
}
}
+ _scriptsloaded = true;
+ }
+
+ public bool ScriptsLoaded
+ {
+ get
+ {
+ return _scriptsloaded;
+ }
+ }
+
+ // 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
@@ -91,6 +112,15 @@ namespace Oqtane.Themes
return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/";
}
+ // fingerprint hash code for static assets
+ public string Fingerprint
+ {
+ get
+ {
+ return ThemeState.Fingerprint;
+ }
+ }
+
// url methods
// navigate url
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("") - 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("
-
-
-
+
+
+
@((MarkupString)_scripts)
@((MarkupString)_bodyResources)
@if (_renderMode == RenderModes.Static)
{
-
+
}
}
else
@@ -94,6 +95,7 @@
private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private bool _prerender = true;
+ private string _fingerprint = "";
private int _visitorId = -1;
private string _antiForgeryToken = "";
private string _remoteIPAddress = "";
@@ -136,6 +138,8 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
+ _fingerprint = site.Fingerprint;
+
var modules = new List();
Route route = new Route(url, alias.Path);
@@ -174,7 +178,7 @@
// get jwt token for downstream APIs
if (Context.User.Identity.IsAuthenticated)
{
- CreateJwtToken(alias);
+ await GetJwtToken(alias);
}
// includes resources
@@ -441,13 +445,19 @@
}
}
- private void CreateJwtToken(Alias alias)
+ private async Task GetJwtToken(Alias alias)
{
- var sitesettings = Context.GetSiteSettings();
- var secret = sitesettings.GetValue("JwtOptions:Secret", "");
- if (!string.IsNullOrEmpty(secret))
+ // 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))
{
- _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
+ // 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")));
+ }
}
}
@@ -514,7 +524,7 @@
private void AddScript(Resource resource, Alias alias)
{
var script = CreateScript(resource, alias);
- if (resource.Location == Shared.ResourceLocation.Head && !resource.Reload)
+ if (resource.Location == Shared.ResourceLocation.Head && resource.LoadBehavior != ResourceLoadBehavior.BlazorPageScript)
{
if (!_headResources.Contains(script))
{
@@ -532,11 +542,27 @@
private string CreateScript(Resource resource, Alias alias)
{
- if (!resource.Reload)
+ if (resource.LoadBehavior == ResourceLoadBehavior.BlazorPageScript)
+ {
+ return "";
+ }
+ 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 +578,6 @@
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
">";
}
- else
- {
- return "";
- }
}
private void SetLocalizationCookie(string cookieValue)
@@ -583,13 +605,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.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), 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)
@@ -597,7 +619,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.Fingerprint, site.RenderMode);
}
}
// theme settings components are dynamically loaded within the framework Page Management module
@@ -607,7 +629,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.Fingerprint, site.RenderMode);
}
}
@@ -616,7 +638,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.Fingerprint, site.RenderMode);
// handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@@ -662,7 +684,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?.Fingerprint, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@@ -683,7 +705,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?.Fingerprint, site.RenderMode);
}
// container settings component
@@ -693,7 +715,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.Fingerprint, site.RenderMode);
}
}
}
@@ -709,7 +731,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.Fingerprint, site.RenderMode);
}
}
}
@@ -717,7 +739,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 fingerprint, string rendermode)
{
if (resources != null)
{
@@ -737,7 +759,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, fingerprint));
}
}
}
diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs
index 70d17d1d..4bdb8a90 100644
--- a/Oqtane.Server/Controllers/FileController.cs
+++ b/Oqtane.Server/Controllers/FileController.cs
@@ -21,6 +21,8 @@ using System.Net.Http;
using Microsoft.AspNetCore.Cors;
using System.IO.Compression;
using Oqtane.Services;
+using Microsoft.Extensions.Primitives;
+using Microsoft.AspNetCore.Http.HttpResults;
// ReSharper disable StringIndexOfIsCultureSpecific.1
@@ -427,75 +429,98 @@ 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 (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
- 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();
+ _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) || !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 Is Missing Required Headers");
+ return StatusCode((int)HttpStatusCode.Forbidden);
+ }
+
+ // 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, formfile.FileName), FileMode.Create))
- {
- await formfile.CopyToAsync(stream);
- }
-
- string upload = await MergeFile(folderPath, formfile.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)
@@ -510,10 +535,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 +561,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")
diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs
index 92e5b289..207d082c 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, config.Register);
}
}
else
@@ -257,7 +257,7 @@ namespace Oqtane.Controllers
}
}
- private async Task RegisterContact(string email)
+ 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)}")).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
- [HttpPost("register")]
- [Authorize(Roles = RoleNames.Host)]
- public async Task Register(string email)
- {
- await RegisterContact(email);
- }
-
public struct ClientAssembly
{
public ClientAssembly(string filepath, bool hashfilename)
@@ -294,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
{
@@ -305,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/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs
index 8e439fd2..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
{
diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs
index 795eb525..c0b943bc 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, true);
+ }
+ }
+ }
+ return email;
+ }
+
private async Task GetJson(HttpClient httpClient, string url)
{
try
diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs
index 44e38808..f8ad1925 100644
--- a/Oqtane.Server/Controllers/PageController.cs
+++ b/Oqtane.Server/Controllers/PageController.cs
@@ -9,7 +9,8 @@ using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
-using System;
+using System.Xml.Linq;
+using Microsoft.AspNetCore.Diagnostics;
namespace Oqtane.Controllers
{
@@ -189,15 +190,16 @@ 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 = 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 = parent.Path + "/" + user.Username;
- 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 = "";
@@ -250,6 +252,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
@@ -274,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.PageId, page.Path, newPath, deleted);
// get differences between current and new page permissions
var added = GetPermissionsDifferences(page.PermissionList, currentPermissions);
@@ -315,6 +318,7 @@ namespace Oqtane.Controllers
});
}
}
+
// permissions removed
foreach (Permission permission in removed)
{
@@ -338,8 +342,29 @@ 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
@@ -351,6 +376,39 @@ namespace Oqtane.Controllers
return page;
}
+ private void UpdatePage(Page page, int pageId, string oldPath, string newPath, bool deleted)
+ {
+ var update = (page.PageId == pageId);
+ 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, pageId, oldPath, newPath, deleted);
+ }
+ }
+
private List GetPermissionsDifferences(List permissions1, List permissions2)
{
var differences = new List();
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/Controllers/SystemController.cs b/Oqtane.Server/Controllers/SystemController.cs
index b34611f4..ce067c2e 100644
--- a/Oqtane.Server/Controllers/SystemController.cs
+++ b/Oqtane.Server/Controllers/SystemController.cs
@@ -53,7 +53,9 @@ 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", ""));
systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl));
+ systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", ""));
break;
case "log":
string log = "";
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", $"" },
};
});
}
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/Controllers/UserRoleController.cs b/Oqtane.Server/Controllers/UserRoleController.cs
index 38f89705..bc557e75 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,57 +101,59 @@ 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;
}
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/
diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
index 56b0d3bd..0e58e4b6 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
@@ -527,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();
@@ -591,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/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 30593b3b..2273cded 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 automated 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 = "";
}
}
@@ -261,6 +265,7 @@ namespace Oqtane.Infrastructure
var installation = IsInstalled();
try
{
+ UpdateInstallation();
UpdateConnectionString(install.ConnectionString);
UpdateDatabaseType(install.DatabaseType);
@@ -487,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();
}
@@ -662,6 +668,11 @@ namespace Oqtane.Infrastructure
return connectionString;
}
+ public void UpdateInstallation()
+ {
+ _config.GetInstallationId();
+ }
+
public void UpdateConnectionString(string connectionString)
{
connectionString = DenormalizeConnectionString(connectionString);
@@ -673,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/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/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs
index e3fd1cd4..90b351ef 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
{
@@ -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 = ""
diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs
index 96b762f9..09b1f30a 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
{
@@ -68,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.
"
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
{
diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs
index 0ba6642b..210961c4 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;
@@ -71,8 +72,8 @@ namespace Oqtane.Infrastructure
case "5.2.1":
Upgrade_5_2_1(tenant, scope);
break;
- case "6.0.1":
- Upgrade_6_0_1(tenant, scope);
+ case "6.1.0":
+ Upgrade_6_1_0(tenant, scope);
break;
}
}
@@ -446,52 +447,14 @@ namespace Oqtane.Infrastructure
AddPagesToSites(scope, tenant, pageTemplates);
}
- private void Upgrade_6_0_1(Tenant tenant, IServiceScope scope)
+ private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope)
{
- // assemblies which have been relocated to the bin/refs folder in .NET 9
+ // remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql)
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"
+ "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.1 Upgrade Error Removing {assembly} - {ex}"));
- }
- }
+ RemoveAssemblies(tenant, assemblies, "6.1.0");
}
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates)
@@ -504,5 +467,27 @@ namespace Oqtane.Infrastructure
sites.CreatePages(site, pageTemplates, null);
}
}
+
+ 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 (tenant.Name == TenantNames.Master && !_environment.IsDevelopment())
+ {
+ 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}"));
+ }
+ }
+ }
+ }
}
}
diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs
index 7745f429..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
@@ -363,28 +364,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 (UserSecurity.ContainsRole(user.Roles, 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);
}
}
}
@@ -490,6 +499,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 = "";
}
@@ -512,7 +524,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;
}
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));
diff --git a/Oqtane.Server/Migrations/Master/06010001_AddThemeVersion.cs b/Oqtane.Server/Migrations/Master/06010001_AddThemeVersion.cs
new file mode 100644
index 00000000..d88f55c1
--- /dev/null
+++ b/Oqtane.Server/Migrations/Master/06010001_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.01.00.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/Migrations/Tenant/06010001_AddFolderCacheControl.cs b/Oqtane.Server/Migrations/Tenant/06010001_AddFolderCacheControl.cs
new file mode 100644
index 00000000..d2cd810b
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/06010001_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.01.00.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/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index ba241482..5c1852df 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -3,7 +3,7 @@
net9.0Debug;Release
- 6.0.1
+ 6.1.0OqtaneShaun Walker.NET Foundation
@@ -11,7 +11,7 @@
.NET Foundationhttps://www.oqtane.orghttps://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.1.0https://github.com/oqtane/oqtane.frameworkGitOqtane
@@ -34,21 +34,21 @@
-
-
-
-
-
+
+
+
+
+ allruntime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -57,7 +57,7 @@
-
+
diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs
index 463534fc..5af6737a 100644
--- a/Oqtane.Server/Pages/Files.cshtml.cs
+++ b/Oqtane.Server/Pages/Files.cshtml.cs
@@ -122,17 +122,23 @@ 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;
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 +146,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))
@@ -253,12 +254,16 @@ 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
{
+ 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, file.GetMimeType());
+ return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName));
}
}
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.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) =>
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;
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/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs
index 15eadf9e..204578e4 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.Fingerprint = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm"));
}
return ModuleDefinition;
diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs
index 2802f8f0..fd010b8c 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.Fingerprint = 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/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs
index 71c80e7c..774c3189 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();
}
}
@@ -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 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;
+ }
}
}
diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs
index 2a12b38b..11c624c3 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.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));
}
else
{
@@ -155,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))
diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs
index d873bd24..8ab42dd8 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;
});
@@ -202,9 +203,15 @@ namespace Oqtane
app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions
{
- ServeUnknownFileTypes = true,
OnPrepareResponse = (ctx) =>
{
+ // static asset caching
+ var cachecontrol = Configuration.GetSection("CacheControl");
+ if (!string.IsNullOrEmpty(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);
diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json
index 28270bab..54c22b30 100644
--- a/Oqtane.Server/appsettings.json
+++ b/Oqtane.Server/appsettings.json
@@ -55,4 +55,4 @@
"Default": "Information"
}
}
-}
+}
\ No newline at end of file
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/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 @@
-
-
-
-
-
+
+
+
+
+
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;
}
}
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 @@
-
-
-
-
+
+
+
+
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 @@
-
-
-
+
+
+
diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js
index ee81109c..719eb63e 100644
--- a/Oqtane.Server/wwwroot/js/interop.js
+++ b/Oqtane.Server/wwwroot/js/interop.js
@@ -308,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) {
diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js
index bb9ad076..6766e74d 100644
--- a/Oqtane.Server/wwwroot/js/reload.js
+++ b/Oqtane.Server/wwwroot/js/reload.js
@@ -1,67 +1,70 @@
-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 "true"... or if the script has not been loaded previously and data-reload is "once"
+ let dataReload = script.getAttribute('data-reload');
+ if ((dataReload === 'always' || dataReload === 'true') || (!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);
- }
- }
- }
-
- // remove scripts that are no longer referenced
- for (const [key, scriptInfo] of scriptInfoBySrc) {
- if (scriptInfo.timestamp !== timestamp) {
- scriptInfoBySrc.delete(key);
+function reloadScript(script) {
+ try {
+ if (isValid(script)) {
+ replaceScript(script);
}
+ } catch (error) {
+ console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, 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(`Blazor Script Reload does not support scripts using document.write(): ${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 +73,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..6751208e
--- /dev/null
+++ b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs
@@ -0,0 +1,10 @@
+namespace Oqtane.Shared
+{
+ public enum ResourceLoadBehavior
+ {
+ Once,
+ Always,
+ None,
+ BlazorPageScript
+ }
+}
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
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
diff --git a/Oqtane.Shared/Models/ModuleDefinition.cs b/Oqtane.Shared/Models/ModuleDefinition.cs
index 5c374667..ce344d09 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 Fingerprint { 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 92b64bef..42ecb5f9 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
@@ -84,7 +83,22 @@ namespace Oqtane.Models
///
public string Namespace { get; set; }
- public Resource Clone(ResourceLevel level, string name)
+ ///
+ /// Unique identifier of the version of the theme or module that declared the resource - for cache busting - only used in SiteRouter
+ ///
+ 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;
+ }
+ }
+ }
+
+ public Resource Clone(ResourceLevel level, string name, string fingerprint)
{
var resource = new Resource();
resource.ResourceType = ResourceType;
@@ -96,7 +110,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)
{
@@ -107,6 +121,7 @@ namespace Oqtane.Models
}
resource.Level = level;
resource.Namespace = name;
+ resource.Fingerprint = fingerprint;
return resource;
}
@@ -125,5 +140,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..1bcdceb6 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;
@@ -12,21 +13,17 @@ 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 Content, string Type)
- {
- SetDefaults();
- this.Content = Content;
- this.Type = Type;
- }
-
public Script(string Src, string Integrity, string CrossOrigin)
{
SetDefaults();
@@ -35,6 +32,65 @@ namespace Oqtane.Models
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();
+ this.Content = Content;
+ this.Type = Type;
+ }
+
+ public Script(string Content, ResourceLoadBehavior LoadBehavior)
+ {
+ SetDefaults();
+ this.Content = Content;
+ this.LoadBehavior = LoadBehavior;
+ }
+
+ public Script(string Content, ResourceLocation Location, ResourceLoadBehavior LoadBehavior, Dictionary DataAttributes, string Type, string RenderMode)
+ {
+ SetDefaults();
+ 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();
+ 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 +101,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;
}
+
}
}
diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs
index b108cea2..aeb6e37b 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; }
+ ///
+ /// fingerprint for framework static assets
+ ///
+ [NotMapped]
+ public string Fingerprint { 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,
+ Fingerprint = Fingerprint
};
}
diff --git a/Oqtane.Shared/Models/Theme.cs b/Oqtane.Shared/Models/Theme.cs
index 4ff9fe2f..a67a2d97 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 Fingerprint { get; set; }
+
#region Obsolete Properties
[Obsolete("This property is obsolete. Use Themes instead.", false)]
diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj
index e5209a21..91f13025 100644
--- a/Oqtane.Shared/Oqtane.Shared.csproj
+++ b/Oqtane.Shared/Oqtane.Shared.csproj
@@ -3,7 +3,7 @@
net9.0Debug;Release
- 6.0.1
+ 6.1.0OqtaneShaun Walker.NET Foundation
@@ -11,7 +11,7 @@
.NET Foundationhttps://www.oqtane.orghttps://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.1.0https://github.com/oqtane/oqtane.frameworkGitOqtane
@@ -19,11 +19,11 @@
-
-
-
+
+
+
-
+
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));
diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs
index 7f56d72f..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.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.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.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)
{
diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj
index 24dd1348..58cd963e 100644
--- a/Oqtane.Updater/Oqtane.Updater.csproj
+++ b/Oqtane.Updater/Oqtane.Updater.csproj
@@ -3,7 +3,7 @@
net9.0Exe
- 6.0.1
+ 6.1.0OqtaneShaun Walker.NET Foundation
@@ -11,7 +11,7 @@
.NET Foundationhttps://www.oqtane.orghttps://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.1.0https://github.com/oqtane/oqtane.frameworkGitOqtane
diff --git a/README.md b/README.md
index 3280b496..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).
@@ -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.
[](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
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.