Compare commits

...

134 Commits

Author SHA1 Message Date
611084d00c Merge pull request #3590 from oqtane/master
5.0.1 Release
2023-12-21 12:38:37 -05:00
1b7c810eeb Merge pull request #3589 from oqtane/dev
5.0.1 Release
2023-12-21 12:38:08 -05:00
e99df58bce Merge pull request #3585 from sbwalker/dev
add an AllowRichText parameter to the RichTextEditor component and add logic to handle JS Interop failures
2023-12-20 16:59:06 -05:00
70139221ab add an AllowRichText parameter to the RichTextEditor component and add logic to handle JS Interop failures 2023-12-20 16:58:46 -05:00
5d8d2b4a34 Merge pull request #3579 from sbwalker/dev
ensure database provider assemblies are included in Publish folder
2023-12-19 16:54:25 -05:00
f59f322226 ensure database provider assemblies are included in Publish folder 2023-12-19 16:54:11 -05:00
acca3f37b5 Merge pull request #3577 from sbwalker/dev
remove spaces from file extensions
2023-12-19 14:30:30 -05:00
0de62f620d remove spaces from file extensions 2023-12-19 14:30:18 -05:00
351c4a00e9 Merge pull request #3576 from sbwalker/dev
prepare for 5.0.1 release
2023-12-19 12:51:58 -05:00
b048d81ac0 prepare for 5.0.1 release 2023-12-19 12:51:44 -05:00
b9c9a2b75f Merge pull request #3575 from sbwalker/dev
add defensive logic to Alias.Path, improve new GetAlias method
2023-12-19 08:41:49 -05:00
4cea22d813 add defensive logic to Alias.Path, improve new GetAlias method 2023-12-19 08:41:36 -05:00
edad4e7d35 Merge pull request #3572 from thabaum/patch-5
Sets Logo max height to 90px plus padding 5px right and left.
2023-12-19 08:02:09 -05:00
e28acd8d2d Merge pull request #3574 from sbwalker/dev
handle loading/saving of File Extensions
2023-12-19 07:40:01 -05:00
86feaff82f handle loading/saving of File Extensions 2023-12-19 07:39:48 -05:00
5f747db224 Logo max-height and padding 2023-12-18 14:12:26 -08:00
ccda41e18e Logo max-height 90px, Padding 5px L/R 2023-12-18 14:11:29 -08:00
2bb83c6532 Revert app logo css changes 2023-12-18 14:10:23 -08:00
ec59faba8c Merge pull request #3573 from sbwalker/dev
add database provider projects to Oqtane.sln
2023-12-18 16:31:06 -05:00
c5d8c44b81 add database provider projects to Oqtane.sln 2023-12-18 16:30:49 -05:00
7a4e4629e5 Logo max height 90px 2023-12-18 13:03:19 -08:00
4d3dd95a60 Merge pull request #3568 from sbwalker/dev
add additional SetAlias overload
2023-12-18 15:07:20 -05:00
713021359e add additional SetAlias overload 2023-12-18 15:07:07 -05:00
927d2c36e8 Merge pull request #3566 from sbwalker/dev
abstract padding logic - don't repeat
2023-12-18 09:58:44 -05:00
afc6368915 abstract padding logic - don't repeat 2023-12-18 09:58:30 -05:00
3f604ad7b5 Merge pull request #3565 from sbwalker/dev
fix #3556 - pad token secret to 32 characters to resolve IDX1-720, change id of "secret" input to reduce chance of form autocomplete causing issues
2023-12-18 09:51:43 -05:00
c5d4e237ad fix #3556 - pad token secret to 32 characters to resolve IDX1-720, change id of "secret" input to reduce chance of form autocomplete causing issues 2023-12-18 09:51:18 -05:00
0b50b21779 Merge pull request #3559 from thabaum/patch-3
Correct formatting of _pagesWithModules declaration
2023-12-18 08:01:44 -05:00
cafc70f1c9 Correct formatting of _pagesWithModules declaration 2023-12-15 10:47:41 -08:00
b4377c346f Merge pull request #3558 from sbwalker/dev
modify module and theme template release.cmd to remove old nupkg files before packaging
2023-12-15 11:08:24 -05:00
7c206af757 modify module and theme template release.cmd to remove old nupkg files before packaging 2023-12-15 11:08:05 -05:00
6212d0a052 Merge pull request #3557 from sbwalker/dev
a simple dependency manager for assemblies
2023-12-15 11:06:01 -05:00
8909822aea a simple dependency manager for assemblies 2023-12-15 11:05:48 -05:00
5fc0964f73 Merge pull request #3555 from sbwalker/dev
suppress compiler warnings for internal Entity Framework Core API usage and Analyzer 'Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.WebApplicationBuilderAnalyzer' (.NET8)
2023-12-14 15:23:04 -05:00
7725559a44 suppress compiler warnings for internal Entity Framework Core API usage and Analyzer 'Microsoft.AspNetCore.Analyzers.WebApplicationBuilder.WebApplicationBuilderAnalyzer' (.NET8) 2023-12-14 15:22:43 -05:00
03414a58e9 Merge pull request #3553 from sbwalker/dev
eliminate database provider nuget packages
2023-12-14 14:32:32 -05:00
ade0419bf6 eliminate database provider nuget packages 2023-12-14 14:32:19 -05:00
9d646b2eed Merge pull request #3552 from sbwalker/dev
ignore Blazor framework requests
2023-12-13 18:25:34 -05:00
0d718a5ca2 ignore Blazor framework requests 2023-12-13 18:25:21 -05:00
78c8b36dfa Merge pull request #3551 from sbwalker/dev
remove unecessary resource key
2023-12-13 13:34:24 -05:00
b5ca0874fa remove unecessary resource key 2023-12-13 13:34:08 -05:00
e209becff9 Merge pull request #3548 from leigh-pointer/ModuleMangPages
Module Management Ehancement - show where modules are used.
2023-12-13 11:22:22 -05:00
652d42aa6b Rework
Not to display the number of instances of the module on those pages.
Name the tab "Pages".
2023-12-13 17:19:19 +01:00
058a11597f Merge pull request #3550 from sbwalker/dev
fix regression issue
2023-12-13 11:18:44 -05:00
dd73d6e19a fix regression issue 2023-12-13 11:18:32 -05:00
3793f618a8 Merge pull request #3549 from sbwalker/dev
HTML encode notifications sent by non-admins to prevent HTML injection
2023-12-13 10:07:43 -05:00
6621983a9c HTML encode notifications sent by non-admins to prevent HTML injection 2023-12-13 10:07:21 -05:00
e0b0302f5a Check for Deleted Pageds 2023-12-12 22:56:02 +01:00
b02584bec6 Enhancment to the Module Managment to show where module is used.
Added a new tab that lists all the pages where there is a module instance with the count of the number of instances.
This helps when making a decision to delete the module from the framework.
2023-12-12 22:28:18 +01:00
c832d61409 Merge pull request #3547 from sbwalker/dev
set authentication cookie to HttpOnly
2023-12-12 15:56:30 -05:00
ac701f28b5 set authentication cookie to HttpOnly 2023-12-12 15:56:16 -05:00
53032140e7 Merge pull request #3546 from sbwalker/dev
security improvement - ensure returnurl is a relativre path
2023-12-12 15:55:03 -05:00
3c7633564f security improvement - ensure returnurl is a relativre path 2023-12-12 15:54:52 -05:00
43eae40404 Merge pull request #3545 from sbwalker/dev
add support for root sitemap.xml and robots.txt
2023-12-12 14:50:21 -05:00
e34b1b54d5 add support for root sitemap.xml and robots.txt 2023-12-12 14:50:08 -05:00
0658a7de0d Merge pull request #3544 from sbwalker/dev
comsider name and email claim values as optional
2023-12-12 14:04:08 -05:00
9e0a4dfac8 comsider name and email claim values as optional 2023-12-12 14:03:47 -05:00
82034ade90 Merge pull request #3543 from sbwalker/dev
modify #3528 so that it only saves the ImagesFiles and UploadableFiles if they are different than the default - otherwise it sets them to blank. This provides forward compatibility if the default values are updated in future framework versions.
2023-12-12 13:52:03 -05:00
b979203e12 modify #3528 so that it only saves the ImagesFiles and UploadableFiles if they are different than the default - otherwise it sets them to blank. This provides forward compatibility if the default values are updated in future framework versions. 2023-12-12 13:51:41 -05:00
c9a368f13a Merge pull request #3542 from sbwalker/dev
move message into file upload region
2023-12-12 13:39:52 -05:00
cfabf22fef move message into file upload region 2023-12-12 13:39:37 -05:00
61aef77916 Merge pull request #3541 from sbwalker/dev
refactor fix for #3539 to avoid string comparison
2023-12-12 13:32:45 -05:00
8b6c6beceb refactor fix for #3539 to avoid string comparison 2023-12-12 13:32:33 -05:00
4688e49454 Merge pull request #3540 from leigh-pointer/LoginURL
Fix #3539 for Multiple click on login button
2023-12-12 13:16:16 -05:00
76cc28b215 Merge pull request #3535 from rcpacheco/rcp_manage_language
Edit languages to set default.
2023-12-12 13:02:17 -05:00
467e5423d2 Fix #3539 for Multiple click on login button
Updated the LoginBase, if the URI doesn't contain the login URL then the NavigationManager is updated.
2023-12-12 07:25:35 +01:00
422bf7b689 Edit languages to set default. 2023-12-07 18:12:49 -06:00
1d230bd4aa Merge pull request #3533 from sbwalker/dev
fix issue where the list of containers was not being refreshed when the theme was changed
2023-12-05 16:57:42 -05:00
029850842b fix issue where the list of containers was not being refreshed when the theme was changed 2023-12-05 16:57:22 -05:00
3b090396a4 Merge pull request #3532 from sbwalker/dev
added HybridEnabled field to Site table to indicate if .NET MAUI hybrid applications can be integrated
2023-12-04 16:35:24 -05:00
2e4656ae8b added HybridEnabled field to Site table to indicate if .NET MAUI hybrid applications can be integrated 2023-12-04 16:35:03 -05:00
ebce63baa4 Merge pull request #3531 from sbwalker/dev
add generic Result model for leveraging Result pattern
2023-12-04 15:41:19 -05:00
08f691ee0d add generic Result model for leveraging Result pattern 2023-12-04 15:41:00 -05:00
7550a19951 Merge pull request #3528 from leigh-pointer/FileExtentions
File Extension management - site wide. Fix for #3493
2023-12-04 15:16:42 -05:00
daeb76df11 Corrected the IsPrivate 2023-12-04 17:26:37 +01:00
31315e5ba6 Merge pull request #3530 from sbwalker/dev
allow AddModuleMessage to support displaying the message at the bottom of the module instance
2023-12-04 09:57:10 -05:00
e4e1fa6a4c allow AddModuleMessage to support displaying the message at the bottom of the module instance 2023-12-04 09:55:53 -05:00
6e36312be8 Update Site with ImageFiles and UploadableFiles 2023-12-04 09:07:11 +01:00
9cc7ba1d82 Merge branch 'oqtane:dev' into FileExtentions 2023-12-04 08:15:49 +01:00
9182001147 Merge pull request #3529 from sbwalker/dev
invalidate client assemblies cache when site is updated
2023-12-03 10:27:53 -05:00
5d1510083e invalidate client assemblies cache when site is updated 2023-12-03 10:27:33 -05:00
7035f4cc1f File Extension management - site wide.
I have added to the site settings file extension management.
The Constants remain for backward compatibility.
If the extensions are not updated then the Constant will be used.
2023-12-01 21:09:33 +01:00
da826b2a22 Merge pull request #3527 from sbwalker/dev
optimize assembly loading performance
2023-12-01 14:54:56 -05:00
a152d8664d optimize assembly loading performance 2023-12-01 14:54:44 -05:00
6ad75d963a Merge pull request #3526 from sbwalker/dev
only allow download of assemblies when using WebAssembly
2023-12-01 14:38:32 -05:00
e92d34a9e3 only allow download of assemblies when using WebAssembly 2023-12-01 14:38:16 -05:00
768066db58 Merge pull request #3525 from leigh-pointer/Retention
Retention controls and data should be number / int not string
2023-12-01 12:19:48 -05:00
886b63e55b Retention controls and data should be number / int not string
The Retention input controls in Log, Visitors and Notifications allowed text to be saved.  Modified all controls to accept only number.
2023-12-01 18:05:52 +01:00
3c7ee65b7d Merge pull request #3523 from beolafsen/dev
Extend the Section component  with "IsVisible" parameter
2023-12-01 08:50:19 -05:00
0e060c4564 Extend the Section component with "Visible" parameter 2023-12-01 07:34:17 +01:00
3e127dbd9c Merge pull request #3522 from sbwalker/dev
remove Blazor Hybrid from Hosting Models selection as it is not actually a hosting model
2023-11-30 17:19:18 -05:00
ad66d14a1b remove Blazor Hybrid from Hosting Models selection as it is not actually a hosting model 2023-11-30 17:18:37 -05:00
5db61b78ea Merge pull request #3518 from sbwalker/dev
add defensive logic to handle scenario where a tenant connection string does not exist in appsettings.json
2023-11-29 17:32:08 -05:00
85f9597f2c add defensive logic to handle scenario where a tenant connection string does not exist in appsettings.json 2023-11-29 17:31:51 -05:00
73aca22605 Merge pull request #3517 from sbwalker/dev
suppress logging for resized image files which no longer exist
2023-11-29 13:59:06 -05:00
077343ca20 suppress logging for resized image files which no longer exist 2023-11-29 13:58:52 -05:00
c76946c770 Merge pull request #3516 from sbwalker/dev
update SQLite and PostgreSQL to latest packages
2023-11-29 13:29:17 -05:00
2344a49260 update SQLite and PostgreSQL to latest packages 2023-11-29 13:28:53 -05:00
65f463dbbd Merge pull request #3471 from PfaffIC/dev-loginauthcookie
Added auth cookie expiration for external login via OAuth2.
2023-11-29 13:22:39 -05:00
6e100a70b9 Merge pull request #3510 from markdav-is/patch-4
Update _Host.cshtml.cs to account for fully qualified type names in theme resources
2023-11-29 13:21:39 -05:00
77650ebe09 Merge pull request #3515 from sbwalker/dev
refactor alwaysremember logic for consistency
2023-11-29 13:19:59 -05:00
cdba2bd9e2 refactor alwaysremember logic for consistency 2023-11-29 13:19:45 -05:00
e84cc92fcd Merge pull request #3514 from sbwalker/dev
add ClaimsPrincipal extension methods
2023-11-29 13:02:10 -05:00
a2890948bb add ClaimsPrincipal extension methods 2023-11-29 13:01:54 -05:00
f2e1ebef45 Merge pull request #3513 from sbwalker/dev
user identity improvements
2023-11-29 10:42:50 -05:00
3c33614115 user identity improvements 2023-11-29 10:42:23 -05:00
fcc4b1e8ee Merge pull request #3511 from sbwalker/dev
include Review Claims option in External Login for troubleshooting settings
2023-11-27 15:37:51 -05:00
c8ac4ec1e8 include a more detailed error message for tenant database migration issues 2023-11-27 15:35:58 -05:00
93ab8b88d4 include Review Claims option in External Login for troubleshooting settings 2023-11-27 15:07:48 -05:00
cc12c302e3 Update _Host.cshtml.cs
Noticed some spaces showing up in theme resources and tracked it down to this line.  Wrapping the name in a call to Utilities.GetTypeName cleared things up.
2023-11-26 18:13:41 -08:00
73941ca30e Merge pull request #3506 from sbwalker/dev
remove InstallDatabase method as it is no longer required now that all database providers are installed automatically
2023-11-22 16:10:03 -05:00
f963711820 remove InstallDatabase method as it is no longer required now that all database providers are installed automatically 2023-11-22 15:53:25 -05:00
1a0a301fb6 Merge pull request #3505 from sbwalker/dev
change terminology to Uninstall rather than Delete in Module Definitions and Themes - to better describe the action
2023-11-22 15:44:25 -05:00
f87a9b7aae change terminology to Uninstall rather than Delete in Module Definitions and Themes - to better describe the action 2023-11-22 15:44:07 -05:00
f18f0bc762 Merge pull request #3504 from sbwalker/dev
only display Authorization Response Type option for OpenIDConnect and fix missing localization
2023-11-22 15:37:08 -05:00
c3b1bc6674 only display Authorization Response Type option for OpenIDConnect and fix missing localization 2023-11-22 15:36:49 -05:00
e9bb228528 Merge pull request #3503 from sbwalker/dev
prevent localized Microsoft.CodeAnalysis.*.resources.dll files from being included in release
2023-11-22 15:04:19 -05:00
a906957454 prevent localized Microsoft.CodeAnalysis.*.resources.dll files from being included in release 2023-11-22 15:04:01 -05:00
49d10337a1 Merge pull request #3502 from sbwalker/dev
add Update API validation to default module template
2023-11-22 14:55:07 -05:00
87c6c31f1f add Update API validation to default module template 2023-11-22 14:54:06 -05:00
5f3639e396 Merge pull request #3501 from sbwalker/dev
add additional validation logic to Update API methods to ensure model ID matches ID parameter
2023-11-22 14:47:53 -05:00
14d36ef8dc add additional validation logic to Update API methods to ensure model ID matches ID parameter 2023-11-22 14:47:28 -05:00
fc186f1718 Merge pull request #3496 from sbwalker/dev
ignore nuget packages
2023-11-21 15:41:23 -05:00
70aa93fe8d ignore nuget packages 2023-11-21 15:41:13 -05:00
901df22d39 Merge pull request #3495 from sbwalker/dev
add database provider packages back to wwwroot/Packages as they are needed when cloning a fresh instance of source from Github
2023-11-21 15:22:05 -05:00
cd0fe3d6d4 add database provider packages back to wwwroot/Packages as they are needed when cloning a fresh instance of source from Github 2023-11-21 15:21:44 -05:00
09f2c3f645 Merge pull request #3494 from sbwalker/dev
fix .NET upgrade issue related to database provider packages
2023-11-21 15:15:17 -05:00
116542d8e4 fix .NET upgrade issue related to database provider packages 2023-11-21 15:09:14 -05:00
ffae6e269b Merge pull request #3487 from sbwalker/dev
increase size of MaximumReceiveMessageSize for Blazor Server
2023-11-17 16:06:24 -05:00
2194dc0463 increase size of MaximumReceiveMessageSize for Blazor Server 2023-11-17 16:06:11 -05:00
da2d426137 Update README.md 2023-11-16 15:49:22 -05:00
8df7672624 Update README.md 2023-11-16 15:15:34 -05:00
b9dba61c97 Update README.md 2023-11-16 15:07:53 -05:00
96e8e9736f Added auth cookie expiration for external login via OAuth2.
Auth cookie expiration time ist set to value provided in Setting "LoginOptions:CookieExpiration" (if provided).
2023-11-13 11:28:58 +01:00
130 changed files with 1870 additions and 1009 deletions

2
.gitignore vendored
View File

@ -16,7 +16,7 @@ _ReSharper.Caches
Oqtane.Server/appsettings.json
Oqtane.Server/Data
/Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml
Oqtane.Server/Properties/PublishProfiles/FolderProfile.pubxml
Oqtane.Server/Content
Oqtane.Server/Packages
Oqtane.Server/wwwroot/Content

View File

@ -13,7 +13,7 @@
{
string url = NavigateUrl(p.Path);
<p class="col-md-2 mx-auto text-center mb-3">
<NavLink class="nav-link text-primary" href="@url" Match="NavLinkMatch.All">
<NavLink class="nav-link text-body" href="@url" Match="NavLinkMatch.All">
<h2><span class="@p.Icon" aria-hidden="true"></span></h2>
<p class="lead">@((MarkupString)SharedLocalizer[p.Name].ToString().Replace(" ", "<br />"))</p>
</NavLink>

View File

@ -4,6 +4,7 @@
@inject NavigationManager NavigationManager
@inject IFileService FileService
@inject IFolderService FolderService
@inject ISettingService SettingService
@inject IStringLocalizer<Add> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -80,6 +81,7 @@
{
validated = true;
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{
if (_url == string.Empty || _folderId == -1)
@ -93,7 +95,7 @@
_name = _url.Substring(_url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", "")))
if (!PageState.Site.UploadableFiles.Split(',').Contains(Path.GetExtension(_name).ToLower().Replace(".", "")))
{
AddModuleMessage(Localizer["Message.Download.InvalidExtension"], MessageType.Warning);
return;

View File

@ -45,7 +45,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of log entries to retain for this job" ResourceKey="RetentionLog">Retention Log (Items): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retentionHistory" maxlength="4" required />
<input id="retention" type="number" min="0" step ="1" class="form-control" @bind="@_retentionHistory" maxlength="4" required />
</div>
</div>
<div class="row mb-1 align-items-center">

View File

@ -131,8 +131,6 @@ else
var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
NavigationManager.NavigateTo(NavigationManager.Uri, true);
}
}

View File

@ -0,0 +1,110 @@
@namespace Oqtane.Modules.Admin.Languages
@inherits ModuleBase
@using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject NavigationManager NavigationManager
@inject ILocalizationService LocalizationService
@inject ILanguageService LanguageService
@inject IPackageService PackageService
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_code == null)
{
<p><em>@SharedLocalizer["Loading"]</em></p>
}
else
{
<TabStrip>
<TabPanel Name="Manage" ResourceKey="Manage" Heading="Manage">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Name Of The Language" ResourceKey="Name">Name:</Label>
<div class="col-sm-9">
<input id="code" class="form-control" @bind="@_code" readonly/>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="default" HelpText="Indicates Whether Or Not This Language Is The Default For The Site" ResourceKey="IsDefault">Default?</Label>
<div class="col-sm-9">
<select id="default" class="form-select" @bind="@_default" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<button type="button" class="btn btn-success" @onclick="SaveLanguage">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</form>
</TabPanel>
</TabStrip>
}
@code {
private ElementReference form;
private bool validated = false;
private int _languageId = -1;
private string _code = string.Empty;
private string _cultureName = string.Empty;
private string _default = "False";
private List<Language> _languages;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnInitializedAsync()
{
_languageId = Int32.Parse(PageState.QueryString["id"]);
_languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId, Constants.ClientId);
Language language = _languages.Where(x => x.LanguageId == _languageId).FirstOrDefault();
if (language != null)
{
_code = language.Code;
_cultureName = language.Name;
_default = language.IsDefault.ToString();
if (language.SiteId == null)
{
language.SiteId = PageState.Site.SiteId;
}
};
}
private async Task SaveLanguage()
{
Language language = _languages.Where(x => x.LanguageId == _languageId).FirstOrDefault();
if (language != null)
{
language.IsDefault = Boolean.Parse(_default);
try
{
await LanguageService.EditLanguageAsync(language);
if (language.IsDefault)
{
await SetCultureAsync(language.Code);
}
await logger.LogInformation("Language Edited {Language}", language);
NavigationManager.NavigateTo(NavigateUrl());
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Editing Language {Language} {Error}", language, ex.Message);
AddModuleMessage(Localizer["Error.Language.Edit"], MessageType.Error);
}
};
}
private async Task SetCultureAsync(string culture)
{
if (culture != CultureInfo.CurrentUICulture.Name)
{
var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360);
}
}
}

View File

@ -16,6 +16,7 @@ else
<Pager Items="@_languages" SearchProperties="Name,Code">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
@ -27,6 +28,12 @@ else
}
</Header>
<Row>
<td>
@if (!context.IsDefault)
{
<ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.LanguageId.ToString())" ResourceKey="EditLanguage" />
}
</td>
<td><ActionDialog Header="Delete Language" Message="@string.Format(Localizer["Confirm.Language.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteLanguage(context))" Disabled="@((context.IsDefault && _languages.Count > 2) || context.Code == Constants.DefaultCulture)" ResourceKey="DeleteLanguage" /></td>
<td>@context.Name</td>
<td>@context.Code</td>

View File

@ -38,10 +38,10 @@
<div class="form-group mt-2">
@if (!_alwaysremember)
{
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
@ -99,6 +99,7 @@
{
_allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
_allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
_alwaysremember = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AlwaysRemember", "false"));
_togglepassword = SharedLocalizer["ShowPassword"];
@ -155,10 +156,6 @@
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
}
}
if (PageState.Site.Settings.TryGetValue("LoginOptions:AlwaysRemember", out string alwaysRememberStr))
{
_alwaysremember = Convert.ToBoolean(alwaysRememberStr);
}
}
catch (Exception ex)
{
@ -194,13 +191,7 @@
if (!twofactor)
{
bool alwaysRemember = false;
if (PageState.Site.Settings.TryGetValue("LoginOptions:AlwaysRemember", out string alwaysRememberStr))
{
alwaysRemember = Convert.ToBoolean(alwaysRememberStr);
}
bool remember = alwaysRemember || _remember;
_remember = remember;
_remember = _alwaysremember || _remember;
user = await UserService.LoginUserAsync(user, hybrid, _remember);
}
else

View File

@ -81,7 +81,7 @@ else
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of events to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
<input id="retention" type="number" min="0" step="1" class="form-control" @bind="@_retention" />
</div>
</div>
</div>
@ -97,7 +97,7 @@ else
private string _rows = "10";
private int _page = 1;
private List<Log> _logs;
private string _retention = "";
private int _retention = 30;
public override string UrlParametersTemplate => "/{level}/{function}/{rows}/{page}";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host;
@ -126,7 +126,7 @@ else
await GetLogs();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = SettingService.GetSetting(settings, "LogRetention", "30");
_retention = int.Parse( SettingService.GetSetting(settings, "LogRetention", "30"));
}
catch (Exception ex)
{
@ -218,7 +218,7 @@ else
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "LogRetention", _retention, true);
settings = SettingService.SetSetting(settings, "LogRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);

View File

@ -8,6 +8,8 @@
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IPageModuleService PageModuleService
@inject IModuleService ModuleService
@if (_initialized)
{
@ -131,10 +133,22 @@
<button type="button" class="btn btn-success" @onclick="SaveModuleDefinition">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
</TabPanel>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
<Pager Items="@_pagesWithModules" RowClass="align-middle">
<Header>
<th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th>
</Header>
<Row>
<td><button type="button" class="btn btn-secondary" @onclick="@(async () => NavigationManager.NavigateTo(Browse(context)))">@Localizer["Browse"]</button></td>
<td>@(string.IsNullOrEmpty(context.Title) ? @context.Name : @context.Title )</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Translations" ResourceKey="Translations" Heading="Translations">
@if (_languages != null && _languages.Count > 0)
{
<Pager Items="@_languages">
<Pager Items="@_languages">
<Header>
<th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Code"]</th>
@ -240,6 +254,7 @@
private DateTime _createdon;
private string _modifiedby;
private DateTime _modifiedon;
private List<Page> _pagesWithModules;
#pragma warning disable 649
private PermissionGrid _permissionGrid;
@ -291,6 +306,18 @@
_languages = _languages.OrderBy(item => item.Name).ToList();
}
// Group modules by PageId
// Get distinct PageIds where modules are present
var distinctPageIds = PageState.Modules
.Where(md => md.ModuleDefinition.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false)
.Select(md => md.PageId)
.Distinct();
// Filter and retrieve the corresponding pages
_pagesWithModules = PageState.Pages
.Where(pg => distinctPageIds.Contains(pg.PageId) && pg.IsDeleted == false)
.ToList();
_initialized = true;
}
}
@ -439,5 +466,5 @@
AddModuleMessage(Localizer["Error.Validate"], MessageType.Error);
}
}
private string Browse(Page page) => string.IsNullOrEmpty(page.Url) ? NavigateUrl(page.Path) : page.Url;
}

View File

@ -306,13 +306,15 @@
private void ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
// if theme chosen is different than default site theme, display warning message to user
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
}
}

View File

@ -447,13 +447,15 @@
private void ThemeChanged(ChangeEventArgs e)
{
_themetype = (string)e.Value;
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
// if theme chosen is different than default site theme, display warning message to user
if (ThemeService.GetTheme(PageState.Site.Themes, _themetype)?.ThemeName != ThemeService.GetTheme(PageState.Site.Themes, PageState.Site.DefaultThemeType)?.ThemeName)
{
AddModuleMessage(Localizer["ThemeChanged.Message"], MessageType.Warning);
_containers = ThemeService.GetContainerControls(PageState.Site.Themes, _themetype);
_containertype = _containers.First().TypeName;
ThemeSettings();
StateHasChanged();
}
}

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Admin.Register
@using System.Net
@inherits ModuleBase
@inject NavigationManager NavigationManager
@inject IUserService UserService
@ -88,9 +89,9 @@ else
}
protected override void OnParametersSet()
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
{
_togglepassword = SharedLocalizer["ShowPassword"];
}
private async Task Register()
{
@ -120,7 +121,14 @@ else
if (user != null)
{
await logger.LogInformation("User Created {Username} {Email}", _username, _email);
AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info);
if (PageState.QueryString.ContainsKey("returnurl"))
{
NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"]));
}
else // legacy behavior
{
AddModuleMessage(Localizer["Info.User.AccountCreate"], MessageType.Info);
}
}
else
{

View File

@ -74,7 +74,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logo" HelpText="Specify a logo for the site" ResourceKey="Logo">Logo: </Label>
<div class="col-sm-9">
<FileManager FileId="@_logofileid" Filter="@Constants.ImageFiles" @ref="_logofilemanager" />
<FileManager FileId="@_logofileid" Filter="@_ImageFiles" @ref="_logofilemanager" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -119,6 +119,22 @@
</div>
</div>
</Section>
<Section Name="FileExtensions" Heading="File Extensions" ResourceKey="FileExtensions">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_ImageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_UploadableFiles" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
<div class="container">
<div class="row mb-1 align-items-center">
@ -207,7 +223,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of notifications to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
<button type="button" class="btn btn-secondary" @onclick="SendEmail">@Localizer["Smtp.TestConfig"]</button>
@ -295,12 +311,11 @@
<Section Name="Hosting" Heading="Hosting Model" ResourceKey="Hosting">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="runtime" HelpText="The Blazor runtime hosting model" ResourceKey="Runtime">Runtime: </Label>
<Label Class="col-sm-3" For="runtime" HelpText="The Blazor runtime hosting model for the site" ResourceKey="Runtime">Runtime: </Label>
<div class="col-sm-9">
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
<option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select>
</div>
</div>
@ -313,6 +328,15 @@
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="hybridenabled" HelpText="Specifies if the site can be integrated with an external .NET MAUI hybrid application" ResourceKey="HybridEnabled">Hybrid Enabled? </Label>
<div class="col-sm-9">
<select id="hybridenabled" class="form-select" @bind="@_hybridenabled" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
</Section>
<Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
@ -378,7 +402,9 @@
private string _smtpsender = string.Empty;
private string _smtprelay = "False";
private string _smtpenabled = "True";
private string _retention = string.Empty;
private string _ImageFiles = string.Empty;
private string _UploadableFiles = string.Empty;
private int _retention = 30;
private string _pwaisenabled;
private int _pwaappiconfileid = -1;
private FileManager _pwaappiconfilemanager;
@ -390,6 +416,7 @@
private string _defaultalias;
private string _runtime = "";
private string _prerender = "";
private string _hybridenabled = "";
private string _tenant = string.Empty;
private string _database = string.Empty;
private string _connectionstring = string.Empty;
@ -415,7 +442,7 @@
_homepageid = site.HomePageId.Value.ToString();
}
_isdeleted = site.IsDeleted.ToString();
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/pages/sitemap.xml";
_sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml";
_siteguid = site.SiteGuid;
_version = site.Version;
@ -461,7 +488,13 @@
_smtpsender = SettingService.GetSetting(settings, "SMTPSender", string.Empty);
_smtprelay = SettingService.GetSetting(settings, "SMTPRelay", "False");
_smtpenabled = SettingService.GetSetting(settings, "SMTPEnabled", "True");
_retention = SettingService.GetSetting(settings, "NotificationRetention", "30");
_retention = int.Parse(SettingService.GetSetting(settings, "NotificationRetention", "30"));
// file extensions
_ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles;
_UploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_UploadableFiles = (string.IsNullOrEmpty(_UploadableFiles)) ? Constants.UploadableFiles : _UploadableFiles;
// aliases
await GetAliases();
@ -469,6 +502,7 @@
// hosting model
_runtime = site.Runtime;
_prerender = site.RenderMode.Replace(_runtime, "");
_hybridenabled = site.HybridEnabled.ToString();
// database
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
@ -578,34 +612,35 @@
}
// PWA
if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{
site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
reload = true; // needs to be reloaded on server
}
int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
if (pwaappiconfileid == -1) pwaappiconfileid = null;
if (site.PwaAppIconFileId != pwaappiconfileid)
{
site.PwaAppIconFileId = pwaappiconfileid;
reload = true; // needs to be reloaded on server
}
int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
if (site.PwaSplashIconFileId != pwasplashiconfileid)
{
site.PwaSplashIconFileId = pwasplashiconfileid;
reload = true; // needs to be reloaded on server
}
if (site.PwaIsEnabled.ToString() != _pwaisenabled)
{
site.PwaIsEnabled = Boolean.Parse(_pwaisenabled);
reload = true; // needs to be reloaded on server
}
int? pwaappiconfileid = _pwaappiconfilemanager.GetFileId();
if (pwaappiconfileid == -1) pwaappiconfileid = null;
if (site.PwaAppIconFileId != pwaappiconfileid)
{
site.PwaAppIconFileId = pwaappiconfileid;
reload = true; // needs to be reloaded on server
}
int? pwasplashiconfileid = _pwasplashiconfilemanager.GetFileId();
if (pwasplashiconfileid == -1) pwasplashiconfileid = null;
if (site.PwaSplashIconFileId != pwasplashiconfileid)
{
site.PwaSplashIconFileId = pwasplashiconfileid;
reload = true; // needs to be reloaded on server
}
// hosting model
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender)
if (site.Runtime != _runtime || site.RenderMode != _runtime + _prerender || site.HybridEnabled != bool.Parse(_hybridenabled))
{
site.Runtime = _runtime;
site.RenderMode = _runtime + _prerender;
reload = true; // needs to be reloaded on server
site.HybridEnabled = bool.Parse(_hybridenabled);
reload = true; // needs to be reloaded on serve
}
}
@ -622,7 +657,12 @@
settings = SettingService.SetSetting(settings, "SMTPRelay", _smtprelay, true);
settings = SettingService.SetSetting(settings, "SMTPEnabled", _smtpenabled, true);
settings = SettingService.SetSetting(settings, "SiteGuid", _siteguid, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention, true);
settings = SettingService.SetSetting(settings, "NotificationRetention", _retention.ToString(), true);
// file extensions
settings = SettingService.SetSetting(settings, "ImageFiles", (_ImageFiles != Constants.ImageFiles) ? _ImageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_UploadableFiles != Constants.UploadableFiles) ? _UploadableFiles.Replace(" ", "") : "", false);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await logger.LogInformation("Site Settings Saved {Site}", site);

View File

@ -76,7 +76,6 @@ else
<select id="runtime" class="form-select" @bind="@_runtime" required>
<option value="Server">@SharedLocalizer["BlazorServer"]</option>
<option value="WebAssembly">@SharedLocalizer["BlazorWebAssembly"]</option>
<option value="Hybrid">@SharedLocalizer["BlazorHybrid"]</option>
</select>
</div>
</div>

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Modules.Admin.UserProfile
@using System.Net
@using System.Text.RegularExpressions;
@inherits ModuleBase
@inject NavigationManager NavigationManager
@ -76,7 +77,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="@photofileid.ToString()" HelpText="A photo of yourself" ResourceKey="Photo"></Label>
<div class="col-sm-9">
<FileManager FileId="@photofileid" Filter="@Constants.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" />
<FileManager FileId="@photofileid" Filter="@PageState.Site.ImageFiles" ShowFolders="false" ShowFiles="true" UploadMultiple="false" FolderId="@folderid" @ref="filemanager" />
</div>
</div>
</div>
@ -310,7 +311,7 @@
private int folderid = -1;
private int photofileid = -1;
private File photo = null;
private string _ImageFiles = string.Empty;
private List<Profile> profiles;
private Dictionary<string, string> settings;
private string category = string.Empty;
@ -337,6 +338,11 @@
email = PageState.User.Email;
displayname = PageState.User.DisplayName;
if (string.IsNullOrEmpty(email))
{
AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning);
}
// get user folder
var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath);
if (folder != null)
@ -354,6 +360,8 @@
photofileid = -1;
photo = null;
}
var sitesettings = await SettingService.GetSiteSettingsAsync(SiteState.Alias.SiteId);
_ImageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
settings = await SettingService.GetUserSettingsAsync(PageState.User.UserId);
@ -427,8 +435,15 @@
await SettingService.UpdateUserSettingsAsync(settings, PageState.User.UserId);
await logger.LogInformation("User Profile Saved");
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
if (PageState.QueryString.ContainsKey("returnurl"))
{
NavigationManager.NavigateTo(WebUtility.UrlDecode(PageState.QueryString["returnurl"]));
}
else // legacy behavior
{
AddModuleMessage(Localizer["Success.Profile.Update"], MessageType.Success);
StateHasChanged();
}
}
else
{

View File

@ -195,7 +195,7 @@ else
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="The external login provider name which will be displayed on the login page" ResourceKey="ProviderName">Provider Name:</Label>
<Label Class="col-sm-3" For="providername" HelpText="Specify a friendly name for the external login provider which will be displayed on the Login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
@ -254,7 +254,25 @@ else
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authresponsetype" HelpText="Specify the authorization response type. The default is Authorization Code which is considered to be the most secure option based on the latest OAuth specification." ResourceKey="AuthResponseType">Authorization Response Type:</Label>
<div class="col-sm-9">
<select id="authresponsetype" class="form-select" @bind="@_authresponsetype" required>
<option value="code">@Localizer["AuthFlow.Code"]</option>
<option value="code id_token">@Localizer["AuthFlow.CodeIdToken"]</option>
<option value="code id_token token">@Localizer["AuthFlow.CodeIdTokenToken"]</option>
<option value="code token">@Localizer["AuthFlow.CodeToken"]</option>
<option value="id_token">@Localizer["AuthFlow.IdToken"]</option>
<option value="id_token token">@Localizer["AuthFlow.IdTokenToken"]</option>
<option value="token">@Localizer["AuthFlow.Token"]</option>
<option value="none">@Localizer["AuthFlow.None"]</option>
</select>
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
@ -266,21 +284,6 @@ else
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authresponsetype" HelpText="Specify the authorization response type" ResourceKey="AuthResponseType">Authorization Response Type</Label>
<div class="col-sm-9">
<select id="authresponsetype" class="form-select" @bind="@_authresponsetype" required>
<option value="code">@Localizer["AuthFlow.Code"]</option>
<option value="code id_token">@Localizer["AuthFlow.CodeIdToken"]</option>
<option value="code id_token token">@Localizer["AuthFlow.CodeIdTokenToken"]</option>
<option value="code token">@Localizer["AuthFlow.CodeToken"]</option>
<option value="id_token">@Localizer["AuthFlow.IdToken"]</option>
<option value="id_token token">@Localizer["AuthFlow.IdTokenToken"]</option>
<option value="token">@Localizer["AuthFlow.Token"]</option>
<option value="none">@Localizer["AuthFlow.None"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9">
@ -296,33 +299,51 @@ else
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="The name of the unique user identifier claim provided by the provider" ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="reviewclaims" HelpText="This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled." ResourceKey="ReviewClaims">Review Claims?</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="reviewclaims" class="form-select" @bind="@_reviewclaims" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
@if (_reviewclaims == "true")
{
<a href="@_externalloginurl" target="_blank" class="btn btn-secondary">@SharedLocalizer["Test"]</a>
}
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="identifierclaimtype" HelpText="Specify the type name of the unique user identifier claim provided by the provider. The default value is 'sub'." ResourceKey="IdentifierClaimType">Identifier Claim:</Label>
<div class="col-sm-9">
<input id="identifierclaimtype" class="form-control" @bind="@_identifierclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The name of the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="nameclaimtype" HelpText="Optionally specify the type name of the user's name claim provided by the provider. The typical value is 'name'." ResourceKey="NameClaimType">Name Claim:</Label>
<div class="col-sm-9">
<input id="nameclaimtype" class="form-control" @bind="@_nameclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="Optionally specify the type name of the email address claim provided by the provider. The typical value is 'email'," ResourceKey="EmailClaimType">Email Claim:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
@if (_providertype == AuthenticationProviderTypes.OpenIDConnect)
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label>
<div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</div>
}
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
@ -351,10 +372,10 @@ else
</Section>
<Section Name="Token" Heading="Token Settings" ResourceKey="TokenSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="secret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<Label Class="col-sm-3" For="jwtsecret" HelpText="If you want to want to provide API access, please specify a secret which will be used to encrypt your tokens. The secret should be 16 characters or more to ensure optimal security. Please note that if you change this secret, all existing tokens will become invalid and will need to be regenerated." ResourceKey="Secret">Secret:</Label>
<div class="col-sm-9">
<div class="input-group">
<input type="@_secrettype" id="secret" class="form-control" @bind="@_secret" />
<input type="@_secrettype" id="jwtsecret" class="form-control" @bind="@_secret" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleSecret">@_togglesecret</button>
</div>
</div>
@ -425,12 +446,15 @@ else
private string _clientsecret;
private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty;
private string _authresponsetype;
private string _scopes;
private string _parameters;
private string _pkce;
private string _authresponsetype;
private string _redirecturl;
private string _reviewclaims;
private string _externalloginurl;
private string _identifierclaimtype;
private string _nameclaimtype;
private string _emailclaimtype;
private string _roleclaimtype;
private string _profileclaimtypes;
@ -486,12 +510,15 @@ else
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false");
_externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external");
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
@ -578,19 +605,20 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
if (!string.IsNullOrEmpty(_secret) && _secret.Length < 16) _secret = (_secret + "????????????????").Substring(0, 16);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);

View File

@ -78,7 +78,7 @@ else
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of visitor activity to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" @bind="@_retention" />
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -104,7 +104,7 @@ else
private List<Visitor> _visitors;
private string _tracking;
private string _filter = "";
private string _retention = "";
private int _retention = 30;
private string _correlation = "true";
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -129,7 +129,7 @@ else
_tracking = PageState.Site.VisitorTracking.ToString();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter);
_retention = SettingService.GetSetting(settings, "VisitorRetention", "30");
_retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30"));
_correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true");
}
@ -180,7 +180,7 @@ else
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention, true);
settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true);
settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);

View File

@ -3,6 +3,7 @@
@inherits ModuleControlBase
@inject IFolderService FolderService
@inject IFileService FileService
@inject ISettingService SettingService
@inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@ -83,6 +84,14 @@
}
}
}
@if (!string.IsNullOrEmpty(_message))
{
<div class="row mt-1">
<div class="col">
<ModuleMessage Message="@_message" Type="@_messagetype" />
</div>
</div>
}
</div>
</div>
@if (_image != string.Empty)
@ -92,14 +101,6 @@
</div>
}
</div>
@if (!string.IsNullOrEmpty(_message))
{
<div class="row mt-1">
<div class="col">
<ModuleMessage Message="@_message" Type="@_messagetype" />
</div>
</div>
}
</div>
}
@ -343,6 +344,7 @@
_message = string.Empty;
var interop = new Interop(JSRuntime);
var uploads = await interop.GetFiles(_fileinputid);
if (uploads.Length > 0)
{
string restricted = "";
@ -350,7 +352,7 @@
{
var filename = upload.Split(':')[0];
var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : "";
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower()))
{
restricted += (restricted == "" ? "" : ",") + extension;
}

View File

@ -1,79 +1,86 @@
@using System.Text.RegularExpressions
@namespace Oqtane.Modules.Controls
@inherits ModuleControlBase
@inject ISettingService SettingService
@inject IStringLocalizer<RichTextEditor> Localizer
<div class="row" style="margin-bottom: 50px;">
<div class="col">
<TabStrip>
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowRawHtml)
{
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>@((MarkupString)"&nbsp;&nbsp;")
}
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement">
</div>
</div>
</div>
</TabPanel>
@if (AllowRichText)
{
<TabPanel Name="Rich" Heading="Rich Text Editor" ResourceKey="RichTextEditor">
@if (_richfilemanager)
{
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
<div class="d-flex justify-content-center mb-2">
@if (AllowRawHtml)
{
<button type="button" class="btn btn-secondary" @onclick="RefreshRichText">@Localizer["SynchronizeContent"]</button>
@((MarkupString)"&nbsp;&nbsp;")
}
@if (AllowFileManagement)
{
<button type="button" class="btn btn-primary" @onclick="InsertRichImage">@Localizer["InsertImage"]</button>
}
@if (_richfilemanager)
{
@((MarkupString)"&nbsp;&nbsp;")
<button type="button" class="btn btn-secondary" @onclick="CloseRichFileManager">@Localizer["Close"]</button>
}
</div>
<div class="row">
<div class="col">
<div @ref="@_toolBar">
@if (ToolbarContent != null)
{
@ToolbarContent
}
else
{
<select class="ql-header">
<option selected=""></option>
<option value="1"></option>
<option value="2"></option>
<option value="3"></option>
<option value="4"></option>
<option value="5"></option>
</select>
<span class="ql-formats">
<button class="ql-bold"></button>
<button class="ql-italic"></button>
<button class="ql-underline"></button>
<button class="ql-strike"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered"></button>
<button class="ql-list" value="bullet"></button>
</span>
<span class="ql-formats">
<button class="ql-link"></button>
</span>
}
</div>
<div @ref="@_editorElement">
</div>
</div>
</div>
</TabPanel>
}
@if (AllowRawHtml)
{
<TabPanel Name="Raw" Heading="Raw HTML Editor" ResourceKey="HtmlEditor">
@if (_rawfilemanager)
{
<FileManager @ref="_fileManager" Filter="@Constants.ImageFiles" />
<FileManager @ref="_fileManager" Filter="@PageState.Site.ImageFiles" />
<ModuleMessage Message="@_message" Type="MessageType.Warning"></ModuleMessage>
<br />
}
@ -104,119 +111,130 @@
</div>
@code {
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
private FileManager _fileManager;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
private ElementReference _editorElement;
private ElementReference _toolBar;
private bool _richfilemanager = false;
private FileManager _fileManager;
private string _richhtml = string.Empty;
private string _originalrichhtml = string.Empty;
private bool _rawfilemanager = false;
private string _rawhtml = string.Empty;
private string _originalrawhtml = string.Empty;
private string _message = string.Empty;
[Parameter]
public string Content { get; set; }
[Parameter]
public string Content { get; set; }
[Parameter]
public bool ReadOnly { get; set; } = false;
[Parameter]
public bool ReadOnly { get; set; } = false;
[Parameter]
public string Placeholder { get; set; } = "Enter Your Content...";
[Parameter]
public string Placeholder { get; set; } = "Enter Your Content...";
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
public bool AllowFileManagement { get; set; } = true;
[Parameter]
public bool AllowRawHtml { get; set; } = true;
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
[Parameter]
public bool AllowRichText { get; set; } = true;
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public bool AllowRawHtml { get; set; } = true;
[Parameter]
public string DebugLevel { get; set; } = "info";
// parameters only applicable to rich text editor
[Parameter]
public RenderFragment ToolbarContent { get; set; }
public override List<Resource> Resources => new List<Resource>()
[Parameter]
public string Theme { get; set; } = "snow";
[Parameter]
public string DebugLevel { get; set; } = "info";
public override List<Resource> Resources => new List<Resource>()
{
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill.min.js" },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-blot-formatter.min.js" },
new Resource { ResourceType = ResourceType.Script, Bundle = "Quill", Url = "js/quill-interop.js" }
};
protected override void OnParametersSet()
{
_richhtml = Content;
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
}
protected override void OnParametersSet()
{
_richhtml = Content;
_rawhtml = Content;
_originalrawhtml = _rawhtml; // preserve for comparison later
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
var interop = new RichTextEditorInterop(JSRuntime);
var interop = new RichTextEditorInterop(JSRuntime);
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
if (firstRender)
{
await interop.CreateEditor(
_editorElement,
_toolBar,
ReadOnly,
Placeholder,
Theme,
DebugLevel);
await interop.LoadEditorContent(_editorElement, _richhtml);
await interop.LoadEditorContent(_editorElement, _richhtml);
// preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement);
}
}
if (AllowRichText)
{
// preserve a copy of the rich text content (Quill sanitizes content so we need to retrieve it from the editor)
_originalrichhtml = await interop.GetHtml(_editorElement);
}
}
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRichFileManager()
{
_richfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void CloseRawFileManager()
{
_rawfilemanager = false;
_message = string.Empty;
StateHasChanged();
}
public void RefreshRichText()
{
_richhtml = _rawhtml;
StateHasChanged();
}
public void RefreshRichText()
{
_richhtml = _rawhtml;
StateHasChanged();
}
public async Task RefreshRawHtml()
{
var interop = new RichTextEditorInterop(JSRuntime);
_rawhtml = await interop.GetHtml(_editorElement);
StateHasChanged();
}
public async Task RefreshRawHtml()
{
var interop = new RichTextEditorInterop(JSRuntime);
_rawhtml = await interop.GetHtml(_editorElement);
StateHasChanged();
}
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
// return rich text content if it has changed
var interop = new RichTextEditorInterop(JSRuntime);
var richhtml = await interop.GetHtml(_editorElement);
if (richhtml != _originalrichhtml)
public async Task<string> GetHtml()
{
// evaluate raw html content as first priority
if (_rawhtml != _originalrawhtml)
{
return _rawhtml;
}
else
{
var richhtml = "";
if (AllowRichText)
{
// return rich text content if it has changed
var interop = new RichTextEditorInterop(JSRuntime);
richhtml = await interop.GetHtml(_editorElement);
}
// rich text value will only be blank if AllowRichText is disabled or the JS Interop method failed
if (richhtml != _originalrichhtml && !string.IsNullOrEmpty(richhtml) && !string.IsNullOrEmpty(_originalrichhtml))
{
return richhtml;
}

View File

@ -1,27 +1,30 @@
@namespace Oqtane.Modules.Controls
@inherits LocalizableComponent
<div class="d-flex mt-2">
<div>
<a data-bs-toggle="collapse" class="app-link-unstyled" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<h5>@_heading</h5>
</a>
@if (IsVisible)
{
<div class="d-flex mt-2">
<div>
<a data-bs-toggle="collapse" class="app-link-unstyled" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<h5>@_heading</h5>
</a>
</div>
<div class="ms-auto">
<a data-bs-toggle="collapse" class="app-link-unstyled float-right" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<i class="oi oi-chevron-bottom"></i>&nbsp;
</a>
</div>
</div>
<div class="ms-auto">
<a data-bs-toggle="collapse" class="app-link-unstyled float-right" href="#@Name" aria-expanded="@_expanded" aria-controls="@Name" @onclick:preventDefault="true">
<i class="oi oi-chevron-bottom"></i>&nbsp;
</a>
<div class="d-flex">
<hr class="app-rule" />
</div>
</div>
<div class="d-flex">
<hr class="app-rule" />
</div>
<div class="collapse @_show" id="@Name">
@if (ChildContent != null)
{
@ChildContent
}
</div>
<div class="collapse @_show" id="@Name">
@if (ChildContent != null)
{
@ChildContent
}
</div>
}
@code {
private string _heading = string.Empty;
@ -40,6 +43,9 @@
[Parameter]
public string Expanded { get; set; } // optional - will default to false if not provided
[Parameter]
public bool IsVisible { get; set; } = true;
protected override void OnParametersSet()
{
base.OnParametersSet(); // must be included to call method in LocalizableComponent

View File

@ -261,7 +261,12 @@ namespace Oqtane.Modules
// UI methods
public void AddModuleMessage(string message, MessageType type)
{
ModuleInstance.AddModuleMessage(message, type);
AddModuleMessage(message, type, "top");
}
public void AddModuleMessage(string message, MessageType type, string position)
{
ModuleInstance.AddModuleMessage(message, type, position);
}
public void ClearModuleMessage()

View File

@ -4,7 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace>

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Error.Language.Edit" xml:space="preserve">
<value>Error Updating Language</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Name Of The Langauage</value>
</data>
<data name="IsDefault.HelpText" xml:space="preserve">
<value>Indicates Whether Or Not This Language Is The Default For The Site</value>
</data>
<data name="Name.Text" xml:space="preserve">
<value>Name:</value>
</data>
<data name="IsDefault.Text" xml:space="preserve">
<value>Default?</value>
</data>
<data name="Success.Language.Download" xml:space="preserve">
<value>Translation Package Saved Successfully. You Must &lt;a href={0}&gt;Restart&lt;/a&gt; To Complete The Installation.</value>
</data>
<data name="LanguageUpload.HelpText" xml:space="preserve">
<value>Upload one or more translation packages.</value>
</data>
<data name="LanguageUpload.Text" xml:space="preserve">
<value>Translation</value>
</data>
<data name="Manage.Heading" xml:space="preserve">
<value>Manage</value>
</data>
<data name="Upload.Heading" xml:space="preserve">
<value>Upload</value>
</data>
<data name="Error.Language.Load" xml:space="preserve">
<value>Error Loading Language</value>
</data>
</root>

View File

@ -204,8 +204,8 @@
<data name="ExternalLoginStatus.DuplicateEmail" xml:space="preserve">
<value>Multiple User Accounts Already Exist With The Email Address Of Your External Login. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.InvalidEmail" xml:space="preserve">
<value>The External Login Provider Did Not Provide A Valid Email Address For Your Account. Please Contact Your Administrator For Further Instructions.</value>
<data name="ExternalLoginStatus.MissingClaims" xml:space="preserve">
<value>The External Login Provider Did Not Provide All Of The Required Information. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.ProviderKeyMismatch" xml:space="preserve">
<value>An Error Occurred Verifying Your External Login. Please Contact Your Administrator For Further Instructions.</value>
@ -225,4 +225,7 @@
<data name="ExternalLoginStatus.RemoteFailure" xml:space="preserve">
<value>Your External Login Failed. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="ExternalLoginStatus.ReviewClaims" xml:space="preserve">
<value>The Review Claims Option Was Enabled In External Login Settings. Please Visit The Event Log To View The Claims Returned By The Provider.</value>
</data>
</root>

View File

@ -240,4 +240,10 @@
<data name="Validate" xml:space="preserve">
<value>Validate</value>
</data>
<data name="Browse" xml:space="preserve">
<value>Browse</value>
</data>
<data name="Pages.Heading" xml:space="preserve">
<value>Pages</value>
</data>
</root>

View File

@ -127,7 +127,7 @@
<value>Error Downloading Module</value>
</data>
<data name="Confirm.Module.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete The {0} Module?</value>
<value>Are You Sure You Wish To Uninstall The {0} Module?</value>
</data>
<data name="Error.Module.Load" xml:space="preserve">
<value>Error Loading Modules</value>
@ -142,10 +142,10 @@
<value>Install Module</value>
</data>
<data name="DeleteModule.Header" xml:space="preserve">
<value>Delete Module</value>
<value>Uninstall Module</value>
</data>
<data name="DeleteModule.Text" xml:space="preserve">
<value>Delete</value>
<value>Uninstall</value>
</data>
<data name="InUse" xml:space="preserve">
<value>In Use?</value>

View File

@ -283,7 +283,7 @@
<value>Prerender? </value>
</data>
<data name="Runtime.HelpText" xml:space="preserve">
<value>The Blazor runtime hosting model</value>
<value>The Blazor runtime hosting model for the site</value>
</data>
<data name="Runtime.Text" xml:space="preserve">
<value>Runtime: </value>
@ -402,4 +402,25 @@
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="FileExtensions.Heading" xml:space="preserve">
<value>File Extensions</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="HybridEnabled.HelpText" xml:space="preserve">
<value>Specifies if the site can be integrated with an external .NET MAUI hybrid application</value>
</data>
<data name="HybridEnabled.Text" xml:space="preserve">
<value>Hybrid Enabled?</value>
</data>
</root>

View File

@ -124,7 +124,7 @@
<value>Error Downloading Theme</value>
</data>
<data name="Confirm.Theme.Delete" xml:space="preserve">
<value>Are You Sure You Wish To Delete The {0} Theme?</value>
<value>Are You Sure You Wish To Uninstall The {0} Theme?</value>
</data>
<data name="Error.Theme.Load" xml:space="preserve">
<value>Error Loading Themes</value>
@ -136,10 +136,10 @@
<value>Error Deleting Theme</value>
</data>
<data name="DeleteTheme.Header" xml:space="preserve">
<value>Delete Theme</value>
<value>Uninstall Theme</value>
</data>
<data name="DeleteTheme.Text" xml:space="preserve">
<value>Delete</value>
<value>Uninstall</value>
</data>
<data name="CreateTheme.Text" xml:space="preserve">
<value>Create Theme</value>

View File

@ -147,6 +147,9 @@
<data name="Message.User.NoLogIn" xml:space="preserve">
<value>Current User Is Not Logged In</value>
</data>
<data name="Message.User.NoEmail" xml:space="preserve">
<value>You Must Provide An Email Address For Your User Account</value>
</data>
<data name="Error.Profile.Load" xml:space="preserve">
<value>Error Loading User Profile</value>
</data>

View File

@ -247,7 +247,7 @@
<value>Domain Filter:</value>
</data>
<data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The name of the email address claim provided by the identity provider</value>
<value>Optionally specify the type name of the email address claim provided by the identity provider. The typical value is 'email'.</value>
</data>
<data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim:</value>
@ -274,7 +274,7 @@
<value>Use PKCE?</value>
</data>
<data name="ProviderName.HelpText" xml:space="preserve">
<value>The external login provider name which will be displayed on the login page</value>
<value>Specify a friendly name for the external login provider which will be displayed on the Login page</value>
</data>
<data name="ProviderName.Text" xml:space="preserve">
<value>Provider Name:</value>
@ -373,7 +373,7 @@
<value>Last Login</value>
</data>
<data name="IdentifierClaimType.HelpText" xml:space="preserve">
<value>The name of the unique user identifier claim provided by the identity provider</value>
<value>Specify the type name of the unique user identifier claim provided by the identity provider. The default value is 'sub'.</value>
</data>
<data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value>
@ -385,13 +385,13 @@
<value>Parameters:</value>
</data>
<data name="RoleClaimType.HelpText" xml:space="preserve">
<value>Optionally provide the name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
<value>Optionally provide the type name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value>
</data>
<data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim:</value>
</data>
<data name="ProfileClaimTypes.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of user profile claims provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>
<value>Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>
</data>
<data name="ProfileClaimTypes.Text" xml:space="preserve">
<value>User Profile Claims:</value>
@ -409,31 +409,34 @@
<value>Import Users</value>
</data>
<data name="AuthFlow.Code" xml:space="preserve">
<value>code</value>
<value>Authorization Code</value>
</data>
<data name="AuthFlow.CodeIdToken" xml:space="preserve">
<value>code id_token</value>
<value>Authorization Code + ID Token</value>
</data>
<data name="AuthFlow.CodeIdTokenToken" xml:space="preserve">
<value>code id_token token</value>
<value>Authorization Code + ID Token + Access Token</value>
</data>
<data name="AuthFlow.CodeToken" xml:space="preserve">
<value>code token</value>
<value>Authorization Code + Access Token</value>
</data>
<data name="AuthFlow.IdToken" xml:space="preserve">
<value>id_token</value>
<value>ID Token</value>
</data>
<data name="AuthFlow.IdTokenToken" xml:space="preserve">
<value>id_token token</value>
<value>ID Token + Access Token</value>
</data>
<data name="AuthFlow.None" xml:space="preserve">
<value>none</value>
<value>None</value>
</data>
<data name="AuthFlow.Token" xml:space="preserve">
<value>token</value>
<value>Access Token</value>
</data>
<data name="AuthResponseType" xml:space="preserve">
<value>Authorization Response Type</value>
<data name="AuthResponseType.Text" xml:space="preserve">
<value>Authorization Response Type:</value>
</data>
<data name="AuthResponseType.HelpText" xml:space="preserve">
<value>Specify the authorization response type. The default is Authorization Code which is considered to be the most secure option based on the latest OAuth specification.</value>
</data>
<data name="VerifyUsers.HelpText" xml:space="preserve">
<value>Do you want existing users to perform an additional email verification step to link their external login? If you disable this option, existing users will be linked automatically.</value>
@ -453,4 +456,16 @@
<data name="CookieExpiration.Text" xml:space="preserve">
<value>Cookie Expiration Timespan:</value>
</data>
<data name="ReviewClaims.Text" xml:space="preserve">
<value>Review Claims?</value>
</data>
<data name="ReviewClaims.HelpText" xml:space="preserve">
<value>This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled.</value>
</data>
<data name="NameClaimType.HelpText" xml:space="preserve">
<value>Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'.</value>
</data>
<data name="NameClaimType.Text" xml:space="preserve">
<value>Name Claim:</value>
</data>
</root>

View File

@ -432,4 +432,10 @@
<data name="ProfileRequired" xml:space="preserve">
<value>{0} Is Required</value>
</data>
<data name="Uninstall" xml:space="preserve">
<value>Uninstall</value>
</data>
<data name="Test" xml:space="preserve">
<value>Test</value>
</data>
</root>

View File

@ -1,6 +1,6 @@
using Oqtane.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using Oqtane.Models;
namespace Oqtane.Services
{
@ -39,6 +39,13 @@ namespace Oqtane.Services
/// <returns></returns>
Task<Language> AddLanguageAsync(Language language);
/// <summary>
/// Edits the given language
/// </summary>
/// <param name="language"></param>
/// <returns></returns>
Task EditLanguageAsync(Language language);
/// <summary>
/// Deletes the given language
/// </summary>

View File

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
@ -10,7 +9,7 @@ namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class LanguageService : ServiceBase, ILanguageService
{
{
public LanguageService(HttpClient http, SiteState siteState) : base(http, siteState) { }
private string Apiurl => CreateApiUrl("Language");
@ -35,6 +34,11 @@ namespace Oqtane.Services
return await PostJsonAsync<Language>(Apiurl, language);
}
public async Task EditLanguageAsync(Language language)
{
await PutJsonAsync<Language>(Apiurl, language);
}
public async Task DeleteLanguageAsync(int languageId)
{
await DeleteAsync($"{Apiurl}/{languageId}");

View File

@ -26,8 +26,15 @@ namespace Oqtane.Themes.Controls
var allowexternallogin = (SettingService.GetSetting(PageState.Site.Settings, "ExternalLogin:ProviderType", "") != "") ? true : false;
var allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true"));
Route route = new Route(PageState.Uri.AbsoluteUri, PageState.Alias.Path);
var returnurl = WebUtility.UrlEncode(route.PathAndQuery);
var returnurl = "";
if (!PageState.QueryString.ContainsKey("returnurl"))
{
returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery); // remember current url
}
else
{
returnurl = PageState.QueryString["returnurl"]; // use existing value
}
if (allowexternallogin && !allowsitelogin)
{
@ -39,7 +46,6 @@ namespace Oqtane.Themes.Controls
// local login
NavigationManager.NavigateTo(NavigateUrl("login", "?returnurl=" + returnurl));
}
}
protected async Task LogoutUser()

View File

@ -1,4 +1,5 @@
@namespace Oqtane.Themes.Controls
@using System.Net
@inherits ThemeControlBase
@inject IStringLocalizer<UserProfile> Localizer
@ -26,14 +27,21 @@
[Parameter]
public bool ShowRegister { get; set; }
private string _returnurl = "";
protected override void OnParametersSet()
{
_returnurl = WebUtility.UrlEncode(PageState.Route.PathAndQuery);
}
private void RegisterUser()
{
NavigationManager.NavigateTo(NavigateUrl("register"));
NavigationManager.NavigateTo(NavigateUrl("register", "returnurl=" + _returnurl));
}
private void UpdateProfile()
{
NavigationManager.NavigateTo(NavigateUrl("profile"));
NavigationManager.NavigateTo(NavigateUrl("profile", "returnurl=" + _returnurl));
}
}

View File

@ -5,7 +5,10 @@
@if (CurrentException is null)
{
<ModuleMessage Message="@_message" Type="@_messageType"/>
if (_messagePosition == "top")
{
<ModuleMessage Message="@_message" Type="@_messageType" />
}
@if (ModuleType != null)
{
<DynamicComponent Type="@ModuleType" Parameters="@ModuleParameters"></DynamicComponent>
@ -14,6 +17,10 @@
<div class="app-progress-indicator"></div>
}
}
if (_messagePosition == "bottom")
{
<ModuleMessage Message="@_message" Type="@_messageType" />
}
}
else
{
@ -24,49 +31,58 @@ else
}
@code {
private string _message;
private string _error;
private MessageType _messageType;
private bool _progressIndicator = false;
private string _message;
private string _error;
private MessageType _messageType;
private string _messagePosition;
private bool _progressIndicator = false;
private Type ModuleType { get; set; }
private IDictionary<string, object> ModuleParameters { get; set; }
private Type ModuleType { get; set; }
private IDictionary<string, object> ModuleParameters { get; set; }
[CascadingParameter]
protected PageState PageState { get; set; }
[CascadingParameter]
protected PageState PageState { get; set; }
[CascadingParameter]
private Module ModuleState { get; set; }
[CascadingParameter]
private Module ModuleState { get; set; }
private ModuleMessage ModuleMessage { get; set; }
private ModuleMessage ModuleMessage { get; set; }
protected override void OnParametersSet()
{
_message = "";
if (!string.IsNullOrEmpty(ModuleState.ModuleType))
{
ModuleType = Type.GetType(ModuleState.ModuleType);
if (ModuleType != null)
{
ModuleParameters = new Dictionary<string, object> { { "ModuleInstance", this } };
return;
}
// module does not exist with typename specified
_message = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0));
_messageType = MessageType.Error;
}
else
{
_message = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName);
_messageType = MessageType.Error;
}
}
protected override void OnParametersSet()
{
_message = "";
if (!string.IsNullOrEmpty(ModuleState.ModuleType))
{
ModuleType = Type.GetType(ModuleState.ModuleType);
if (ModuleType != null)
{
ModuleParameters = new Dictionary<string, object> { { "ModuleInstance", this } };
return;
}
// module does not exist with typename specified
_message = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0));
_messageType = MessageType.Error;
_messagePosition = "top";
}
else
{
_message = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName);
_messageType = MessageType.Error;
_messagePosition = "top";
}
}
public void AddModuleMessage(string message, MessageType type)
{
_message = message;
_messageType = type;
_progressIndicator = false;
public void AddModuleMessage(string message, MessageType type)
{
AddModuleMessage(message, type, "top");
}
public void AddModuleMessage(string message, MessageType type, string position)
{
_message = message;
_messageType = type;
_messagePosition = position;
_progressIndicator = false;
StateHasChanged();
}

View File

@ -105,11 +105,18 @@
Route route = new Route(_absoluteUri, SiteState.Alias.Path);
int moduleid = (int.TryParse(route.ModuleId, out moduleid)) ? moduleid : -1;
var action = (!string.IsNullOrEmpty(route.Action)) ? route.Action : Constants.DefaultAction;
var querystring = Utilities.ParseQueryString(route.Query);
var returnurl = "";
if (querystring.ContainsKey("returnurl"))
{
returnurl = WebUtility.UrlDecode(querystring["returnurl"]);
if (!returnurl.StartsWith("/"))
{
// urls which are not relative are vulnerable to open redirects or XSS
returnurl = "";
querystring["returnurl"] = "";
}
}
// reload the client application from the server if there is a forced reload
@ -155,7 +162,8 @@
if (PageState == null || refresh || PageState.Alias.SiteId != SiteState.Alias.SiteId)
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated)
// verify user is authenticated for current site
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey))
{
user = await UserService.GetUserAsync(authState.User.Identity.Name, SiteState.Alias.SiteId);
if (user != null)

View File

@ -1,4 +1,5 @@
@namespace Oqtane.UI
@using System.Net
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager
@inject SiteState SiteState
@ -87,6 +88,13 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
if (!firstRender)
{
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))

View File

@ -1,12 +1,10 @@
using System.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases;
using Oqtane.Shared;
namespace Oqtane.Database.MySQL
{

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,14 +10,10 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
<PackageName>$(MSBuildProjectName).$(Version).nupkg</PackageName>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@ -27,6 +23,14 @@
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<OutputPath>bin</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.0-preview" />
@ -37,7 +41,12 @@
<ProjectReference Include="..\Oqtane.Server\Oqtane.Server.csproj" />
</ItemGroup>
<Target Name="CopyPackage" AfterTargets="Pack">
<Copy SourceFiles="$(OutputPath)..\$(PackageName)" DestinationFiles="..\Oqtane.Server\wwwroot\Packages\$(MSBuildProjectName).nupkg.bak" />
<ItemGroup>
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" />
</ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')">
<Copy SourceFiles="@(MySQLFiles)" DestinationFiles="@(MySQLFiles->'%(DestinationPath)')" />
</Target>
</Project>

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.MySQL</id>
<version>5.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane MySQL Provider</title>
<summary>MySQL database support for the Oqtane Framework</summary>
<description>MySQL database support for the Oqtane Framework</description>
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="bin\net8.0\Oqtane.Database.MySQL.dll" target="lib\net8.0" />
<file src="bin\net8.0\Oqtane.Database.MySQL.pdb" target="lib\net8.0" />
<file src="bin\net8.0\MySql.EntityFrameworkCore.dll" target="lib\net8.0" />
<file src="bin\net8.0\MySql.Data.dll" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,14 +10,10 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
<PackageName>$(MSBuildProjectName).$(Version).nupkg</PackageName>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@ -27,18 +23,31 @@
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<OutputPath>bin</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.0-rc.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0-rc.2" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Oqtane.Server\Oqtane.Server.csproj" />
</ItemGroup>
<Target Name="CopyPackage" AfterTargets="Pack">
<Copy SourceFiles="$(OutputPath)..\$(PackageName)" DestinationFiles="..\Oqtane.Server\wwwroot\Packages\$(MSBuildProjectName).nupkg.bak" />
<ItemGroup>
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" />
</ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(PostgreSQLFiles)" Outputs="@(PostgreSQLFiles->'%(DestinationPath)')">
<Copy SourceFiles="@(PostgreSQLFiles)" DestinationFiles="@(PostgreSQLFiles->'%(DestinationPath)')" />
</Target>
</Project>

View File

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.PostgreSQL</id>
<version>5.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane PostgreSQL Provider</title>
<summary>PostgreSQL database support for the Oqtane Framework</summary>
<description>PostgreSQL database support for the Oqtane Framework</description>
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="bin\net8.0\Oqtane.Database.PostgreSQL.dll" target="lib\net8.0" />
<file src="bin\net8.0\Oqtane.Database.PostgreSQL.pdb" target="lib\net8.0" />
<file src="bin\net8.0\EFCore.NamingConventions.dll" target="lib\net8.0" />
<file src="bin\net8.0\Npgsql.EntityFrameworkCore.PostgreSQL.dll" target="lib\net8.0" />
<file src="bin\net8.0\Npgsql.dll" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>

View File

@ -1,11 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Internal;
using Oqtane.Migrations.Framework;
using Oqtane.Models;
using Oqtane.Shared;
// ReSharper disable ClassNeverInstantiated.Global

View File

@ -9,7 +9,6 @@ using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Npgsql;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Oqtane.Databases;
using Oqtane.Shared;
namespace Oqtane.Database.PostgreSQL
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,14 +10,10 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
<PackageName>$(MSBuildProjectName).$(Version).nupkg</PackageName>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@ -28,6 +24,14 @@
<OutputPath>bin</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.0" />
</ItemGroup>
@ -36,8 +40,12 @@
<ProjectReference Include="..\Oqtane.Server\Oqtane.Server.csproj" />
</ItemGroup>
<Target Name="CopyPackage" AfterTargets="Pack">
<Copy SourceFiles="$(OutputPath)..\$(PackageName)" DestinationFiles="..\Oqtane.Server\wwwroot\Packages\$(MSBuildProjectName).nupkg.bak" />
<ItemGroup>
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" />
</ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqlServerFiles)" Outputs="@(SqlServerFiles->'%(DestinationPath)')">
<Copy SourceFiles="@(SqlServerFiles)" DestinationFiles="@(SqlServerFiles->'%(DestinationPath)')" />
</Target>
</Project>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.SqlServer</id>
<version>5.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQL Server Provider</title>
<summary>SQL Server database support for the Oqtane Framework</summary>
<description>SQL Server database support for the Oqtane Framework</description>
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="bin\net8.0\Oqtane.Database.SqlServer.dll" target="lib\net8.0" />
<file src="bin\net8.0\Oqtane.Database.SqlServer.pdb" target="lib\net8.0" />
<file src="bin\net8.0\Microsoft.EntityFrameworkCore.SqlServer.dll" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>

View File

@ -1,13 +1,9 @@
using System;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.SqlServer.Migrations.Internal;
using Microsoft.EntityFrameworkCore.Storage;
using Oqtane.Migrations.Framework;
using Oqtane.Models;
using Oqtane.Shared;
// ReSharper disable ClassNeverInstantiated.Global

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases;
using Oqtane.Shared;
namespace Oqtane.Database.SqlServer
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,14 +10,10 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<NuspecFile>$(MSBuildProjectName).nuspec</NuspecFile>
<PackageName>$(MSBuildProjectName).$(Version).nupkg</PackageName>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
@ -28,6 +24,14 @@
<OutputPath>bin</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
</ItemGroup>
@ -36,8 +40,12 @@
<ProjectReference Include="..\Oqtane.Server\Oqtane.Server.csproj" />
</ItemGroup>
<Target Name="CopyPackage" AfterTargets="Pack">
<Copy SourceFiles="$(OutputPath)..\$(PackageName)" DestinationFiles="..\Oqtane.Server\wwwroot\Packages\$(MSBuildProjectName).nupkg.bak" />
<ItemGroup>
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" />
</ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqliteFiles)" Outputs="@(SqliteFiles->'%(DestinationPath)')">
<Copy SourceFiles="@(SqliteFiles)" DestinationFiles="@(SqliteFiles->'%(DestinationPath)')" />
</Target>
</Project>

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Database.Sqlite</id>
<version>5.0.0</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane SQLite Provider</title>
<summary>SQLite database support for the Oqtane Framework</summary>
<description>SQLite database support for the Oqtane Framework</description>
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>
<files>
<file src="bin\net8.0\Oqtane.Database.Sqlite.dll" target="lib\net8.0" />
<file src="bin\net8.0\Oqtane.Database.Sqlite.pdb" target="lib\net8.0" />
<file src="bin\net8.0\Microsoft.EntityFrameworkCore.Sqlite.dll" target="lib\net8.0" />
<file src="icon.png" target="" />
</files>
</package>

View File

@ -1,11 +1,9 @@
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Sqlite.Migrations.Internal;
using Oqtane.Migrations.Framework;
using Oqtane.Models;
using Oqtane.Shared;
// ReSharper disable ClassNeverInstantiated.Global

View File

@ -6,7 +6,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using Oqtane.Databases;
using Oqtane.Shared;
namespace Oqtane.Database.Sqlite
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,56 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28822.285
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{77EECA8C-B58E-469E-B8C5-D543AFC9A654}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.gitignore = .gitignore
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.MySQL", "Oqtane.Database.MySQL\Oqtane.Database.MySQL.csproj", "{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.PostgreSQL", "Oqtane.Database.PostgreSQL\Oqtane.Database.PostgreSQL.csproj", "{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.Sqlite", "Oqtane.Database.Sqlite\Oqtane.Database.Sqlite.csproj", "{E4F50CA9-19A6-465A-9469-C033748AD95B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Database.SqlServer", "Oqtane.Database.SqlServer\Oqtane.Database.SqlServer.csproj", "{033DCA37-6354-4A3D-8250-4EC20740EE19}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "Oqtane.Server\Oqtane.Server.csproj", "{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A996FD2D-DAC8-4DFA-92B2-51DF32C6E014}.Release|Any CPU.Build.0 = Release|Any CPU
{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B29B35F-65E7-4819-9AED-EAC7FCFA309B}.Release|Any CPU.Build.0 = Release|Any CPU
{E4F50CA9-19A6-465A-9469-C033748AD95B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4F50CA9-19A6-465A-9469-C033748AD95B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4F50CA9-19A6-465A-9469-C033748AD95B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4F50CA9-19A6-465A-9469-C033748AD95B}.Release|Any CPU.Build.0 = Release|Any CPU
{033DCA37-6354-4A3D-8250-4EC20740EE19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{033DCA37-6354-4A3D-8250-4EC20740EE19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{033DCA37-6354-4A3D-8250-4EC20740EE19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{033DCA37-6354-4A3D-8250-4EC20740EE19}.Release|Any CPU.Build.0 = Release|Any CPU
{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A60C4DD-67E6-42A7-B9AA-A1EE45AD45C7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {1FB11796-35DE-4AED-9A52-17733557FCC4}
EndGlobalSection
EndGlobal

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<Version>5.0.0</Version>
<Version>5.0.1</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace>
@ -31,7 +31,7 @@
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>5.0.0</ApplicationDisplayVersion>
<ApplicationDisplayVersion>5.0.1</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Client</id>
<version>5.0.0</version>
<version>5.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Framework</id>
<version>5.0.0</version>
<version>5.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.0.0Oqtane.Framework.5.0.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.0.1/Oqtane.Framework.5.0.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane framework</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Server</id>
<version>5.0.0</version>
<version>5.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Shared</id>
<version>5.0.0</version>
<version>5.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>Oqtane.Updater</id>
<version>5.0.0</version>
<version>5.0.1</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.1</releaseNotes>
<icon>icon.png</icon>
<tags>oqtane</tags>
</metadata>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.0.Install.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.1.Install.zip" -Force

View File

@ -1,8 +1,6 @@
del "*.nupkg"
del "*.zip"
dotnet clean -c Release ..\Oqtane.Databases.sln
dotnet clean -c Release ..\Oqtane.sln
dotnet build -c Release ..\Oqtane.Databases.sln
dotnet build -c Release ..\Oqtane.sln
nuget.exe pack Oqtane.Client.nuspec
nuget.exe pack Oqtane.Server.nuspec

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.0.Upgrade.zip" -Force
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.0.1.Upgrade.zip" -Force

View File

@ -76,7 +76,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Host)]
public Alias Put(int id, [FromBody] Alias alias)
{
if (ModelState.IsValid && _aliases.GetAlias(alias.AliasId, false) != null)
if (ModelState.IsValid && alias.AliasId == id && _aliases.GetAlias(alias.AliasId, false) != null)
{
alias = _aliases.UpdateAlias(alias);
_syncManager.AddSyncEvent(alias.TenantId, EntityNames.Alias, alias.AliasId, SyncEventActions.Update);

View File

@ -35,8 +35,8 @@ namespace Oqtane.Controllers
private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
private readonly Alias _alias;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
private readonly ISettingRepository _settingRepository;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager)
{
_environment = environment;
_files = files;
@ -45,6 +45,7 @@ namespace Oqtane.Controllers
_syncManager = syncManager;
_logger = logger;
_alias = tenantManager.GetAlias();
_settingRepository = settingRepository;
}
// GET: api/<controller>?folder=x
@ -207,7 +208,7 @@ namespace Oqtane.Controllers
public Models.File Put(int id, [FromBody] Models.File file)
{
var File = _files.GetFile(file.FileId, false);
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && File != null // ensure file exists
if (ModelState.IsValid && file.Folder.SiteId == _alias.SiteId && file.FileId == id && File != null // ensure file exists
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, File.FolderId, PermissionNames.Edit) // ensure user had edit rights to original folder
&& _userPermissions.IsAuthorized(User, file.Folder.SiteId, EntityNames.Folder, file.FolderId, PermissionNames.Edit)) // ensure user has edit rights to new folder
{
@ -287,6 +288,8 @@ namespace Oqtane.Controllers
folder = _folders.GetFolder(FolderId);
}
var _UploadableFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue ?? Constants.UploadableFiles) ?? Constants.UploadableFiles;
if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, folder.PermissionList))
{
string folderPath = _folders.GetFolderPath(folder);
@ -297,7 +300,7 @@ namespace Oqtane.Controllers
name = url.Substring(url.LastIndexOf("/", StringComparison.Ordinal) + 1);
}
// check for allowable file extensions
if (!Constants.UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
if (!_UploadableFiles.Split(',').Contains(Path.GetExtension(name).ToLower().Replace(".", "")))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "File Could Not Be Downloaded From Url Due To Its File Extension {Url}", url);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Conflict;
@ -362,6 +365,10 @@ namespace Oqtane.Controllers
return;
}
// Get the UploadableFiles extensions
string uploadfilesSetting = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "UploadableFiles")?.SettingValue;
string _UploadableFiles = uploadfilesSetting ?? Constants.UploadableFiles;
// ensure filename is valid
string token = ".part_";
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token))
@ -371,7 +378,7 @@ namespace Oqtane.Controllers
// check for allowable file extensions (ignore token)
var extension = Path.GetExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))).Replace(".", "");
if (!Constants.UploadableFiles.Split(',').Contains(extension.ToLower()))
if (!_UploadableFiles.Split(',').Contains(extension.ToLower()))
{
return;
}
@ -604,9 +611,11 @@ namespace Oqtane.Controllers
public IActionResult GetImage(int id, int width, int height, string mode, string position, string background, string rotate, string recreate)
{
var file = _files.GetFile(id);
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
@ -658,8 +667,15 @@ namespace Oqtane.Controllers
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
if (file != null)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt {FileId}", id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
string errorPath = Path.Combine(GetFolderPath("wwwroot/images"), "error.png");
@ -763,6 +779,7 @@ namespace Oqtane.Controllers
private Models.File CreateFile(string filename, int folderid, string filepath)
{
var file = _files.GetFile(folderid, filename);
var _ImageFiles = (_settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue ?? Constants.ImageFiles) ?? Constants.ImageFiles;
int size = 0;
var folder = _folders.GetFolder(folderid, false);
@ -789,7 +806,7 @@ namespace Oqtane.Controllers
file.ImageHeight = 0;
file.ImageWidth = 0;
if (Constants.ImageFiles.Split(',').Contains(file.Extension.ToLower()))
if (_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
try
{

View File

@ -204,7 +204,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Folder Put(int id, [FromBody] Folder folder)
{
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
if (ModelState.IsValid && folder.SiteId == _alias.SiteId && folder.FolderId == id && _folders.GetFolder(folder.FolderId, false) != null && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, folder.FolderId, PermissionNames.Edit))
{
if (folder.IsPathValid())
{

View File

@ -30,11 +30,12 @@ namespace Oqtane.Controllers
private readonly IMemoryCache _cache;
private readonly IHttpContextAccessor _accessor;
private readonly IAliasRepository _aliases;
private readonly ISiteRepository _sites;
private readonly ILogger<InstallationController> _filelogger;
private readonly ITenantManager _tenantManager;
private readonly IServerStateManager _serverState;
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
public InstallationController(IConfigManager configManager, IInstallationManager installationManager, IDatabaseManager databaseManager, ILocalizationManager localizationManager, IMemoryCache cache, IHttpContextAccessor accessor, IAliasRepository aliases, ISiteRepository sites, ILogger<InstallationController> filelogger, ITenantManager tenantManager, IServerStateManager serverState)
{
_configManager = configManager;
_installationManager = installationManager;
@ -43,6 +44,7 @@ namespace Oqtane.Controllers
_cache = cache;
_accessor = accessor;
_aliases = aliases;
_sites = sites;
_filelogger = filelogger;
_tenantManager = tenantManager;
_serverState = serverState;
@ -108,6 +110,70 @@ namespace Oqtane.Controllers
return GetAssemblyList().Select(item => item.HashedName).ToList();
}
private List<ClientAssembly> GetAssemblyList()
{
var alias = _tenantManager.GetAlias();
return _cache.GetOrCreate($"assemblieslist:{alias.SiteKey}", entry =>
{
var assemblyList = new List<ClientAssembly>();
var site = _sites.GetSite(alias.SiteId);
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
bool hashfilename = true;
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
{
hashfilename = false;
}
// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(alias.SiteKey).Assemblies;
// populate assembly list
foreach (var assembly in assemblies)
{
if (assembly != Constants.ClientId)
{
var filepath = Path.Combine(binFolder, assembly) + ".dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
}
}
}
// insert satellite assemblies at beginning of list
foreach (var culture in _localizationManager.GetInstalledCultures())
{
if (culture != Constants.DefaultCulture)
{
var assembliesFolderPath = Path.Combine(binFolder, culture);
if (Directory.Exists(assembliesFolderPath))
{
foreach (var assembly in assemblies)
{
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
}
}
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
}
}
}
}
return assemblyList;
});
}
// GET api/<controller>/load?list=x,y
[HttpGet("load")]
public IActionResult Load(string list = "*")
@ -115,126 +181,79 @@ namespace Oqtane.Controllers
return File(GetAssemblies(list), System.Net.Mime.MediaTypeNames.Application.Octet, "oqtane.dll");
}
private List<ClientAssembly> GetAssemblyList()
{
var siteKey = _tenantManager.GetAlias().SiteKey;
return _cache.GetOrCreate($"assemblieslist:{siteKey}", entry =>
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblyList = new List<ClientAssembly>();
// testmode setting is used for validating that the API is downloading the appropriate assemblies to the client
bool hashfilename = true;
if (_configManager.GetSetting($"{SettingKeys.TestModeKey}", "false") == "true")
{
hashfilename = false;
}
// get site assemblies which should be downloaded to client
var assemblies = _serverState.GetServerState(siteKey).Assemblies;
// populate assembly list
foreach (var assembly in assemblies)
{
if (assembly != Constants.ClientId)
{
var filepath = Path.Combine(binFolder, assembly) + ".dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Add(new ClientAssembly(Path.Combine(binFolder, assembly + ".dll"), hashfilename));
}
}
}
// insert satellite assemblies at beginning of list
foreach (var culture in _localizationManager.GetInstalledCultures())
{
if (culture != Constants.DefaultCulture)
{
var assembliesFolderPath = Path.Combine(binFolder, culture);
if (Directory.Exists(assembliesFolderPath))
{
foreach (var assembly in assemblies)
{
var filepath = Path.Combine(assembliesFolderPath, assembly) + ".resources.dll";
if (System.IO.File.Exists(filepath))
{
assemblyList.Insert(0, new ClientAssembly(Path.Combine(assembliesFolderPath, assembly + ".resources.dll"), hashfilename));
}
}
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, $"The Satellite Assembly Folder For {culture} Does Not Exist"));
}
}
}
return assemblyList;
});
}
private byte[] GetAssemblies(string list)
{
var siteKey = _tenantManager.GetAlias().SiteKey;
var alias = _tenantManager.GetAlias();
if (list == "*")
{
return _cache.GetOrCreate($"assemblies:{siteKey}", entry =>
return _cache.GetOrCreate($"assemblies:{alias.SiteKey}", entry =>
{
return GetZIP(list);
return GetZIP(list, alias);
});
}
else
{
return GetZIP(list);
return GetZIP(list, alias);
}
}
private byte[] GetZIP(string list)
private byte[] GetZIP(string list, Alias alias)
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// get list of assemblies which should be downloaded to client
List<ClientAssembly> assemblies = GetAssemblyList();
if (list != "*")
var site = _sites.GetSite(alias.SiteId);
if (site != null && (site.Runtime == "WebAssembly" || site.HybridEnabled))
{
var filter = list.Split(',').ToList();
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
}
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
// create zip file containing assemblies and debug symbols
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
// get list of assemblies which should be downloaded to client
List<ClientAssembly> assemblies = GetAssemblyList();
if (list != "*")
{
foreach (var assembly in assemblies)
var filter = list.Split(',').ToList();
assemblies.RemoveAll(item => !filter.Contains(item.HashedName));
}
// create zip file containing assemblies and debug symbols
using (var memoryStream = new MemoryStream())
{
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
foreach (var assembly in assemblies)
{
if (System.IO.File.Exists(assembly.FilePath))
if (Path.GetFileNameWithoutExtension(assembly.FilePath) != Constants.ClientId)
{
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
if (System.IO.File.Exists(assembly.FilePath))
{
filestream.CopyTo(entrystream);
using (var filestream = new FileStream(assembly.FilePath, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName).Open())
{
filestream.CopyTo(entrystream);
}
}
}
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
if (System.IO.File.Exists(pdb))
{
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
var pdb = assembly.FilePath.Replace(".dll", ".pdb");
if (System.IO.File.Exists(pdb))
{
filestream.CopyTo(entrystream);
using (var filestream = new FileStream(pdb, FileMode.Open, FileAccess.Read))
using (var entrystream = archive.CreateEntry(assembly.HashedName.Replace(".dll", ".pdb")).Open())
{
filestream.CopyTo(entrystream);
}
}
}
}
}
}
return memoryStream.ToArray();
return memoryStream.ToArray();
}
}
else
{
// return empty zip
using (var memoryStream = new MemoryStream())
{
using (var zip = new ZipArchive(memoryStream, ZipArchiveMode.Create)) {}
return memoryStream.ToArray();
}
}
}

View File

@ -67,7 +67,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Host)]
public Job Put(int id, [FromBody] Job job)
{
if (ModelState.IsValid && _jobs.GetJob(job.JobId, false) != null)
if (ModelState.IsValid && job.JobId == id && _jobs.GetJob(job.JobId, false) != null)
{
job = _jobs.UpdateJob(job);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", job);

View File

@ -1,5 +1,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Authorization;
@ -9,9 +12,6 @@ using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
using System.Linq;
using System.Diagnostics;
using System.Globalization;
namespace Oqtane.Controllers
{
@ -102,6 +102,24 @@ namespace Oqtane.Controllers
}
}
[HttpPut]
[Authorize(Roles = RoleNames.Admin)]
public void Put([FromBody] Language language)
{
if (ModelState.IsValid && language.SiteId == _alias.SiteId)
{
_languages.UpdateLanguage(language);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Language, language.LanguageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Language Updated {Language}", language);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Put Attempt {Language}", language);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
[HttpPost]
[Authorize(Roles = RoleNames.Admin)]
public Language Post([FromBody] Language language)

View File

@ -154,7 +154,7 @@ namespace Oqtane.Controllers
{
var _module = _modules.GetModule(module.ModuleId, false);
if (ModelState.IsValid && module.SiteId == _alias.SiteId && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
if (ModelState.IsValid && module.SiteId == _alias.SiteId && module.ModuleId == id && _module != null && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Module, module.ModuleId, PermissionNames.Edit))
{
module = _modules.UpdateModule(module);

View File

@ -167,7 +167,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public void Put(int id, [FromBody] ModuleDefinition moduleDefinition)
{
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
if (ModelState.IsValid && moduleDefinition.SiteId == _alias.SiteId && moduleDefinition.ModuleDefinitionId == id && _moduleDefinitions.GetModuleDefinition(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId) != null)
{
_moduleDefinitions.UpdateModuleDefinition(moduleDefinition);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update);

View File

@ -161,6 +161,12 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId))
{
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);
}
notification = _notifications.AddNotification(notification);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Notification Added {NotificationId}", notification.NotificationId);
@ -179,8 +185,14 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)]
public Notification Put(int id, [FromBody] Notification notification)
{
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && _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 && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId)))
{
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);
}
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);

View File

@ -269,7 +269,7 @@ namespace Oqtane.Controllers
// get current page
var currentPage = _pages.GetPage(page.PageId, false);
if (ModelState.IsValid && page.SiteId == _alias.SiteId && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
if (ModelState.IsValid && page.SiteId == _alias.SiteId && page.PageId == id && currentPage != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, page.PageId, PermissionNames.Edit))
{
// get current page permissions
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();

View File

@ -109,7 +109,7 @@ namespace Oqtane.Controllers
public PageModule Put(int id, [FromBody] PageModule pageModule)
{
var page = _pages.GetPage(pageModule.PageId);
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
if (ModelState.IsValid && page != null && page.SiteId == _alias.SiteId && pageModule.PageModuleId == id && _pageModules.GetPageModule(pageModule.PageModuleId, false) != null && _userPermissions.IsAuthorized(User, page.SiteId, EntityNames.Page, pageModule.PageId, PermissionNames.Edit))
{
pageModule = _pageModules.UpdatePageModule(pageModule);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.PageModule, pageModule.PageModuleId, SyncEventActions.Update);

View File

@ -94,7 +94,7 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.Profile}:{PermissionNames.Write}:{RoleNames.Admin}")]
public Profile Put(int id, [FromBody] Profile profile)
{
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && _profiles.GetProfile(profile.ProfileId, false) != null)
if (ModelState.IsValid && profile.SiteId == _alias.SiteId && profile.ProfileId == id && _profiles.GetProfile(profile.ProfileId, false) != null)
{
profile = _profiles.UpdateProfile(profile);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Profile, profile.ProfileId, SyncEventActions.Update);

View File

@ -98,7 +98,7 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.Role}:{PermissionNames.Write}:{RoleNames.Admin}")]
public Role Put(int id, [FromBody] Role role)
{
if (ModelState.IsValid && role.SiteId == _alias.SiteId && _roles.GetRole(role.RoleId, false) != null)
if (ModelState.IsValid && role.SiteId == _alias.SiteId && role.RoleId == id && _roles.GetRole(role.RoleId, false) != null)
{
role = _roles.UpdateRole(role);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Role, role.RoleId, SyncEventActions.Update);

View File

@ -128,7 +128,7 @@ namespace Oqtane.Controllers
[HttpPut("{id}")]
public Setting Put(int id, [FromBody] Setting setting)
{
if (ModelState.IsValid && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
if (ModelState.IsValid && setting.SettingId == id && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{
setting = _settings.UpdateSetting(setting);
AddSyncEvent(setting.EntityName, setting.SettingId, SyncEventActions.Update);

View File

@ -86,6 +86,12 @@ namespace Oqtane.Controllers
.Where(item => !item.IsPrivate || User.IsInRole(RoleNames.Admin))
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
// populate File Extensions
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
? site.Settings["ImageFiles"] : Constants.ImageFiles;
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
? site.Settings["UploadableFiles"] : Constants.UploadableFiles;
// pages
List<Setting> settings = _settings.GetSettings(EntityNames.Page).ToList();
site.Pages = new List<Page>();
@ -192,7 +198,7 @@ namespace Oqtane.Controllers
public Site Put(int id, [FromBody] Site site)
{
var current = _sites.GetSite(site.SiteId, false);
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && current != null)
if (ModelState.IsValid && site.SiteId == _alias.SiteId && site.TenantId == _alias.TenantId && site.SiteId == id && current != null)
{
site = _sites.UpdateSite(site);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, site.SiteId, SyncEventActions.Update);

View File

@ -71,7 +71,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public void Put(int id, [FromBody] Theme theme)
{
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
if (ModelState.IsValid && theme.SiteId == _alias.SiteId && theme.ThemeId == id && _themes.GetTheme(theme.ThemeId,theme.SiteId) != null)
{
_themes.UpdateTheme(theme);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update);

View File

@ -118,7 +118,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Admin)]
public UrlMapping Put(int id, [FromBody] UrlMapping urlMapping)
{
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
if (ModelState.IsValid && urlMapping.SiteId == _alias.SiteId && urlMapping.UrlMappingId == id && _urlMappings.GetUrlMapping(urlMapping.UrlMappingId, false) != null)
{
urlMapping = _urlMappings.UpdateUrlMapping(urlMapping);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UrlMapping, urlMapping.UrlMappingId, SyncEventActions.Update);

View File

@ -173,15 +173,16 @@ namespace Oqtane.Controllers
[Authorize]
public async Task<User> Put(int id, [FromBody] User user)
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
user = await _userManager.UpdateUser(user);
}
else
{
user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Post Attempt {User}", user);
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Put Attempt {User}", user);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
user = null;
}

View File

@ -149,7 +149,7 @@ namespace Oqtane.Controllers
public UserRole Put(int id, [FromBody] UserRole userRole)
{
var role = _roles.GetRole(userRole.RoleId);
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
if (ModelState.IsValid && role != null && SiteValid(role.SiteId) && RoleValid(role.Name) && userRole.UserRoleId == id && _userRoles.GetUserRole(userRole.UserRoleId, false) != null)
{
userRole = _userRoles.UpdateUserRole(userRole);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.UserRole, userRole.UserRoleId, SyncEventActions.Update);

View File

@ -0,0 +1,54 @@
using System.Linq;
using System.Security.Claims;
namespace Oqtane.Extensions
{
public static class ClaimsPrincipalExtensions
{
public static string Username(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.Name))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name).Value;
}
else
{
return "";
}
}
public static int UserId(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == ClaimTypes.NameIdentifier))
{
return int.Parse(claimsPrincipal.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
}
else
{
return -1;
}
}
public static string Roles(this ClaimsPrincipal claimsPrincipal)
{
var roles = "";
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
roles += ((roles == "") ? "" : ";") + claim.Value;
}
return roles;
}
public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey"))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value;
}
else
{
return "";
}
}
}
}

View File

@ -124,7 +124,7 @@ namespace Microsoft.Extensions.DependencyInjection
// note that ConfigureApplicationCookie internally uses an ApplicationScheme of "Identity.Application"
services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = false;
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Events.OnRedirectToLogin = context =>
@ -179,7 +179,7 @@ namespace Microsoft.Extensions.DependencyInjection
options.Lockout.AllowedForNewUsers = false;
// SignIn settings
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedPhoneNumber = false;
// User settings

View File

@ -50,7 +50,6 @@ namespace Oqtane.Extensions
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // authorization code flow
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
// cookie config is required to avoid Correlation Failed errors
@ -62,6 +61,7 @@ namespace Oqtane.Extensions
options.MetadataAddress = sitesettings.GetValue("ExternalLogin:MetadataUrl", "");
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
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"));
if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", "")))
{
@ -150,15 +150,16 @@ namespace Oqtane.Extensions
private static async Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
// OAuth 2.0
var email = "";
var id = "";
var claims = "";
var id = "";
var name = "";
var email = "";
if (context.Options.UserInformationEndpoint != "")
{
try
{
// call user information endpoint
// call user information endpoint using access token
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
@ -167,32 +168,49 @@ namespace Oqtane.Extensions
response.EnsureSuccessStatusCode();
claims = await response.Content.ReadAsStringAsync();
// parse json output
// get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
if (!claims.StartsWith("[") && !claims.EndsWith("]"))
// some user endpoints can return multiple objects (ie. GitHub) so convert single object to array (if necessary)
var jsonclaims = claims;
if (!jsonclaims.StartsWith("[") && !jsonclaims.EndsWith("]"))
{
claims = "[" + claims + "]"; // convert to json array
jsonclaims = "[" + jsonclaims + "]";
}
JsonNode items = JsonNode.Parse(claims)!;
// parse claim values
JsonNode items = JsonNode.Parse(jsonclaims)!;
foreach (var item in items.AsArray())
{
if (item[emailClaimType] != null)
name = "";
email = "";
// id claim is required
if (!string.IsNullOrEmpty(idClaimType) && item[idClaimType] != null)
{
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
id = item[idClaimType].ToString();
// name claim is optional
if (!string.IsNullOrEmpty(nameClaimType) && item[nameClaimType] != null)
{
email = item[emailClaimType].ToString().ToLower();
if (item[idClaimType] != null)
name = item[nameClaimType].ToString();
}
// email claim is optional
if (!string.IsNullOrEmpty(emailClaimType) && item[emailClaimType] != null)
{
if (EmailValid(item[emailClaimType].ToString(), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
id = item[idClaimType].ToString();
email = item[emailClaimType].ToString().ToLower();
}
break;
}
}
}
if (string.IsNullOrEmpty(id))
{
id = email;
if (!string.IsNullOrEmpty(id))
{
break;
}
}
}
catch (Exception ex)
@ -203,7 +221,7 @@ namespace Oqtane.Extensions
}
// validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
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));
@ -213,6 +231,14 @@ namespace Oqtane.Extensions
// pass properties to OnTicketReceived
context.Properties.SetParameter("status", identity.Label);
context.Properties.SetParameter("redirecturl", context.Properties.RedirectUri);
// set cookie expiration
string cookieExpStr = context.HttpContext.GetSiteSettings().GetValue("LoginOptions:CookieExpiration", "");
if (!string.IsNullOrEmpty(cookieExpStr) && TimeSpan.TryParse(cookieExpStr, out TimeSpan cookieExpTS))
{
context.Properties.ExpiresUtc = DateTime.Now.Add(cookieExpTS);
context.Properties.IsPersistent = true;
}
}
private static Task OnTicketReceived(TicketReceivedContext context)
@ -231,28 +257,46 @@ namespace Oqtane.Extensions
private static async Task OnTokenValidated(TokenValidatedContext context)
{
// OpenID Connect
var claims = "";
var id = "";
var name = "";
var email = "";
// serialize claims
foreach (var claim in context.Principal.Claims)
{
claims += "\"" + claim.Type + "\":\"" + claim.Value + "\",";
}
claims = "{" + claims.Substring(0, claims.Length - 1) + "}";
// get claim types
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var id = context.Principal.FindFirstValue(idClaimType);
var nameClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:NameClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType);
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
// parse claim values - id claim is required
id = context.Principal.FindFirstValue(idClaimType);
// name claim is optional
if (!string.IsNullOrEmpty(nameClaimType) && context.Principal.FindFirstValue(nameClaimType) != null)
{
name = context.Principal.FindFirstValue(nameClaimType);
}
// email claim is optional
if (!string.IsNullOrEmpty(emailClaimType) && context.Principal.FindFirstValue(emailClaimType) != null)
{
if (EmailValid(context.Principal.FindFirstValue(emailClaimType), context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
email = context.Principal.FindFirstValue(emailClaimType);
}
}
// validate user
var identity = await ValidateUser(email, id, claims, context.HttpContext, context.Principal);
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success)
{
// external roles
if (!string.IsNullOrEmpty(context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
foreach (var claim in context.Principal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
// include access token
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity);
}
@ -284,12 +328,20 @@ namespace Oqtane.Extensions
return Task.CompletedTask;
}
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
private static async Task<ClaimsIdentity> ValidateUser(string id, string name, string email, string claims, HttpContext httpContext, ClaimsPrincipal claimsPrincipal)
{
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
// use identity.Label as a temporary location to store validation status information
// review claims feature (for testing - external login is disabled)
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:ReviewClaims", "false")))
{
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "Provider Returned The Following Claims: {Claims}", claims);
identity.Label = ExternalLoginStatus.ReviewClaims;
return identity;
}
var providerType = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderType", "");
var providerName = httpContext.GetSiteSettings().GetValue("ExternalLogin:ProviderName", "");
var alias = httpContext.GetAlias();
@ -308,136 +360,158 @@ namespace Oqtane.Extensions
}
else
{
if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
bool duplicates = false;
if (!string.IsNullOrEmpty(email))
{
bool duplicates = false;
try
{
identityuser = await _identityUserManager.FindByEmailAsync(email);
}
catch
{
// FindByEmailAsync will throw an error if the email matches multiple user accounts
catch // FindByEmailAsync will throw an error if the email matches multiple user accounts
{
duplicates = true;
}
if (identityuser == null)
}
if (identityuser == null)
{
if (duplicates)
{
if (duplicates)
identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identity.Label = ExternalLoginStatus.DuplicateEmail;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
}
else
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss", CultureInfo.InvariantCulture));
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = email,
DisplayName = email,
Email = email,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
// user identifiers
var username = "";
var emailaddress = "";
var displayname = "";
bool emailconfirmed = false;
if (user != null)
if (!string.IsNullOrEmpty(email)) // email claim provided
{
username = email;
emailaddress = email;
displayname = (!string.IsNullOrEmpty(name)) ? name : email;
emailconfirmed = true;
}
else if (!string.IsNullOrEmpty(name)) // name claim provided
{
username = name.ToLower().Replace(" ", "") + DateTime.UtcNow.ToString("mmss");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = name;
}
else // neither email nor name provided
{
username = Guid.NewGuid().ToString("N");
emailaddress = ""; // unknown - will need to be requested from user later
displayname = username;
}
identityuser = new IdentityUser();
identityuser.UserName = username;
identityuser.Email = emailaddress;
identityuser.EmailConfirmed = emailconfirmed;
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
var password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
var result = await _identityUserManager.CreateAsync(identityuser, password);
if (result.Succeeded)
{
user = new User
{
SiteId = alias.SiteId,
Username = username,
DisplayName = displayname,
Email = emailaddress,
LastLoginOn = null,
LastIPAddress = ""
};
user = _users.AddUser(user);
if (user != null)
{
if (!string.IsNullOrEmpty(email))
{
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
}
}
else
{
identity.Label = ExternalLoginStatus.UserDoesNotExist;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
}
else
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
identity.Label = ExternalLoginStatus.UserNotCreated;
_logger.Log(alias.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
else
{
// provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
identity.Label = ExternalLoginStatus.UserDoesNotExist;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
}
else // email invalid
else
{
identity.Label = ExternalLoginStatus.InvalidEmail;
if (!string.IsNullOrEmpty(email))
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
if (login == null)
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email);
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true")))
{
// external login using existing user account - verification required
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = httpContext.Request.Scheme + "://" + alias.Name;
url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
_notifications.AddNotification(notification);
identity.Label = ExternalLoginStatus.VerificationRequired;
_logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
// external login using existing user account - link automatically
user = _users.GetUser(identityuser.UserName);
user.SiteId = alias.SiteId;
var _notifications = httpContext.RequestServices.GetRequiredService<INotificationRepository>();
string url = httpContext.Request.Scheme + "://" + alias.Name;
string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Notification", body);
_notifications.AddNotification(notification);
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName));
_logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName);
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims);
// provider keys do not match
identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
}
}
}
@ -455,6 +529,25 @@ namespace Oqtane.Extensions
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
// external roles
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role))
{
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role))
{
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value))
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value));
}
}
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
}
}
// user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{
@ -493,7 +586,7 @@ namespace Oqtane.Extensions
}
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. The Valid Claims Are {Claims}.", mapping.Split(":")[0], claims);
_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
@ -506,9 +599,10 @@ namespace Oqtane.Extensions
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
}
}
else // id invalid
else // claims invalid
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims);
identity.Label = ExternalLoginStatus.MissingClaims;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
}
return identity;

View File

@ -105,12 +105,6 @@ namespace Oqtane.Infrastructure
IsNewTenant = false
};
// on upgrade install the associated Nuget package
if (!string.IsNullOrEmpty(install.ConnectionString))
{
InstallDatabase(install);
}
var installation = IsInstalled();
if (!installation.Success)
{
@ -209,57 +203,6 @@ namespace Oqtane.Infrastructure
return result;
}
private Installation InstallDatabase(InstallConfig install)
{
var result = new Installation {Success = false, Message = string.Empty};
try
{
bool installPackages = false;
// iterate database packages in installation folder
var packagesFolder = new DirectoryInfo(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder));
foreach (var package in packagesFolder.GetFiles("*.nupkg.bak"))
{
// determine if package needs to be upgraded or installed
bool upgrade = System.IO.File.Exists(package.FullName.Replace(".nupkg.bak",".log"));
if (upgrade || package.Name.StartsWith(Utilities.GetAssemblyName(install.DatabaseType)))
{
var packageName = Path.Combine(package.DirectoryName, package.Name);
packageName = packageName.Substring(0, packageName.IndexOf(".bak"));
package.MoveTo(packageName, true);
installPackages = true;
}
}
if (installPackages)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var installationManager = scope.ServiceProvider.GetRequiredService<IInstallationManager>();
installationManager.InstallPackages();
}
}
// load the installation database type (if necessary)
if (Type.GetType(install.DatabaseType) == null)
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
var assembliesFolder = new DirectoryInfo(assemblyPath);
var assemblyFile = new FileInfo($"{assembliesFolder}/{Utilities.GetAssemblyName(install.DatabaseType)}.dll");
AssemblyLoadContext.Default.LoadOqtaneAssembly(assemblyFile);
}
result.Success = true;
}
catch (Exception ex)
{
result.Message = ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
return result;
}
private Installation CreateDatabase(InstallConfig install)
{
var result = new Installation { Success = false, Message = string.Empty };
@ -268,8 +211,6 @@ namespace Oqtane.Infrastructure
{
try
{
InstallDatabase(install);
var databaseType = install.DatabaseType;
// get database type
@ -436,7 +377,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Migrating A Tenant Database. This Is Usually Related To A Tenant Database Not Being In A Supported State. " + ex.ToString();
result.Message = "An Error Occurred Migrating The Database For Tenant " + tenant.Name + ". This Is Usually Related To Database Permissions, Connection String Mappings, Or The Database Not Being In A Supported State. " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
@ -457,7 +398,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Executing Upgrade Logic. " + ex.ToString();
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
}
@ -527,7 +468,7 @@ namespace Oqtane.Infrastructure
}
catch (Exception ex)
{
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " - " + ex.ToString();
result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
}
}
}
@ -614,6 +555,7 @@ namespace Oqtane.Infrastructure
SiteTemplateType = install.SiteTemplate,
Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value,
RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value,
HybridEnabled = false
};
site = sites.AddSite(site);

View File

@ -15,10 +15,18 @@ namespace Oqtane.Infrastructure.EventSubscribers
public void EntityChanged(SyncEvent syncEvent)
{
// when site entities change (ie. site, pages, modules, etc...) a site refresh event is raised and the site cache item needs to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Refresh)
{
_cache.Remove($"site:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
// when a site entity is updated the hosting model may have changed, so the client assemblies cache items need to be refreshed
if (syncEvent.EntityName == EntityNames.Site && syncEvent.Action == SyncEventActions.Update)
{
_cache.Remove($"assemblieslist:{syncEvent.TenantId}:{syncEvent.EntityId}");
_cache.Remove($"assemblies:{syncEvent.TenantId}:{syncEvent.EntityId}");
}
}
}
}

View File

@ -13,7 +13,6 @@ using System.Xml;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Oqtane.Controllers;
using Oqtane.Shared;
// ReSharper disable AssignNullToNotNullAttribute
@ -41,51 +40,25 @@ namespace Oqtane.Infrastructure
}
}
// method must be static as it is called in ConfigureServices during Startup
public static string InstallPackages(string webRootPath, string contentRootPath)
{
string errors = "";
string binPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
string sourceFolder = Path.Combine(contentRootPath, "Packages");
string sourceFolder = Path.Combine(contentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(sourceFolder))
{
Directory.CreateDirectory(sourceFolder);
}
// move packages to secure /Packages folder
foreach (var folderName in "Modules,Themes,Packages".Split(","))
{
string folder = Path.Combine(webRootPath, folderName);
if (Directory.Exists(folder))
{
foreach (var file in Directory.GetFiles(folder, "*.nupkg*"))
{
var destinationFile = Path.Combine(sourceFolder, Path.GetFileName(file));
if (File.Exists(destinationFile))
{
File.Delete(destinationFile);
}
// read assembly log
var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
if (destinationFile.ToLower().EndsWith(".nupkg.bak"))
{
// leave a copy in the current folder as it is distributed with the core framework
File.Copy(file, destinationFile);
}
else
{
// move to destination
File.Move(file, destinationFile);
}
}
}
else
{
Directory.CreateDirectory(folder);
}
}
// iterate through Nuget packages in source folder
foreach (string packagename in Directory.GetFiles(sourceFolder, "*.nupkg"))
// install Nuget packages in secure Packages folder
var packages = Directory.GetFiles(sourceFolder, "*.nupkg");
foreach (string packagename in packages)
{
try
{
@ -154,10 +127,29 @@ namespace Oqtane.Infrastructure
// ContentRootPath sometimes produces inconsistent path casing - so can't use string.Replace()
filename = Regex.Replace(filename, Regex.Escape(contentRootPath), "", RegexOptions.IgnoreCase);
assets.Add(filename);
if (!manifest && Path.GetExtension(filename) == ".log")
// packages can include a manifest (rather than relying on the framework to dynamically create one)
if (!manifest && filename.EndsWith(name + ".log"))
{
manifest = true;
}
// register assembly
if (Path.GetExtension(filename) == ".dll")
{
// if package version was not installed previously
if (!File.Exists(Path.Combine(sourceFolder, name + ".log")))
{
if (assemblies.ContainsKey(Path.GetFileName(filename)))
{
assemblies[Path.GetFileName(filename)] += 1;
}
else
{
assemblies.Add(Path.GetFileName(filename), 1);
}
}
}
}
}
@ -187,6 +179,12 @@ namespace Oqtane.Infrastructure
File.Delete(packagename);
}
if (packages.Length != 0)
{
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
}
return errors;
}
@ -232,6 +230,10 @@ namespace Oqtane.Infrastructure
{
if (!string.IsNullOrEmpty(PackageName))
{
// read assembly log
var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log");
var assemblies = GetAssemblyLog(assemblyLogPath);
// get manifest with highest version
string packagename = "";
string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log");
@ -249,17 +251,31 @@ namespace Oqtane.Infrastructure
{
// legacy support for assets that were stored as absolute paths
string filepath = asset.StartsWith("\\") ? Path.Combine(_environment.ContentRootPath, asset.Substring(1)) : asset;
if (File.Exists(filepath))
// delete assets
if (Path.GetExtension(filepath) == ".dll")
{
// do not remove licensing assemblies - this is a temporary fix until a more robust dependency management solution is available
if (!filepath.Contains("Oqtane.Licensing."))
// use assembly log to determine if assembly is used in other packages
if (assemblies.ContainsKey(Path.GetFileName(filepath)))
{
File.Delete(filepath);
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
if (assemblies[Path.GetFileName(filepath)] == 1)
{
Directory.Delete(Path.GetDirectoryName(filepath), true);
DeleteFile(filepath);
assemblies.Remove(Path.GetFileName(filepath));
}
else
{
assemblies[Path.GetFileName(filepath)] -= 1;
}
}
else // does not exist in assembly log
{
DeleteFile(filepath);
}
}
else // not an assembly
{
DeleteFile(filepath);
}
}
@ -269,6 +285,9 @@ namespace Oqtane.Infrastructure
File.Delete(asset);
}
// save assembly log
SetAssemblyLog(assemblyLogPath, assemblies);
return true;
}
}
@ -276,6 +295,76 @@ namespace Oqtane.Infrastructure
return false;
}
private void DeleteFile(string filepath)
{
if (File.Exists(filepath))
{
File.Delete(filepath);
if (!Directory.EnumerateFiles(Path.GetDirectoryName(filepath)).Any())
{
Directory.Delete(Path.GetDirectoryName(filepath), true);
}
}
}
public int RegisterAssemblies()
{
var assemblyLogPath = GetAssemblyLogPath();
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var assemblies = GetAssemblyLog(assemblyLogPath);
// remove assemblies that no longer exist
foreach (var dll in assemblies)
{
if (!File.Exists(Path.Combine(binFolder, dll.Key)))
{
assemblies.Remove(dll.Key);
}
}
// add assemblies which are not registered
foreach (var dll in Directory.GetFiles(binFolder, "*.dll"))
{
if (!assemblies.ContainsKey(Path.GetFileName(dll)))
{
assemblies.Add(Path.GetFileName(dll), 1);
}
}
SetAssemblyLog(assemblyLogPath, assemblies);
return assemblies.Count;
}
private string GetAssemblyLogPath()
{
string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);
if (!Directory.Exists(packagesFolder))
{
Directory.CreateDirectory(packagesFolder);
}
return Path.Combine(packagesFolder, "assemblies.log");
}
private static Dictionary<string, int> GetAssemblyLog(string assemblyLogPath)
{
Dictionary<string, int> assemblies = new Dictionary<string, int>();
if (File.Exists(assemblyLogPath))
{
assemblies = JsonSerializer.Deserialize<Dictionary<string, int>>(File.ReadAllText(assemblyLogPath));
}
return assemblies;
}
private static void SetAssemblyLog(string assemblyLogPath, Dictionary<string, int> assemblies)
{
if (File.Exists(assemblyLogPath))
{
File.Delete(assemblyLogPath);
}
File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true }));
}
public async Task UpgradeFramework()
{
string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder);

View File

@ -6,6 +6,7 @@ namespace Oqtane.Infrastructure
{
void InstallPackages();
bool UninstallPackage(string PackageName);
int RegisterAssemblies();
Task UpgradeFramework();
void RestartApplication();
}

View File

@ -7,6 +7,7 @@ namespace Oqtane.Infrastructure
Alias GetAlias();
Tenant GetTenant();
void SetAlias(Alias alias);
void SetAlias(int tenantId, int siteId);
void SetTenant(int tenantId);
}
}

View File

@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure
var logRepository = provider.GetRequiredService<ILogRepository>();
var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>();
var installationManager = provider.GetRequiredService<IInstallationManager>();
// iterate through sites for current tenant
List<Site> sites = siteRepository.GetSites().ToList();
@ -96,6 +97,17 @@ namespace Oqtane.Infrastructure
}
}
// register assemblies
try
{
var assemblies = installationManager.RegisterAssemblies();
log += assemblies.ToString() + " Assemblies Registered<br />";
}
catch (Exception ex)
{
log += $"Error Registering Assemblies - {ex.Message}<br />";
}
return log;
}

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Oqtane.Repository;
using Oqtane.Shared;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
namespace Oqtane.Infrastructure
{
@ -22,8 +23,7 @@ namespace Oqtane.Infrastructure
var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager;
string path = context.Request.Path.ToString();
if (config.IsInstalled() && !path.StartsWith("/_blazor"))
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
{
// get alias (note that this also sets SiteState.Alias)
var tenantManager = context.RequestServices.GetService(typeof(ITenantManager)) as ITenantManager;
@ -57,9 +57,25 @@ namespace Oqtane.Infrastructure
{
if (path.StartsWith("/" + alias.Path) && (Constants.ReservedRoutes.Any(item => path.Contains("/" + item + "/"))))
{
context.Request.Path = path.Replace("/" + alias.Path, "");
context.Request.Path = path.Substring(alias.Path.Length + 1);
}
}
// handle sitemap.xml request
if (context.Request.Path.ToString().Contains("/sitemap.xml") && !context.Request.Path.ToString().Contains("/pages"))
{
context.Request.Path = "/pages/sitemap.xml";
}
// handle robots.txt root request (does not support subfolder aliases)
if (context.Request.Path.StartsWithSegments("/robots.txt") && string.IsNullOrEmpty(alias.Path))
{
// allow all user agents and specify site map
var robots = $"User-agent: *\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml";
context.Response.ContentType = "text/plain";
await context.Response.WriteAsync(robots);
return;
}
}
}

View File

@ -26,13 +26,14 @@ namespace Oqtane.Infrastructure
{
Alias alias = null;
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
// does not support mock Alias objects (GetTenant should be used to retrieve a TenantId)
if (_siteState?.Alias != null && _siteState.Alias.AliasId != -1)
{
alias = _siteState.Alias;
}
else
{
// if there is http context
// if there is HttpContext
var httpcontext = _httpContextAccessor.HttpContext;
if (httpcontext != null)
{
@ -78,15 +79,19 @@ namespace Oqtane.Infrastructure
return null;
}
// background processes can set the alias using the SiteState service
public void SetAlias(Alias alias)
{
// background processes can set the alias using the SiteState service
_siteState.Alias = alias;
}
public void SetAlias(int tenantId, int siteId)
{
_siteState.Alias = _aliasRepository.GetAliases().ToList().FirstOrDefault(item => item.TenantId == tenantId && item.SiteId == siteId);
}
public void SetTenant(int tenantId)
{
// background processes can set the alias using the SiteState service
_siteState.Alias = new Alias { TenantId = tenantId, AliasId = -1, SiteId = -1 };
}
}

View File

@ -106,7 +106,7 @@ namespace Oqtane.Managers
{
if (string.IsNullOrEmpty(user.Password))
{
// create random interal password based on random date and punctuation ie. Jan-23-1981+14:43:12!
// generate password based on random date and punctuation ie. Jan-23-1981+14:43:12!
Random rnd = new Random();
var date = DateTime.UtcNow.AddDays(-rnd.Next(50 * 365)).AddHours(rnd.Next(0, 24)).AddMinutes(rnd.Next(0, 60)).AddSeconds(rnd.Next(0, 60));
user.Password = date.ToString("MMM-dd-yyyy+HH:mm:ss", CultureInfo.InvariantCulture) + (char)rnd.Next(33, 47);
@ -152,7 +152,7 @@ namespace Oqtane.Managers
{
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, User, "User Account Verification", body);
_notifications.AddNotification(notification);
}
@ -205,8 +205,22 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email)
{
await _identityUserManager.SetEmailAsync(identityuser, user.Email);
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
// if email address changed and user is not administrator, email verification is required for new email address
if (!user.EmailConfirmed)
{
var alias = _tenantManager.GetAlias();
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
else
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
}
}
user = _users.UpdateUser(user);
@ -308,7 +322,7 @@ namespace Oqtane.Managers
user = _users.GetUser(identityuser.UserName);
if (user != null)
{
if (identityuser.EmailConfirmed)
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
{
user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow;
@ -323,7 +337,7 @@ namespace Oqtane.Managers
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
}
}
}

View File

@ -0,0 +1,30 @@
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.05.00.01.00")]
public class AddSiteHybridEnabled : MultiDatabaseMigration
{
public AddSiteHybridEnabled(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.AddBooleanColumn("HybridEnabled", true);
siteEntityBuilder.UpdateColumn("HybridEnabled", "0", "bool", ""); // default to false
}
protected override void Down(MigrationBuilder migrationBuilder)
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropColumn("HybridEnabled");
}
}
}

Some files were not shown because too many files have changed in this diff Show More