@@ -537,6 +549,7 @@
private string _defaultalias;
private string _rendermode = RenderModes.Interactive;
+ private string _enhancednavigation = "True";
private string _runtime = Runtimes.Server;
private string _prerender = "True";
private string _hybrid = "False";
@@ -660,6 +673,7 @@
// hosting model
_rendermode = site.RenderMode;
+ _enhancednavigation = site.EnhancedNavigation.ToString();
_runtime = site.Runtime;
_prerender = site.Prerender.ToString();
_hybrid = site.Hybrid.ToString();
@@ -807,13 +821,11 @@
// hosting model
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
- if (site.RenderMode != _rendermode || site.Runtime != _runtime || site.Prerender != bool.Parse(_prerender) || site.Hybrid != bool.Parse(_hybrid))
- {
- site.RenderMode = _rendermode;
- site.Runtime = _runtime;
- site.Prerender = bool.Parse(_prerender);
- site.Hybrid = bool.Parse(_hybrid);
- }
+ site.RenderMode = _rendermode;
+ site.EnhancedNavigation = bool.Parse(_enhancednavigation);
+ site.Runtime = _runtime;
+ site.Prerender = bool.Parse(_prerender);
+ site.Hybrid = bool.Parse(_hybrid);
}
site = await SiteService.UpdateSiteAsync(site);
diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor
index 6933783a..e4e98fba 100644
--- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor
+++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor
@@ -7,7 +7,7 @@
@inject ISettingService SettingService
@inject IStringLocalizer
Localizer
-
+
@_textEditorComponent
@@ -18,6 +18,8 @@
private RenderFragment _textEditorComponent;
private ITextEditor _textEditor;
+ private string _style = "margin-bottom: 50px;";
+
[Parameter]
public string Content { get; set; }
@@ -30,6 +32,9 @@
[Parameter]
public string Provider { get; set; }
+ [Parameter]
+ public string Style { get; set; } // optional
+
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary
AdditionalAttributes { get; set; } = new Dictionary();
@@ -40,6 +45,12 @@
protected override void OnParametersSet()
{
+
+ if (!string.IsNullOrEmpty(Style))
+ {
+ _style = Style;
+ }
+
_textEditorComponent = (builder) =>
{
CreateTextEditor(builder);
diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs
index 00dc2c61..264930c4 100644
--- a/Oqtane.Client/Modules/ModuleBase.cs
+++ b/Oqtane.Client/Modules/ModuleBase.cs
@@ -460,6 +460,11 @@ namespace Oqtane.Modules
public string ReplaceTokens(string content, object obj)
{
+ // check for null or empty content
+ if (string.IsNullOrEmpty(content))
+ {
+ return content;
+ }
// Using StringBuilder avoids the performance penalty of repeated string allocations
// that occur with string.Replace or string concatenation inside loops.
var sb = new StringBuilder();
diff --git a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
index 0e212ab8..fbb57778 100644
--- a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx
@@ -204,7 +204,7 @@
Page Deleted Successfully
-
+
All Pages Deleted Successfully
diff --git a/Oqtane.Client/Resources/UI/ModuleInstance.resx b/Oqtane.Client/Resources/UI/ModuleInstance.resx
index fc0994f2..4c4a4c63 100644
--- a/Oqtane.Client/Resources/UI/ModuleInstance.resx
+++ b/Oqtane.Client/Resources/UI/ModuleInstance.resx
@@ -124,6 +124,9 @@
Module Type Is Invalid For {0}
- An Unexpected Error Has Occurred
+ An Unexpected Error Has Occurred
+
+
+ Missing service(s): {0}. Please make sure they have been registered correctly.
\ No newline at end of file
diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor
index abf96087..b6b5e0af 100644
--- a/Oqtane.Client/UI/RenderModeBoundary.razor
+++ b/Oqtane.Client/UI/RenderModeBoundary.razor
@@ -1,7 +1,11 @@
@namespace Oqtane.UI
+@using System.Reflection
+@using Module = Oqtane.Models.Module
+@inject IServiceProvider ServiceProvider
@inject SiteState ComponentSiteState
@inject IStringLocalizer Localizer
@inject ILogService LoggingService
+@inject NavigationManager NavigationManager
@inherits ErrorBoundary
@@ -67,37 +71,50 @@
{
if (ShouldRender())
{
- if (!string.IsNullOrEmpty(ModuleState.ModuleType))
- {
- ModuleType = Type.GetType(ModuleState.ModuleType);
- if (ModuleType != null)
- {
- // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary)
- ComponentSiteState.Hydrate(SiteState);
-
- DynamicComponent = builder =>
- {
- builder.OpenComponent(0, ModuleType);
- builder.AddAttribute(1, "RenderModeBoundary", this);
- builder.CloseComponent();
- };
- }
- else
- {
- // module does not exist with typename specified
- _messageContent = string.Format(Localizer["Error.Module.InvalidName"], Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0));
- _messageType = MessageType.Error;
- _messagePosition = "top";
- _messageStyle = MessageStyle.Alert;
- }
- }
- else
+ if (string.IsNullOrEmpty(ModuleState.ModuleType))
{
_messageContent = string.Format(Localizer["Error.Module.InvalidType"], ModuleState.ModuleDefinitionName);
_messageType = MessageType.Error;
_messagePosition = "top";
_messageStyle = MessageStyle.Alert;
+
+ return;
}
+
+ ModuleType = Type.GetType(ModuleState.ModuleType);
+ var moduleName = Utilities.GetTypeNameLastSegment(ModuleState.ModuleType, 0);
+ if (ModuleType == null)
+ {
+ // module does not exist with typename specified
+ _messageContent = string.Format(Localizer["Error.Module.InvalidName"], moduleName);
+ _messageType = MessageType.Error;
+ _messagePosition = "top";
+ _messageStyle = MessageStyle.Alert;
+
+ return;
+ }
+
+ //only validate the services injection in development environment
+ if (NavigationManager.BaseUri.Contains("localhost:") && !ValidateModuleTypeInjectedServices(ModuleType, out IList missingServices))
+ {
+ // module type is not valid for instantiation
+ _messageContent = string.Format(Localizer["Error.Module.InvalidInjectedServices"], string.Join(",", missingServices));
+ _messageType = MessageType.Error;
+ _messagePosition = "top";
+ _messageStyle = MessageStyle.Alert;
+
+ return;
+ }
+
+ // repopulate the SiteState service based on the values passed in the SiteState parameter (this is how state is marshalled across the render mode boundary)
+ ComponentSiteState.Hydrate(SiteState);
+
+ DynamicComponent = builder =>
+ {
+ builder.OpenComponent(0, ModuleType);
+ builder.AddAttribute(1, "RenderModeBoundary", this);
+ builder.CloseComponent();
+ };
}
}
@@ -165,4 +182,26 @@
_error = "";
base.Recover();
}
+
+ private bool ValidateModuleTypeInjectedServices(Type moduleType, out IList missingServices)
+ {
+ missingServices = new List();
+
+ var properties = Utilities.GetPropertiesIncludingInherited(moduleType, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ foreach(var property in properties)
+ {
+ var injectAttribute = property.GetCustomAttribute(typeof(InjectAttribute));
+ if (injectAttribute != null)
+ {
+ var serviceType = property.PropertyType;
+ var service = ServiceProvider.GetService(serviceType);
+ if (serviceType != null && service == null)
+ {
+ missingServices.Add(Utilities.GetTypeNameLastSegment(serviceType.FullName, 0));
+ }
+ }
+ }
+
+ return !missingServices.Any();
+ }
}
\ No newline at end of file
diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec
index 789f9215..6e4219a3 100644
--- a/Oqtane.Package/Oqtane.Server.nuspec
+++ b/Oqtane.Package/Oqtane.Server.nuspec
@@ -31,7 +31,7 @@
-
+
diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor
index 333ed42f..0082e669 100644
--- a/Oqtane.Server/Components/App.razor
+++ b/Oqtane.Server/Components/App.razor
@@ -60,7 +60,7 @@
}
@((MarkupString)_headResources)
-
+
@if (string.IsNullOrEmpty(_message))
{
@if (_renderMode == RenderModes.Static)
@@ -97,6 +97,7 @@
private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server;
private bool _prerender = true;
+ private bool _enhancedNavigation = true;
private string _fingerprint = "";
private int _visitorId = -1;
private string _antiForgeryToken = "";
@@ -141,6 +142,7 @@
_renderMode = site.RenderMode;
_runtime = site.Runtime;
_prerender = site.Prerender;
+ _enhancedNavigation = site.EnhancedNavigation;
_fingerprint = site.Fingerprint;
var cookieConsentSettings = SettingService.GetSetting(site.Settings, "CookieConsent", string.Empty);
diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs
index cb7b8ee5..69bed0bc 100644
--- a/Oqtane.Server/Controllers/PageController.cs
+++ b/Oqtane.Server/Controllers/PageController.cs
@@ -265,7 +265,19 @@ namespace Oqtane.Controllers
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// set user personalized page path
- _settings.AddSetting(new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false });
+ var settingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}";
+ var pathSetting = _settings.GetSetting(EntityNames.User, page.UserId.Value, settingName);
+ if(pathSetting == null)
+ {
+ pathSetting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false };
+ _settings.AddSetting(pathSetting);
+ }
+ else
+ {
+ pathSetting.SettingValue = path;
+ _settings.UpdateSetting(pathSetting);
+ }
+
_syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update);
}
}
diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
index 9d320bd9..a280c71f 100644
--- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
+++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
@@ -186,7 +186,7 @@ namespace Oqtane.Infrastructure
var mailboxAddressValidationError = "";
// sender
- if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") != "True")
+ if ((settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True") && string.IsNullOrEmpty(fromEmail))
{
fromEmail = settingRepository.GetSettingValue(settings, "SMTPSender", "");
fromName = string.IsNullOrEmpty(fromName) ? site.Name : fromName;
diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs
index 4e95d5f6..e6f2661d 100644
--- a/Oqtane.Server/Managers/UserManager.cs
+++ b/Oqtane.Server/Managers/UserManager.cs
@@ -279,7 +279,7 @@ namespace Oqtane.Managers
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
}
- if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")))
+ if (bool.Parse(_settings.GetSettingValue(EntityNames.Site, alias.SiteId, "LoginOptions:RequireConfirmedEmail", "true")) && !user.IsDeleted)
{
if (user.EmailConfirmed)
{
diff --git a/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs b/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs
new file mode 100644
index 00000000..b119c2ee
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/10000101_AddSiteEnhancedNavigation.cs
@@ -0,0 +1,29 @@
+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.10.00.01.01")]
+ public class AddSiteEnhancedNavigation : MultiDatabaseMigration
+ {
+ public AddSiteEnhancedNavigation(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
+ siteEntityBuilder.AddBooleanColumn("EnhancedNavigation", true);
+ siteEntityBuilder.UpdateData("EnhancedNavigation", true);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // not implemented
+ }
+ }
+}
diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index 1236dde0..d14efd87 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -43,7 +43,7 @@
-
+
diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs
index e674be2e..a00fa5b1 100644
--- a/Oqtane.Shared/Models/Site.cs
+++ b/Oqtane.Shared/Models/Site.cs
@@ -115,6 +115,11 @@ namespace Oqtane.Models
///
public bool Hybrid { get; set; }
+ ///
+ /// Indicates if enhanced navigation should be used with static rendering
+ ///
+ public bool EnhancedNavigation { get; set; }
+
///
/// Keeps track of site configuration changes and is used by the ISiteMigration interface
///
@@ -222,6 +227,7 @@ namespace Oqtane.Models
Runtime = Runtime,
Prerender = Prerender,
Hybrid = Hybrid,
+ EnhancedNavigation = EnhancedNavigation,
Version = Version,
HomePageId = HomePageId,
HeadContent = HeadContent,
diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs
index 44cccffc..45888f0c 100644
--- a/Oqtane.Shared/Shared/Utilities.cs
+++ b/Oqtane.Shared/Shared/Utilities.cs
@@ -4,6 +4,8 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
+using System.Reflection;
+using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
@@ -750,6 +752,75 @@ namespace Oqtane.Shared
}
}
+ public static IEnumerable GetPropertiesIncludingInherited(Type type, BindingFlags bindingFlags)
+ {
+ var dictionary = new Dictionary(StringComparer.Ordinal);
+
+ var currentType = type;
+ while (currentType != null)
+ {
+ var properties = currentType.GetProperties(bindingFlags | BindingFlags.DeclaredOnly);
+ foreach (var property in properties)
+ {
+ if (!dictionary.TryGetValue(property.Name, out var others))
+ {
+ dictionary.Add(property.Name, property);
+ }
+ else if (!IsInheritedProperty(property, others))
+ {
+ List many;
+ if (others is PropertyInfo single)
+ {
+ many = new List { single };
+ dictionary[property.Name] = many;
+ }
+ else
+ {
+ many = (List)others;
+ }
+ many.Add(property);
+ }
+ }
+
+ currentType = currentType.BaseType;
+ }
+
+ foreach (var item in dictionary)
+ {
+ if (item.Value is PropertyInfo property)
+ {
+ yield return property;
+ continue;
+ }
+
+ var list = (List)item.Value;
+ var count = list.Count;
+ for (var i = 0; i < count; i++)
+ {
+ yield return list[i];
+ }
+ }
+ }
+
+ private static bool IsInheritedProperty(PropertyInfo property, object others)
+ {
+ if (others is PropertyInfo single)
+ {
+ return single.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition();
+ }
+
+ var many = (List)others;
+ foreach (var other in CollectionsMarshal.AsSpan(many))
+ {
+ if (other.GetMethod?.GetBaseDefinition() == property.GetMethod?.GetBaseDefinition())
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
[Obsolete("ContentUrl(Alias alias, int fileId) is deprecated. Use FileUrl(Alias alias, int fileId) instead.", false)]
public static string ContentUrl(Alias alias, int fileId)
{