diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index 05990fe4..e026b7b5 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -170,6 +170,7 @@ try { Folder folder; + if (_folderId != -1) { folder = await FolderService.GetFolderAsync(_folderId); @@ -179,8 +180,6 @@ folder = new Folder(); } - folder.SiteId = PageState.Site.SiteId; - if (_parentId == -1) { folder.ParentId = null; @@ -189,7 +188,15 @@ { folder.ParentId = _parentId; } + + // check for duplicate folder names + if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId)) + { + AddModuleMessage(Localizer["Message.Folder.Duplicate"], MessageType.Warning); + return; + } + folder.SiteId = PageState.Site.SiteId; folder.Name = _name; folder.Type = _type; folder.ImageSizes = _imagesizes; diff --git a/Oqtane.Client/Modules/Admin/Jobs/Index.razor b/Oqtane.Client/Modules/Admin/Jobs/Index.razor index bc5f5c60..ce5f2b55 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Index.razor @@ -26,8 +26,8 @@ else   - - + + @context.Name @DisplayStatus(context.IsEnabled, context.IsExecuting) @DisplayFrequency(context.Interval, context.Frequency) diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index 6932f784..dfb0cf8d 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -63,7 +63,7 @@ else @Localizer["Function"] - + @context.LogDate @context.Level @context.Feature diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index 9c1f4633..0081e3e8 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -32,7 +32,7 @@
- +
@@ -306,10 +306,9 @@ _languages = _languages.OrderBy(item => item.Name).ToList(); } - // Group modules by PageId - // Get distinct PageIds where modules are present + // get distinct pages where module exists var distinctPageIds = PageState.Modules - .Where(md => md.ModuleDefinition.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false) + .Where(md => md.ModuleDefinition?.ModuleDefinitionId == _moduleDefinitionId && md.IsDeleted == false) .Select(md => md.PageId) .Distinct(); diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor index 2d3b80a7..485f027f 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor @@ -50,7 +50,7 @@ else   - + @if (context.AssemblyName != Constants.ClientId) { diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 0031c729..277f223b 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -94,7 +94,7 @@
} - + @if (_permissions != null) {
@@ -126,9 +126,8 @@ - @code { +@code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; - public override string Title => "Module Settings"; private ElementReference form; private bool validated = false; @@ -144,7 +143,7 @@ private PermissionGrid _permissionGrid; private Type _moduleSettingsType; private object _moduleSettings; - private string _moduleSettingsTitle = "Module Settings"; + private string _moduleSettingsTitle; private RenderFragment ModuleSettingsComponent { get; set; } private Type _containerSettingsType; private object _containerSettings; @@ -158,8 +157,10 @@ protected override void OnInitialized() { + SetModuleTitle(Localizer["ModuleSettings.Title"]); _module = ModuleState.ModuleDefinition.Name; _title = ModuleState.Title; + _moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; _pane = ModuleState.Pane; _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); _containerType = ModuleState.ContainerType; @@ -172,7 +173,8 @@ modifiedon = ModuleState.ModifiedOn; _effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate); _expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate); - + + if (ModuleState.ModuleDefinition != null) { _permissionNames = ModuleState.ModuleDefinition?.PermissionNames; diff --git a/Oqtane.Client/Modules/Admin/Pages/Add.razor b/Oqtane.Client/Modules/Admin/Pages/Add.razor index edaebf1a..b4bc912e 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Add.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Add.razor @@ -198,12 +198,6 @@
- @if (_themeSettingsType != null) - { - - @_themeSettingsComponent - - }
@@ -238,9 +232,6 @@ private string _bodycontent; private string _permissions = null; private PermissionGrid _permissionGrid; - private Type _themeSettingsType; - private object _themeSettings; - private RenderFragment _themeSettingsComponent { get; set; } private bool _refresh = false; protected Page _parent = null; protected Dictionary _icons; @@ -281,7 +272,6 @@ } _effectivedate = Utilities.UtcAsLocalDate(PageState.Page.EffectiveDate); _expirydate = Utilities.UtcAsLocalDate(PageState.Page.ExpiryDate); - ThemeSettings(); _initialized = true; } else @@ -324,7 +314,6 @@ _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 @@ -334,28 +323,6 @@ } } - private void ThemeSettings() - { - _themeSettingsType = null; - _themeSettingsComponent = null; - var theme = PageState.Site.Themes.FirstOrDefault(item => item.Themes.Any(themecontrol => themecontrol.TypeName.Equals(_themetype))); - if (theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType)) - { - _themeSettingsType = Type.GetType(theme.ThemeSettingsType); - if (_themeSettingsType != null) - { - _themeSettingsComponent = builder => - { - builder.OpenComponent(0, _themeSettingsType); - builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary); - builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - builder.CloseComponent(); - }; - } - _refresh = true; - } - } - private async Task SavePage() { validated = true; @@ -482,11 +449,11 @@ await logger.LogInformation("Page Added {Page}", page); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) { - NavigationManager.NavigateTo(PageState.ReturnUrl, true); + NavigationManager.NavigateTo(page.Path, true); // redirect to page added and reload } else { - NavigationManager.NavigateTo(page.Path); // redirect to new page created + NavigationManager.NavigateTo(NavigateUrl()); // redirect to page management } } else diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 453f1f69..59e386b1 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -1,5 +1,6 @@ @namespace Oqtane.Modules.Admin.Pages @using Oqtane.Interfaces +@using System.Globalization @inherits ModuleBase @inject NavigationManager NavigationManager @inject IPageService PageService @@ -362,7 +363,7 @@ _parent = PageState.Pages.FirstOrDefault(item => item.PageId == _page.ParentId); } _children = new List(); - foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid)))) + foreach (Page p in PageState.Pages.Where(item => (_parentid == "-1" && item.ParentId == null) || (item.ParentId == int.Parse(_parentid, CultureInfo.InvariantCulture)))) { if (p.PageId != _pageId && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) { @@ -643,11 +644,11 @@ await logger.LogInformation("Page Saved {Page}", _page); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) { - NavigationManager.NavigateTo(PageState.ReturnUrl, true); + NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload } else { - NavigationManager.NavigateTo(NavigateUrl(), true); // redirect to page being edited + NavigationManager.NavigateTo(NavigateUrl()); // redirect to page management } } else diff --git a/Oqtane.Client/Modules/Admin/Pages/Index.razor b/Oqtane.Client/Modules/Admin/Pages/Index.razor index f1b9e9b4..391c6c42 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Index.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Index.razor @@ -17,7 +17,7 @@ @SharedLocalizer["Name"] - + @(new string('-', context.Level * 2))@(context.Name) diff --git a/Oqtane.Client/Modules/Admin/Profiles/Index.razor b/Oqtane.Client/Modules/Admin/Profiles/Index.razor index 587fa452..6152062d 100644 --- a/Oqtane.Client/Modules/Admin/Profiles/Index.razor +++ b/Oqtane.Client/Modules/Admin/Profiles/Index.razor @@ -22,7 +22,7 @@ else @Localizer["Order"] - + @context.Name @context.Title diff --git a/Oqtane.Client/Modules/Admin/Roles/Index.razor b/Oqtane.Client/Modules/Admin/Roles/Index.razor index 6b0e4610..44ffe0c3 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Index.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Index.razor @@ -20,9 +20,9 @@ else @SharedLocalizer["Name"] - + - + @context.Name diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 56df0b3e..a5626a5d 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -319,7 +319,7 @@
- @@ -337,7 +337,7 @@
- +
-
+
+ +
+ +
+
+
- +
@@ -103,7 +109,8 @@ else private int _page = 1; private List _visitors; private string _tracking; - private string _filter = ""; + private int _duration = 5; + private string _filter = ""; private int _retention = 30; private string _correlation = "true"; @@ -128,7 +135,8 @@ else _tracking = PageState.Site.VisitorTracking.ToString(); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - _filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); + _duration = int.Parse(SettingService.GetSetting(settings, "VisitorDuration", "5")); + _filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); _retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30")); _correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true"); } @@ -179,7 +187,8 @@ else await SiteService.UpdateSiteAsync(site); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); + settings = SettingService.SetSetting(settings, "VisitorDuration", _duration.ToString(), true); + settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index 1125d7b0..defb520d 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -35,11 +35,11 @@ { if (Disabled) { - + } else { - + } } } @@ -83,13 +83,13 @@ else { if (Disabled) { - + } else {
- +
} } @@ -101,6 +101,8 @@ else private bool _editmode = false; private bool _authorized = false; private string _iconSpan = string.Empty; + private string _openIconSpan = string.Empty; + private string _openText = string.Empty; [Parameter] public string Header { get; set; } // required @@ -138,6 +140,9 @@ else [Parameter] public string IconName { get; set; } // optional - specifies an icon for the link - default is no icon + [Parameter] + public bool IconOnly { get; set; } // optional - specifies only icon in opening link + [Parameter] public string Id { get; set; } // optional - specifies a unique id for the compoment - required when there are multiple component instances on a page in static rendering @@ -157,6 +162,8 @@ else { Text = Action; } + _openText = Text; + if (string.IsNullOrEmpty(Class)) { Class = "btn btn-success"; @@ -169,11 +176,17 @@ else if (!string.IsNullOrEmpty(IconName)) { + if (IconOnly) + { + _openText = string.Empty; + } + if (!IconName.Contains(" ")) { IconName = "oi oi-" + IconName; } - _iconSpan = $" "; + _openIconSpan = $"{(IconOnly ? "" : " ")}"; + _iconSpan = $" "; } Text = Localize(nameof(Text), Text); diff --git a/Oqtane.Client/Modules/Controls/ModuleMessage.razor b/Oqtane.Client/Modules/Controls/ModuleMessage.razor index d8155b3d..c42b40cb 100644 --- a/Oqtane.Client/Modules/Controls/ModuleMessage.razor +++ b/Oqtane.Client/Modules/Controls/ModuleMessage.razor @@ -6,23 +6,24 @@ { } @code { + private string _message = string.Empty; private string _classname = string.Empty; - private string _formname = "ModuleMessageForm"; [Parameter] public string Message { get; set; } @@ -30,32 +31,13 @@ [Parameter] public MessageType Type { get; set; } - public void RefreshMessage(string message, MessageType type) - { - Message = message; - Type = type; - - UpdateClassName(); - - StateHasChanged(); - } - - protected override void OnInitialized() - { - if (ModuleState != null) - { - _formname += ModuleState.PageModuleId.ToString(); - } - } + [Parameter] + public RenderModeBoundary Parent { get; set; } protected override void OnParametersSet() { - UpdateClassName(); - } - - private void UpdateClassName() - { - if (!string.IsNullOrEmpty(Message)) + _message = Message; + if (!string.IsNullOrEmpty(_message)) { _classname = GetMessageType(Type); } @@ -82,9 +64,15 @@ return classname; } - - private void DismissModal() + private void CloseMessage(MouseEventArgs e) { - Message = ""; + if(Parent != null) + { + Parent.DismissMessage(); + } + else + { + NavigationManager.NavigateTo(NavigationManager.Uri); + } } } diff --git a/Oqtane.Client/Modules/Controls/Pager.razor b/Oqtane.Client/Modules/Controls/Pager.razor index a37166ec..8e643f17 100644 --- a/Oqtane.Client/Modules/Controls/Pager.razor +++ b/Oqtane.Client/Modules/Controls/Pager.razor @@ -23,16 +23,16 @@ { } @@ -86,16 +86,16 @@ { } @@ -202,16 +202,16 @@ { } @@ -250,16 +250,16 @@ { } diff --git a/Oqtane.Client/Modules/Controls/RichTextEditor.razor b/Oqtane.Client/Modules/Controls/RichTextEditor.razor index ad237acb..83986c35 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditor.razor +++ b/Oqtane.Client/Modules/Controls/RichTextEditor.razor @@ -122,6 +122,7 @@ private string _message = string.Empty; private bool _contentchanged = false; + private int _editorIndex; [Parameter] public string Content { get; set; } @@ -173,7 +174,11 @@ _rawhtml = Content; _originalrawhtml = _rawhtml; // preserve for comparison later _originalrichhtml = ""; - _contentchanged = true; // identifies when Content parameter has changed + + if (Content != _originalrawhtml) + { + _contentchanged = true; // identifies when Content parameter has changed + } if (!AllowRichText) { @@ -275,18 +280,18 @@ // return original raw html content return _originalrawhtml; } - } - } + } + } - public async Task InsertRichImage() - { - _message = string.Empty; - if (_richfilemanager) - { - var file = _fileManager.GetFile(); + public async Task InsertRichImage() + { + _message = string.Empty; + if (_richfilemanager) + { + var file = _fileManager.GetFile(); if (file != null) { - await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name)); + await interop.InsertImage(_editorElement, file.Url, ((!string.IsNullOrEmpty(file.Description)) ? file.Description : file.Name), _editorIndex); _richhtml = await interop.GetHtml(_editorElement); _richfilemanager = false; } @@ -297,6 +302,7 @@ } else { + _editorIndex = await interop.GetCurrentCursor(_editorElement); _richfilemanager = true; } StateHasChanged(); diff --git a/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs b/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs index 6765cad9..338f240a 100644 --- a/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs +++ b/Oqtane.Client/Modules/Controls/RichTextEditorInterop.cs @@ -105,13 +105,25 @@ namespace Oqtane.Modules.Controls } } - public Task InsertImage(ElementReference quillElement, string imageUrl, string altText) + public ValueTask GetCurrentCursor(ElementReference quillElement) + { + try + { + return _jsRuntime.InvokeAsync("Oqtane.RichTextEditor.getCurrentCursor", quillElement); + } + catch + { + return new ValueTask(Task.FromResult(0)); + } + } + + public Task InsertImage(ElementReference quillElement, string imageUrl, string altText, int editorIndex) { try { _jsRuntime.InvokeAsync( "Oqtane.RichTextEditor.insertQuillImage", - quillElement, imageUrl, altText); + quillElement, imageUrl, altText, editorIndex); return Task.CompletedTask; } catch diff --git a/Oqtane.Client/Modules/HtmlText/Index.razor b/Oqtane.Client/Modules/HtmlText/Index.razor index ed45443b..6679cf83 100644 --- a/Oqtane.Client/Modules/HtmlText/Index.razor +++ b/Oqtane.Client/Modules/HtmlText/Index.razor @@ -37,6 +37,10 @@ content = htmltext.Content; content = Utilities.FormatContent(content, PageState.Alias, "render"); } + else + { + content = ""; + } } } catch (Exception ex) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index fee2d4e5..1731e22d 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -52,6 +52,8 @@ namespace Oqtane.Modules public virtual string RenderMode { get { return RenderModes.Interactive; } } // interactive by default + public virtual bool? Prerender { get { return null; } } // allows the Site Prerender property to be overridden + // url parameters public virtual string UrlParametersTemplate { get; set; } @@ -276,7 +278,6 @@ namespace Oqtane.Modules public void AddModuleMessage(string message, MessageType type, string position) { - ClearModuleMessage(); RenderModeBoundary.AddModuleMessage(message, type, position); } diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index f9989583..884b79a0 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net8.0 Exe Debug;Release - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -22,9 +22,9 @@ - - - + + + diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index cd3d71bb..a8d788c6 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -195,4 +195,7 @@ Folder Management + + Folder Name Specified Already Exists In Parent + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx index 722fa2f1..a91aa36e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx @@ -156,7 +156,7 @@ Module: - + Module Settings @@ -177,4 +177,16 @@ Expiry Date: + + Permissions + + + Permissions + + + Container Settings + + + Module Settings + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Profiles/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Profiles/Index.resx index 98f8530c..694119f3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Profiles/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Profiles/Index.resx @@ -147,4 +147,7 @@ Title + + Detail + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 4617bd91..babf23e0 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -277,10 +277,10 @@ UI Component Settings - Specifies if interactive components should prerender their output + Specifies if interactive components should prerender their output on the server - Prerender? + Prerender: The default render mode for the site diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx index 4ee3cbfe..188c09e3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Index.resx @@ -159,4 +159,7 @@ Url + + Edit + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx index 28bc46d1..37599ccb 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Visitors/Index.resx @@ -184,7 +184,7 @@ Number of days of visitor activity to retain - Retention (Days): + Retention: Indicate if new visitors to this site should be correlated based on their IP Address @@ -192,4 +192,10 @@ Correlate Visitors? + + The duration of a browsing session considered to be a distinct visit (in minutes) + + + Session Duration: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index dd4bc841..4161d868 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -453,4 +453,10 @@ Static + + Disabled + + + Enabled + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx index 9fa2eac6..15c567fc 100644 --- a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx +++ b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx @@ -198,4 +198,7 @@ Top + + Copy Existing Module + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Themes/Controls/ModuleActionsBase.resx b/Oqtane.Client/Resources/Themes/Controls/ModuleActionsBase.resx new file mode 100644 index 00000000..fe72e3ea --- /dev/null +++ b/Oqtane.Client/Resources/Themes/Controls/ModuleActionsBase.resx @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Delete Module + + + Export Content + + + Import Content + + + Manage Settings + + + Move Down + + + Move To Bottom + + + Move To Top + + + MoveUp + + + Publish Module + + + Unpublish Module + + \ No newline at end of file diff --git a/Oqtane.Client/Services/VisitorService.cs b/Oqtane.Client/Services/VisitorService.cs index 9febe7f9..b29e9c3c 100644 --- a/Oqtane.Client/Services/VisitorService.cs +++ b/Oqtane.Client/Services/VisitorService.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using Oqtane.Documentation; using Oqtane.Shared; using System; +using System.Globalization; namespace Oqtane.Services { @@ -18,7 +19,7 @@ namespace Oqtane.Services public async Task> GetVisitorsAsync(int siteId, DateTime fromDate) { - List visitors = await GetJsonAsync>($"{Apiurl}?siteid={siteId}&fromdate={fromDate.ToString("dd-MMM-yyyy")}"); + List visitors = await GetJsonAsync>($"{Apiurl}?siteid={siteId}&fromdate={fromDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"); return visitors.OrderByDescending(item => item.VisitedOn).ToList(); } diff --git a/Oqtane.Client/Themes/AdminContainer.razor b/Oqtane.Client/Themes/AdminContainer.razor index 60deaa59..7aec6816 100644 --- a/Oqtane.Client/Themes/AdminContainer.razor +++ b/Oqtane.Client/Themes/AdminContainer.razor @@ -1,6 +1,5 @@ @namespace Oqtane.Themes @inherits ContainerBase -@inject NavigationManager NavigationManager
@@ -291,7 +296,7 @@ _containerType = PageState.Site.DefaultContainerType; _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Site.SiteId); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); - _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',')).Distinct().ToList(); + _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); } } @@ -334,6 +339,13 @@ StateHasChanged(); } + private void ModuleTypeChanged(ChangeEventArgs e) + { + _moduleType = (string)e.Value; + _pageId = "-"; + _moduleId = "-"; + } + private void PageChanged(ChangeEventArgs e) { _pageId = (string)e.Value; @@ -341,7 +353,8 @@ { _modules = PageState.Modules .Where(module => module.PageId == int.Parse(_pageId) && - UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList)) + UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) && + (_moduleType == "add" || module.ModuleDefinition.IsPortable)) .ToList(); } _moduleId = "-"; @@ -354,6 +367,7 @@ { if ((_moduleType == "new" && _moduleDefinitionName != "-") || (_moduleType != "new" && _moduleId != "-")) { + var newModuleId = _moduleId != "-" ? int.Parse(_moduleId) : 0; if (_moduleType == "new") { Module module = new Module(); @@ -361,33 +375,37 @@ module.PageId = PageState.Page.PageId; module.ModuleDefinitionName = _moduleDefinitionName; module.AllPages = false; - - var permissions = new List(); - if (_visibility == "view") - { - // set module view permissions to page view permissions - permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.View); - } - else - { - // set module view permissions to page edit permissions - permissions = SetPermissions(permissions, module.SiteId, PermissionNames.View, PermissionNames.Edit); - } - // set module edit permissions to page edit permissions - permissions = SetPermissions(permissions, module.SiteId, PermissionNames.Edit, PermissionNames.Edit); - module.PermissionList = permissions; + module.PermissionList = GenerateDefaultPermissions(module.SiteId); module = await ModuleService.AddModuleAsync(module); - _moduleId = module.ModuleId.ToString(); + newModuleId = module.ModuleId; + } + else if (_moduleType == "copy") + { + var module = await ModuleService.GetModuleAsync(int.Parse(_moduleId)); + module.ModuleId = 0; + module.SiteId = PageState.Site.SiteId; + module.PageId = PageState.Page.PageId; + module.AllPages = false; + module.PermissionList = GenerateDefaultPermissions(module.SiteId); + + module = await ModuleService.AddModuleAsync(module); + var moduleContent = await ModuleService.ExportModuleAsync(int.Parse(_moduleId), PageState.Page.PageId); + if (!string.IsNullOrEmpty(moduleContent)) + { + await ModuleService.ImportModuleAsync(module.ModuleId, PageState.Page.PageId, moduleContent); + } + + newModuleId = module.ModuleId; } var pageModule = new PageModule { PageId = PageState.Page.PageId, - ModuleId = int.Parse(_moduleId), + ModuleId = newModuleId, Title = _title }; - if (pageModule.Title == "") + if (string.IsNullOrEmpty(pageModule.Title)) { if (_moduleType == "new") { @@ -412,9 +430,16 @@ await PageModuleService.UpdatePageModuleOrderAsync(pageModule.PageId, pageModule.Pane); await UpdateSettingsAsync(); - _message = $"
{Localizer["Success.Page.ModuleAdd"]}
"; - _title = ""; - NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "")); + if (PageState.RenderMode == RenderModes.Interactive) + { + _message = $"
{Localizer["Success.Page.ModuleAdd"]}
"; + _title = ""; + NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "")); + } + else // reload page in static rendering + { + NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true); + } } else { @@ -427,6 +452,25 @@ } } + private List GenerateDefaultPermissions(int siteId) + { + var permissions = new List(); + if (_visibility == "view") + { + // set module view permissions to page view permissions + permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.View); + } + else + { + // set module view permissions to page edit permissions + permissions = SetPermissions(permissions, siteId, PermissionNames.View, PermissionNames.Edit); + } + // set module edit permissions to page edit permissions + permissions = SetPermissions(permissions, siteId, PermissionNames.Edit, PermissionNames.Edit); + + return permissions; + } + private List SetPermissions(List permissions, int siteId, string modulePermission, string pagePermission) { foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission)) diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index e59c9f2e..c02548e0 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -1,21 +1,29 @@ -@namespace Oqtane.Themes.Controls -@inherits ThemeControlBase @using System.Globalization @using Microsoft.AspNetCore.Localization +@using Microsoft.AspNetCore.Http @using Oqtane.Models +@namespace Oqtane.Themes.Controls +@inherits ThemeControlBase @inject ILanguageService LanguageService @inject NavigationManager NavigationManager @if (_supportedCultures?.Count() > 1) {
-
@@ -23,9 +31,15 @@ @code{ private IEnumerable _supportedCultures; + private string MenuAlignment = string.Empty; + [Parameter] public string DropdownAlignment { get; set; } = string.Empty; // Empty or Left or Right - private string MenuAlignment = string.Empty; + [Parameter] + public string ButtonClass { get; set; } = "btn-outline-secondary"; + + [CascadingParameter] + HttpContext HttpContext { get; set; } protected override void OnParametersSet() { @@ -33,16 +47,26 @@ var languages = PageState.Languages; _supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); + + if (PageState.QueryString.ContainsKey("culture")) + { + var culture = PageState.QueryString["culture"]; + if (_supportedCultures.Any(item => item.Name == culture)) + { + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { Path = "/", Expires = DateTimeOffset.UtcNow.AddYears(365) }); + } + NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""), forceLoad: true); + } } private async Task SetCultureAsync(string culture) { if (culture != CultureInfo.CurrentUICulture.Name) { - var interop = new Interop(JSRuntime); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + var interop = new Interop(JSRuntime); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); - NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); } } diff --git a/Oqtane.Client/UI/ModuleInstance.razor b/Oqtane.Client/UI/ModuleInstance.razor index 5d429954..8dfec04b 100644 --- a/Oqtane.Client/UI/ModuleInstance.razor +++ b/Oqtane.Client/UI/ModuleInstance.razor @@ -1,13 +1,17 @@ @namespace Oqtane.UI @inject SiteState SiteState -@if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static) +@if (_comment != null) { - -} -else -{ - + @((MarkupString)_comment) + @if (PageState.RenderMode == RenderModes.Interactive || ModuleState.RenderMode == RenderModes.Static) + { + + } + else + { + + } } @code { @@ -20,6 +24,24 @@ else [CascadingParameter] private Module ModuleState { get; set; } + private bool _prerender; + private string _comment; + + protected override void OnParametersSet() + { + _prerender = ModuleState.Prerender ?? PageState.Site.Prerender; + _comment = ""; + } + [Obsolete("AddModuleMessage is deprecated. Use AddModuleMessage in ModuleBase instead.", false)] public void AddModuleMessage(string message, MessageType type) diff --git a/Oqtane.Client/UI/RenderModeBoundary.razor b/Oqtane.Client/UI/RenderModeBoundary.razor index e4d6873e..2c451f29 100644 --- a/Oqtane.Client/UI/RenderModeBoundary.razor +++ b/Oqtane.Client/UI/RenderModeBoundary.razor @@ -10,14 +10,19 @@ { @if (ModuleType != null) { - @((MarkupString)$"") - + @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "top") + { + + } @DynamicComponent @if (_progressIndicator) {
} - + @if (!string.IsNullOrEmpty(_messageContent) && _messagePosition == "bottom") + { + + } } } else @@ -42,8 +47,6 @@ private string _messagePosition; private bool _progressIndicator = false; private string _error; - private ModuleMessage moduleMessageTop; - private ModuleMessage moduleMessageBottom; [Parameter] public SiteState SiteState { get; set; } @@ -104,12 +107,17 @@ public void AddModuleMessage(string message, MessageType type, string position) { - _messageContent = message; - _messageType = type; - _messagePosition = position; - _progressIndicator = false; + if(message != _messageContent + || type != _messageType + || position != _messagePosition) + { + _messageContent = message; + _messageType = type; + _messagePosition = position; + _progressIndicator = false; - Refresh(); + StateHasChanged(); + } } public void ShowProgressIndicator() @@ -124,25 +132,10 @@ StateHasChanged(); } - private void DismissMessage() + public void DismissMessage() { _messageContent = ""; - } - - private void Refresh() - { - var updateTop = string.IsNullOrEmpty(_messageContent) || _messagePosition == "top"; - var updateBottom = string.IsNullOrEmpty(_messageContent) || _messagePosition == "bottom"; - - if (updateTop && moduleMessageTop != null) - { - moduleMessageTop.RefreshMessage(_messageContent, _messageType); - } - - if (updateBottom && moduleMessageBottom != null) - { - moduleMessageBottom.RefreshMessage(_messageContent, _messageType); - } + StateHasChanged(); } protected override async Task OnErrorAsync(Exception exception) diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index 43acc3a6..9e13eeb8 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -1,6 +1,7 @@ @using System.Diagnostics.CodeAnalysis @using System.Net @using Microsoft.AspNetCore.Http +@using System.Globalization @namespace Oqtane.UI @inject AuthenticationStateProvider AuthenticationStateProvider @inject SiteState SiteState @@ -103,7 +104,7 @@ _error = ""; Route route = new Route(_absoluteUri, SiteState.Alias.Path); - int moduleid = int.Parse(route.ModuleId); + int moduleid = int.Parse(route.ModuleId, CultureInfo.InvariantCulture); var action = route.Action; var querystring = Utilities.ParseQueryString(route.Query); @@ -263,7 +264,7 @@ } else { - editmode = (page.PageId == ((user.Settings.ContainsKey("CP-editmode")) ? int.Parse(user.Settings["CP-editmode"]) : -1)); + editmode = (page.PageId == ((user.Settings.ContainsKey("CP-editmode")) ? int.Parse(user.Settings["CP-editmode"], CultureInfo.InvariantCulture) : -1)); if (!editmode) { var userSettings = new Dictionary { { "CP-editmode", "-1" } }; @@ -476,6 +477,7 @@ // retrieve module component resources var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; module.RenderMode = moduleobject.RenderMode; + module.Prerender = moduleobject.Prerender; page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); if (action.ToLower() == "settings" && module.ModuleDefinition != null) @@ -549,7 +551,7 @@ { foreach (var resource in resources) { - if (resource.Level != ResourceLevel.Site) + if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site) { if (resource.Url.StartsWith("~")) { diff --git a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj index 54cf3a8e..71019446 100644 --- a/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj +++ b/Oqtane.Database.MySQL/Oqtane.Database.MySQL.csproj @@ -2,7 +2,7 @@ net8.0 - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git true diff --git a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj index ca98e444..04093e5a 100644 --- a/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj +++ b/Oqtane.Database.PostgreSQL/Oqtane.Database.PostgreSQL.csproj @@ -2,7 +2,7 @@ net8.0 - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git true @@ -34,8 +34,8 @@ - - + + diff --git a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj index ff2404d0..f742b253 100644 --- a/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj +++ b/Oqtane.Database.SqlServer/Oqtane.Database.SqlServer.csproj @@ -2,7 +2,7 @@ net8.0 - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git true @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj index 3af139ac..80e4e18d 100644 --- a/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj +++ b/Oqtane.Database.Sqlite/Oqtane.Database.Sqlite.csproj @@ -2,7 +2,7 @@ net8.0 - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -10,7 +10,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git true @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 938aaa4f..a7a79ea3 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -6,7 +6,7 @@ Exe - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -31,7 +31,7 @@ 0E29FC31-1B83-48ED-B6E0-9F3C67B775D4 - 5.1.1 + 5.1.2 1 14.2 @@ -65,15 +65,15 @@ - - + + - + - - - + + + diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 49cf9bd5..4b9d0a44 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 5.1.1 + 5.1.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index cfaef73d..d81c1693 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 5.1.1 + 5.1.2 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v5.1.1/Oqtane.Framework.5.1.1.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/download/v5.1.2/Oqtane.Framework.5.1.2.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 93c2942e..7a489cfb 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 5.1.1 + 5.1.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index ac34a811..ab1deb4d 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 5.1.1 + 5.1.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index fe7d85bd..2c51fc0e 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 5.1.1 + 5.1.2 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index ed93995d..57e2d394 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.1.Install.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.2.Install.zip" -Force \ No newline at end of file diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index c5c23da0..0c0d2f56 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.1.Upgrade.zip" -Force \ No newline at end of file +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.1.2.Upgrade.zip" -Force \ No newline at end of file diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 25f2f972..c32e1f97 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -16,6 +16,7 @@ @using Oqtane.Shared @using Oqtane.Themes @using Oqtane.Extensions +@using System.Globalization @inject NavigationManager NavigationManager @inject IAntiforgery Antiforgery @inject IConfigManager ConfigManager @@ -39,7 +40,7 @@ - @if (!string.IsNullOrEmpty(_PWAScript)) + @if (_scripts.Contains("PWA Manifest")) { } @@ -68,20 +69,13 @@ } - @if (!string.IsNullOrEmpty(_reconnectScript)) - { - @((MarkupString)_reconnectScript) - } - @if (!string.IsNullOrEmpty(_PWAScript)) - { - @((MarkupString)_PWAScript) - } - @((MarkupString)_bodyResources) - + - + + @((MarkupString)_scripts) + @((MarkupString)_bodyResources) } else { @@ -105,8 +99,7 @@ private string _headResources = ""; private string _bodyResources = ""; private string _styleSheets = ""; - private string _PWAScript = ""; - private string _reconnectScript = ""; + private string _scripts = ""; private string _message = ""; private PageState _pageState; @@ -175,23 +168,25 @@ CreateJwtToken(alias); } - // include stylesheets to prevent FOUC - var resources = GetPageResources(alias, site, page, int.Parse(route.ModuleId), route.Action); + // includes resources + var resources = GetPageResources(alias, site, page, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action); ManageStyleSheets(resources); + ManageScripts(resources, alias); - // scripts - if (_renderMode == RenderModes.Static) - { - ManageScripts(resources, alias); - } + // generate scripts if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server) { - _reconnectScript = CreateReconnectScript(); + _scripts += CreateReconnectScript(); } if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) { - _PWAScript = CreatePWAScript(alias, site, route); + _scripts += CreatePWAScript(alias, site, route); } + @if (_renderMode == RenderModes.Static) + { + _scripts += CreateScrollPositionScript(); + } + _headResources += ParseScripts(site.HeadContent); _bodyResources += ParseScripts(site.BodyContent); @@ -329,14 +324,26 @@ int? userid = Context.User.UserId(); userid = (userid == -1) ? null : userid; - // check if cookie already exists + // get cookie value + var visitorCookieName = Constants.VisitorCookiePrefix + SiteId.ToString(); + var visitorCookieValue = Context.Request.Cookies[visitorCookieName]; + DateTime expiry = DateTime.MinValue; + if (visitorCookieValue != null && visitorCookieValue.Contains("|")) + { + var values = visitorCookieValue.Split('|'); + int.TryParse(values[0], out _visitorId); + DateTime.TryParse(values[1], out expiry); + } + else // legacy cookie format + { + int.TryParse(visitorCookieValue, out _visitorId); + } + bool setcookie = false; Visitor visitor = null; - bool addcookie = false; - var VisitorCookie = Constants.VisitorCookiePrefix + SiteId.ToString(); - if (!int.TryParse(Context.Request.Cookies[VisitorCookie], out _visitorId)) + + if (_visitorId <= 0) { // if enabled use IP Address correlation - _visitorId = -1; var correlate = bool.Parse(settings.GetValue("VisitorCorrelation", "true")); if (correlate) { @@ -344,12 +351,12 @@ if (visitor != null) { _visitorId = visitor.VisitorId; - addcookie = true; + setcookie = true; } } } - if (_visitorId == -1) + if (_visitorId <= 0) { // create new visitor visitor = new Visitor(); @@ -365,52 +372,59 @@ visitor.VisitedOn = DateTime.UtcNow; visitor = VisitorRepository.AddVisitor(visitor); _visitorId = visitor.VisitorId; - addcookie = true; + setcookie = true; } else { - if (visitor == null) + // check expiry + if (DateTime.UtcNow > expiry) { - // get visitor if it was not previously loaded - visitor = VisitorRepository.GetVisitor(_visitorId); - } - if (visitor != null) - { - // update visitor - visitor.IPAddress = _remoteIPAddress; - visitor.UserAgent = useragent; - visitor.Language = language; - visitor.Url = url; - if (!string.IsNullOrEmpty(referrer)) + if (visitor == null) { - visitor.Referrer = referrer; + // get visitor if not previously loaded + visitor = VisitorRepository.GetVisitor(_visitorId); } - if (userid != null) + if (visitor != null) { - visitor.UserId = userid; + // update visitor + visitor.IPAddress = _remoteIPAddress; + visitor.UserAgent = useragent; + visitor.Language = language; + visitor.Url = url; + if (!string.IsNullOrEmpty(referrer)) + { + visitor.Referrer = referrer; + } + if (userid != null) + { + visitor.UserId = userid; + } + visitor.Visits += 1; + visitor.VisitedOn = DateTime.UtcNow; + VisitorRepository.UpdateVisitor(visitor); + setcookie = true; + } + else + { + // remove cookie if visitor does not exist + Context.Response.Cookies.Delete(visitorCookieName); } - visitor.Visits += 1; - visitor.VisitedOn = DateTime.UtcNow; - VisitorRepository.UpdateVisitor(visitor); - } - else - { - // remove cookie if VisitorId does not exist - Context.Response.Cookies.Delete(VisitorCookie); } } - // append cookie - if (addcookie) + // set cookie + if (setcookie) { + expiry = DateTime.UtcNow.AddMinutes(int.Parse(settings.GetValue("VisitorDuration", "5"))); + Context.Response.Cookies.Append( - VisitorCookie, - _visitorId.ToString(), + visitorCookieName, + $"{_visitorId}|{expiry}", new CookieOptions() - { - Expires = DateTimeOffset.UtcNow.AddYears(1), - IsEssential = true - } + { + Expires = DateTimeOffset.UtcNow.AddYears(10), + IsEssential = true + } ); } } @@ -432,7 +446,7 @@ private string CreatePWAScript(Alias alias, Site site, Route route) { - return + return Environment.NewLine + ""; + "" + Environment.NewLine; } private string CreateReconnectScript() { - return + return Environment.NewLine + ""; + "" + Environment.NewLine; + } + + private string CreateScrollPositionScript() + { + return Environment.NewLine + + "" + Environment.NewLine; } private string ParseScripts(string content) @@ -536,7 +569,7 @@ ((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") + ((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") + ((resource.ES6Module) ? " type=\"module\"" : "") + - " src =\"" + url + "\">"; // src at end of element due to enhanced navigation patch algorithm + " src=\"" + url + "\">"; // src at end of element due to enhanced navigation patch algorithm } else { @@ -566,13 +599,13 @@ var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); if (theme != null) { - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); } else { // fallback to default Oqtane theme theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); - resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); + resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); } var type = Type.GetType(themeType); if (type != null) @@ -580,7 +613,7 @@ var obj = Activator.CreateInstance(type) as IThemeControl; if (obj != null) { - resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace); + resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode); } } @@ -589,7 +622,7 @@ var typename = ""; if (module.ModuleDefinition != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); + resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); // handle default action if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) @@ -635,7 +668,7 @@ var obj = Activator.CreateInstance(moduletype) as IModuleControl; if (obj != null) { - resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); + resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); if (action.ToLower() == "settings" && module.ModuleDefinition != null) { // settings components are embedded within a framework settings module @@ -643,7 +676,7 @@ if (moduletype != null) { obj = Activator.CreateInstance(moduletype) as IModuleControl; - resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); + resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); } } } @@ -656,32 +689,35 @@ { if (module.ModuleDefinition?.Resources != null) { - resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); + resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); } } return resources; } - private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name) + private List AddResources(List pageresources, List resources, ResourceLevel level, Alias alias, string type, string name, string rendermode) { if (resources != null) { foreach (var resource in resources) { - if (resource.Url.StartsWith("~")) + if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site) { - resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); - } - if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) - { - resource.Url = alias.BaseUrl + resource.Url; - } + if (resource.Url.StartsWith("~")) + { + resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); + } + if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) + { + resource.Url = alias.BaseUrl + resource.Url; + } - // ensure resource does not exist already - if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) - { - pageresources.Add(resource.Clone(level, name)); + // ensure resource does not exist already + if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) + { + pageresources.Add(resource.Clone(level, name)); + } } } } @@ -692,6 +728,7 @@ { if (resources != null) { + // include stylesheets to prevent FOUC string batch = DateTime.UtcNow.ToString("yyyyMMddHHmmssfff"); int count = 0; foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Stylesheet)) diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index aeeda0b2..4f5bcb2f 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -760,7 +760,7 @@ namespace Oqtane.Controllers { if (!Directory.Exists(folderpath)) { - string path = ""; + string path = folderpath.StartsWith(Path.DirectorySeparatorChar) ? Path.DirectorySeparatorChar.ToString() : string.Empty; var separators = new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; string[] folders = folderpath.Split(separators, StringSplitOptions.RemoveEmptyEntries); foreach (string folder in folders) diff --git a/Oqtane.Server/Controllers/VisitorController.cs b/Oqtane.Server/Controllers/VisitorController.cs index 46bf8935..224aaa5f 100644 --- a/Oqtane.Server/Controllers/VisitorController.cs +++ b/Oqtane.Server/Controllers/VisitorController.cs @@ -8,6 +8,7 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using System.Net; using System; +using System.Globalization; namespace Oqtane.Controllers { @@ -33,7 +34,7 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - return _visitors.GetVisitors(SiteId, DateTime.Parse(fromdate)); + return _visitors.GetVisitors(SiteId, DateTime.ParseExact(fromdate, "yyyy-MM-dd", CultureInfo.InvariantCulture)); } else { diff --git a/Oqtane.Server/Extensions/ComponentEndpointRouteBuilderExtensions.cs b/Oqtane.Server/Extensions/ComponentEndpointRouteBuilderExtensions.cs index b464e81d..77731577 100644 --- a/Oqtane.Server/Extensions/ComponentEndpointRouteBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ComponentEndpointRouteBuilderExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Routing; using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Antiforgery; namespace OqtaneSSR.Extensions { @@ -23,6 +24,7 @@ namespace OqtaneSSR.Extensions { routeEndpointBuilder.Metadata.Add(new RootComponentMetadata(typeof(App))); routeEndpointBuilder.Metadata.Add(new ComponentTypeMetadata(typeof(App))); + routeEndpointBuilder.Metadata.Add(new RequireAntiforgeryTokenAttribute()); }); } } diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 043d7aa2..ad472f37 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -539,6 +539,8 @@ namespace Oqtane.Infrastructure var identityUserManager = scope.ServiceProvider.GetRequiredService>(); var tenant = tenants.GetTenants().FirstOrDefault(item => item.Name == install.TenantName); + var rendermode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value; + var runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value; site = new Site { @@ -556,9 +558,9 @@ namespace Oqtane.Infrastructure DefaultContainerType = (!string.IsNullOrEmpty(install.DefaultContainer)) ? install.DefaultContainer : Constants.DefaultContainer, AdminContainerType = (!string.IsNullOrEmpty(install.DefaultAdminContainer)) ? install.DefaultAdminContainer : Constants.DefaultAdminContainer, SiteTemplateType = install.SiteTemplate, - RenderMode = (!string.IsNullOrEmpty(install.RenderMode)) ? install.RenderMode : _configManager.GetSection("RenderMode").Value, - Runtime = (!string.IsNullOrEmpty(install.Runtime)) ? install.Runtime : _configManager.GetSection("Runtime").Value, - Prerender = true, + RenderMode = rendermode, + Runtime = runtime, + Prerender = (rendermode == RenderModes.Interactive), Hybrid = false }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index 1307b57f..fdfca028 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -197,6 +197,12 @@ namespace Oqtane.Infrastructure string[] segments = entry.FullName.Split('/'); // ZipArchiveEntries always use unix path separator string filename = Path.Combine(folder, string.Join(Path.DirectorySeparatorChar, segments, ignoreLeadingSegments, segments.Length - ignoreLeadingSegments)); + // validate path to prevent path traversal + if (!Path.GetFullPath(filename).StartsWith(folder + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + { + return ""; + } + try { if (!Directory.Exists(Path.GetDirectoryName(filename))) @@ -227,6 +233,7 @@ namespace Oqtane.Infrastructure // an error occurred extracting the file filename = ""; } + return filename; } diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index cd8f198f..2d763f10 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -201,56 +201,54 @@ namespace Oqtane.Managers IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username); if (identityuser != null) { - var valid = true; if (!string.IsNullOrEmpty(user.Password)) { var validator = new PasswordValidator(); var result = await validator.ValidateAsync(_identityUserManager, null, user.Password); - valid = result.Succeeded; - if (valid) + if (result.Succeeded) { identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password); + await _identityUserManager.UpdateAsync(identityuser); } - } - if (valid) - { - if (!string.IsNullOrEmpty(user.Password)) + else { - await _identityUserManager.UpdateAsync(identityuser); // requires password to be provided + _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username); + return null; } - - if (user.Email != identityuser.Email) - { - await _identityUserManager.SetEmailAsync(identityuser, user.Email); - - // 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); - _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); - _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); - user.Password = ""; // remove sensitive information - _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); } - else + + if (user.Email != identityuser.Email) { - _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username); - user = null; + await _identityUserManager.SetEmailAsync(identityuser, user.Email); + + // if email address changed and it is not confirmed, 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); + } } + + if (user.EmailConfirmed) + { + var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + } + + user = _users.UpdateUser(user); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); + user.Password = ""; // remove sensitive information + _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); + } + else + { + _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. User Does Not Exist.", user.Username); + user = null; } return user; diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 959f4d07..6c9293fb 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -7,6 +7,7 @@ using Oqtane.Repository; using Oqtane.Shared; using Oqtane.Migrations.Framework; using Oqtane.Documentation; +using System.Linq; // ReSharper disable ConvertToUsingDeclaration @@ -29,10 +30,11 @@ namespace Oqtane.Modules.HtmlText.Manager public string ExportModule(Module module) { string content = ""; - var htmlText = _htmlText.GetHtmlText(module.ModuleId); - if (htmlText != null) + var htmltexts = _htmlText.GetHtmlTexts(module.ModuleId); + if (htmltexts != null && htmltexts.Any()) { - content = WebUtility.HtmlEncode(htmlText.Content); + var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First(); + content = WebUtility.HtmlEncode(htmltext.Content); } return content; } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 7fe28c3d..f661e715 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -33,19 +33,19 @@ - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + diff --git a/Oqtane.Server/Pages/Sitemap.cshtml.cs b/Oqtane.Server/Pages/Sitemap.cshtml.cs index c79957c3..4debad0c 100644 --- a/Oqtane.Server/Pages/Sitemap.cshtml.cs +++ b/Oqtane.Server/Pages/Sitemap.cshtml.cs @@ -46,13 +46,16 @@ namespace Oqtane.Pages { var sitemap = new List(); + // internal pages which should not be indexed + string[] internalPaths = { "login", "register", "reset", "404" }; + // build site map var rooturl = _alias.Protocol + (string.IsNullOrEmpty(_alias.Path) ? _alias.Name : _alias.Name.Substring(0, _alias.Name.IndexOf("/"))); var moduleDefinitions = _moduleDefinitions.GetModuleDefinitions(_alias.SiteId).ToList(); var pageModules = _pageModules.GetPageModules(_alias.SiteId); foreach (var page in _pages.GetPages(_alias.SiteId)) { - if (_userPermissions.IsAuthorized(null, PermissionNames.View, page.PermissionList) && page.IsNavigation) + if (_userPermissions.IsAuthorized(null, PermissionNames.View, page.PermissionList) && !internalPaths.Contains(page.Path)) { var pageurl = rooturl; if (string.IsNullOrEmpty(page.Url)) diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index 563a5a1d..f20c3d13 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -287,7 +287,9 @@ namespace Oqtane.Repository { ModuleDefinition moduledefinition; + Type[] moduletypes = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModule))).ToArray(); Type[] modulecontroltypes = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IModuleControl))).ToArray(); + foreach (Type modulecontroltype in modulecontroltypes) { // Check if type should be ignored @@ -299,12 +301,9 @@ namespace Oqtane.Repository int index = moduledefinitions.FindIndex(item => item.ModuleDefinitionName == qualifiedModuleType); if (index == -1) { - // determine if this module implements IModule - Type moduletype = assembly - .GetTypes() - .Where(item => item.Namespace != null) - .Where(item => item.Namespace == modulecontroltype.Namespace || item.Namespace.StartsWith(modulecontroltype.Namespace + ".")) - .FirstOrDefault(item => item.GetInterfaces().Contains(typeof(IModule))); + // determine if this component is part of a module which implements IModule + Type moduletype = moduletypes.FirstOrDefault(item => item.Namespace == modulecontroltype.Namespace); + if (moduletype != null) { // get property values from IModule @@ -399,6 +398,22 @@ namespace Oqtane.Repository moduledefinitions[index] = moduledefinition; } + // process modules without UI components + foreach (var moduletype in moduletypes.Where(m1 => !modulecontroltypes.Any(m2 => m1.Namespace == m2.Namespace))) + { + // get property values from IModule + var moduleobject = Activator.CreateInstance(moduletype) as IModule; + moduledefinition = moduleobject.ModuleDefinition; + moduledefinition.ModuleDefinitionName = moduletype.Namespace + ", " + moduletype.Assembly.GetName().Name; + moduledefinition.AssemblyName = assembly.GetName().Name; + moduledefinition.Categories = "Headless"; + moduledefinition.PermissionList = new List + { + new Permission(PermissionNames.Utilize, RoleNames.Host, true) + }; + moduledefinitions.Add(moduledefinition); + } + return moduledefinitions; } diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 59f5db3c..3328461b 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -165,22 +165,23 @@ namespace Oqtane.Repository if (!serverstate.IsInitialized) { var site = GetSite(alias.SiteId); - - // initialize theme Assemblies - site.Themes = _themeRepository.GetThemes().ToList(); - - // initialize module Assemblies - var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId); - - // execute migrations - var version = ProcessSiteMigrations(alias, site); - version = ProcessPageTemplates(alias, site, moduleDefinitions, version); - if (site.Version != version) + if (site != null) { - site.Version = version; - UpdateSite(site); - } + // initialize theme Assemblies + site.Themes = _themeRepository.GetThemes().ToList(); + // initialize module Assemblies + var moduleDefinitions = _moduleDefinitionRepository.GetModuleDefinitions(alias.SiteId); + + // execute migrations + var version = ProcessSiteMigrations(alias, site); + version = ProcessPageTemplates(alias, site, moduleDefinitions, version); + if (site.Version != version) + { + site.Version = version; + UpdateSite(site); + } + } serverstate.IsInitialized = true; } } @@ -411,7 +412,7 @@ namespace Oqtane.Repository } else { - parent = pages.FirstOrDefault(item => item.Path.ToLower() == pageTemplate.Parent.ToLower()); + parent = pages.FirstOrDefault(item => item.Path.ToLower() == ((pageTemplate.Parent == "/") ? "" : pageTemplate.Parent.ToLower())); } page.ParentId = (parent != null) ? parent.PageId : null; page.Path = page.Path.ToLower(); @@ -487,7 +488,11 @@ namespace Oqtane.Repository pageModule.Order = (pageTemplateModule.Order == 0) ? 1 : pageTemplateModule.Order; pageModule.ContainerType = pageTemplateModule.ContainerType; pageModule.IsDeleted = pageTemplateModule.IsDeleted; - pageModule.Module.PermissionList = pageTemplateModule.PermissionList; + pageModule.Module.PermissionList = new List(); + foreach (var permission in pageTemplateModule.PermissionList) + { + pageModule.Module.PermissionList.Add(permission.Clone(permission)); + } pageModule.Module.AllPages = false; pageModule.Module.IsDeleted = false; try @@ -539,8 +544,11 @@ namespace Oqtane.Repository try { var module = _moduleRepository.GetModule(pageModule.ModuleId); - var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); - ((IPortable)moduleobject).ImportModule(module, pageTemplateModule.Content, moduleDefinition.Version); + if (module != null) + { + var moduleobject = ActivatorUtilities.CreateInstance(_serviceProvider, moduletype); + ((IPortable)moduleobject).ImportModule(module, pageTemplateModule.Content, moduleDefinition.Version); + } } catch (Exception ex) { diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index ae44815d..bd557bdd 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -13,6 +13,7 @@ using Oqtane.Shared; using Oqtane.Themes; using System.Reflection.Metadata; using Oqtane.Migrations.Master; +using Oqtane.Modules; namespace Oqtane.Repository { @@ -224,9 +225,11 @@ namespace Oqtane.Repository private List LoadThemesFromAssembly(List themes, Assembly assembly) { Theme theme; - List themeTypes = new List(); + Type[] themeTypes = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(ITheme))).ToArray(); Type[] themeControlTypes = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IThemeControl))).ToArray(); + Type[] containerControlTypes = assembly.GetTypes().Where(item => item.GetInterfaces().Contains(typeof(IContainerControl))).ToArray(); + foreach (Type themeControlType in themeControlTypes) { // Check if type should be ignored @@ -240,16 +243,9 @@ namespace Oqtane.Repository int index = themes.FindIndex(item => item.ThemeName == qualifiedThemeType); if (index == -1) { - // Find all types in the assembly with the same namespace root - themeTypes = assembly.GetTypes() - .Where(item => !item.IsOqtaneIgnore()) - .Where(item => item.Namespace != null) - .Where(item => item.Namespace == themeControlType.Namespace || item.Namespace.StartsWith(themeControlType.Namespace + ".")) - .ToList(); + // determine if this component is part of a theme which implements ITheme + Type themetype = themeTypes.FirstOrDefault(item => item.Namespace == themeControlType.Namespace); - // determine if this theme implements ITheme - Type themetype = themeTypes - .FirstOrDefault(item => item.GetInterfaces().Contains(typeof(ITheme))); if (themetype != null) { var themeobject = Activator.CreateInstance(themetype) as ITheme; @@ -285,6 +281,7 @@ namespace Oqtane.Repository } theme = themes[index]; + // add theme control var themecontrolobject = Activator.CreateInstance(themeControlType) as IThemeControl; theme.Themes.Add( new ThemeControl @@ -296,14 +293,12 @@ namespace Oqtane.Repository } ); - // containers - Type[] containertypes = themeTypes - .Where(item => item.GetInterfaces().Contains(typeof(IContainerControl))).ToArray(); - foreach (Type containertype in containertypes) + if (!theme.Containers.Any()) { - var containerobject = Activator.CreateInstance(containertype) as IThemeControl; - if (theme.Containers.FirstOrDefault(item => item.TypeName == containertype.FullName + ", " + themeControlType.Assembly.GetName().Name) == null) + // add container controls + foreach (Type containertype in containerControlTypes.Where(item => item.Namespace == themeControlType.Namespace)) { + var containerobject = Activator.CreateInstance(containertype) as IThemeControl; theme.Containers.Add( new ThemeControl { diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 0a83d275..d4bf0161 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -216,6 +216,7 @@ namespace Oqtane app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); + app.UseAntiforgery(); if (_useSwagger) { diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 285cf9b7..8088f6cf 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,9 +13,9 @@ - - - + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index d80b6abd..1bdfa524 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index e143330b..4a10f700 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/Oqtane.Server/wwwroot/js/quill-interop.js b/Oqtane.Server/wwwroot/js/quill-interop.js index e5610a9e..4598d7aa 100644 --- a/Oqtane.Server/wwwroot/js/quill-interop.js +++ b/Oqtane.Server/wwwroot/js/quill-interop.js @@ -35,13 +35,15 @@ Oqtane.RichTextEditor = { enableQuillEditor: function (editorElement, mode) { editorElement.__quill.enable(mode); }, - insertQuillImage: function (quillElement, imageURL, altText) { - var Delta = Quill.import('delta'); - editorIndex = 0; - + getCurrentCursor: function (quillElement) { + var editorIndex = 0; if (quillElement.__quill.getSelection() !== null) { editorIndex = quillElement.__quill.getSelection().index; } + return editorIndex; + }, + insertQuillImage: function (quillElement, imageURL, altText, editorIndex) { + var Delta = Quill.import('delta'); return quillElement.__quill.updateContents( new Delta() diff --git a/Oqtane.Shared/Interfaces/IModuleControl.cs b/Oqtane.Shared/Interfaces/IModuleControl.cs index 1d442931..df607f8f 100644 --- a/Oqtane.Shared/Interfaces/IModuleControl.cs +++ b/Oqtane.Shared/Interfaces/IModuleControl.cs @@ -35,5 +35,10 @@ namespace Oqtane.Modules /// Specifies the required render mode for the module control ie. Static,Interactive /// string RenderMode { get; } + + /// + /// Specifies the prerender mode for the moudle control ie: true or false + /// + bool? Prerender { get; } } } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index bae7a0ca..b90e685c 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -117,6 +117,8 @@ namespace Oqtane.Models public bool UseAdminContainer { get; set; } [NotMapped] public string RenderMode{ get; set; } + [NotMapped] + public bool? Prerender { get; set; } #endregion diff --git a/Oqtane.Shared/Models/Permission.cs b/Oqtane.Shared/Models/Permission.cs index 8d411252..1448e039 100644 --- a/Oqtane.Shared/Models/Permission.cs +++ b/Oqtane.Shared/Models/Permission.cs @@ -101,6 +101,20 @@ namespace Oqtane.Models IsAuthorized = isAuthorized; } + public Permission Clone(Permission permission) + { + return new Permission + { + SiteId = permission.SiteId, + EntityName = permission.EntityName, + EntityId = permission.EntityId, + PermissionName = permission.PermissionName, + RoleName = permission.RoleName, + UserId = permission.UserId, + IsAuthorized = permission.IsAuthorized + }; + } + [Obsolete("The Role property is deprecated", false)] [NotMapped] [JsonIgnore] // exclude from API payload diff --git a/Oqtane.Shared/Models/Route.cs b/Oqtane.Shared/Models/Route.cs index b5a608f3..87577c02 100644 --- a/Oqtane.Shared/Models/Route.cs +++ b/Oqtane.Shared/Models/Route.cs @@ -43,13 +43,19 @@ namespace Oqtane.Models int pos = PagePath.IndexOf("/" + Constants.UrlParametersDelimiter + "/"); if (pos != -1) { - UrlParameters = PagePath.Substring(pos + 3); + if (pos + 3 < PagePath.Length) + { + UrlParameters = PagePath.Substring(pos + 3); + } PagePath = PagePath.Substring(0, pos); } pos = PagePath.IndexOf("/" + Constants.ModuleDelimiter + "/"); if (pos != -1) { - ModuleId = PagePath.Substring(pos + 3); + if (pos + 3 < PagePath.Length) + { + ModuleId = PagePath.Substring(pos + 3); + } PagePath = PagePath.Substring(0, pos); } if (ModuleId.Length != 0) diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 3d8d4a78..f16dcd9c 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net8.0 Debug;Release - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -19,8 +19,8 @@ - - + + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index a7d1173e..b2b8c837 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "5.1.1"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1"; + public static readonly string Version = "5.1.2"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index b9e364c3..5498a87a 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -44,9 +44,10 @@ namespace Oqtane.Shared string querystring = ""; string fragment = ""; + if (!string.IsNullOrEmpty(path) && !path.StartsWith("/")) path = "/" + path; + if (!string.IsNullOrEmpty(parameters)) { - // parse parameters (string urlparameters, querystring, fragment) = ParseParameters(parameters); if (!string.IsNullOrEmpty(urlparameters)) { @@ -138,6 +139,9 @@ namespace Oqtane.Shared public static string FormatContent(string content, Alias alias, string operation) { + if (string.IsNullOrEmpty(content) || alias == null) + return content; + var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; switch (operation) { diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 4554897c..c9b31857 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net8.0 Exe - 5.1.1 + 5.1.2 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.2 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/README.md b/README.md index 0a56d241..633a3940 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Latest Release -[5.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0) was released on Mar 27, 2024 and is a major release providing Static Server Rendering support for Blazor in .NET 8. This release includes 263 pull requests by 6 different contributors, pushing the total number of project commits all-time to over 5100. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[5.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1) was released on Apr 16, 2024 and is primarily a stabilization release, including a variety of improvements to the Static Server-Side Rendering support for Blazor in .NET 8. This release includes 40 pull requests by 6 different contributors, pushing the total number of project commits all-time to over 5200. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json) @@ -8,7 +8,7 @@ ![Oqtane](https://github.com/oqtane/framework/blob/master/oqtane.png?raw=true "Oqtane") -Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI). +Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Static Blazor, Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI). Oqtane is being developed based on some fundamental principles which are outlined in the [Oqtane Philosophy](https://www.oqtane.org/blog/!/20/oqtane-philosophy). @@ -18,15 +18,15 @@ Please note that this project is owned by the .NET Foundation and is governed by **Using Version 5:** -- Install **[.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**. +- Install **[.NET 8.0.4 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**. -- Install the latest edition (v17.8 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. +- Install the latest edition (v17.9 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. - Clone the Oqtane dev branch source code to your local system. - Open the **Oqtane.sln** solution file. -- **Important:** Build the solution. +- **Important:** Rebuild the entire solution before running it. - Make sure you specify Oqtane.Server as the Startup Project @@ -63,8 +63,8 @@ Backlog (TBD) - [ ] Folder Providers - [ ] Generative AI Integration -5.1.1 (Apr 2024) -- [ ] Stabilization improvements +[5.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.1) (Apr 16, 2024) +- [x] Stabilization improvements [5.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.1.0) (Mar 27, 2024) - [x] Migration to the new unified Blazor approach in .NET 8 (ie. blazor.web.js) @@ -79,200 +79,11 @@ Backlog (TBD) [5.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.0.0) (Nov 16, 2023) - [x] Migration to .NET 8 -[4.0.6](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.6) ( Oct 16, 2023 ) -- [x] Stabilization improvements - -[4.0.5](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.5) ( Sep 26, 2023 ) -- [x] Stabilization improvements - -[4.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.4) ( Sep 25, 2023 ) -- [x] Stabilization improvements -- [x] User Import - -[4.0.3](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.3) ( Aug 29, 2023 ) -- [x] Stabilization improvements - -[4.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.2) ( Aug 9, 2023 ) -- [x] Stabilization improvements - -[4.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1) ( Jul 18, 2023 ) -- [x] Stabilization improvements - -[4.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0) ( Jun 26, 2023 ) -- [x] Migration to .NET 7 -- [x] Improved JavaScript, CSS, and Meta support -- [x] Optimized Client Assembly Loading -- [x] Routable Modules (ie. declarative configuration) -- [x] Site Template improvements -- [x] IEventSubscriber interface - -[3.4.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3) ( May 3, 2023 ) -- [x] Stabilization improvements - -[3.4.2](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.2) ( Mar 29, 2023 ) -- [x] Stabilization improvements - -[3.4.1](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.1) ( Mar 13, 2023 ) -- [x] Stabilization improvements - -[3.4.0](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.0) ( Mar 12, 2023 ) -- [x] Permissions performance optimization -- [x] Connection string management improvements -- [x] XML site map generator -- [x] OIDC integration with User Profiles - -[3.3.1](https://github.com/oqtane/oqtane.framework/releases/tag/v3.3.1) ( Jan 14, 2023 ) -- [x] Stabilization improvements - -[3.3.0](https://github.com/oqtane/oqtane.framework/releases/tag/v3.3.0) ( Jan 12, 2023 ) -- [x] Dynamic Authorization Policies -- [x] Entity-Level Permissions -- [x] Extended Module Permissions - -[3.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.1) ( Oct 17, 2022 ) -- [x] Stabilization improvements -- [x] Server Event System - -[3.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v3.2.0) ( Sep 13, 2022 ) -- [x] .NET MAUI / Blazor Hybrid support -- [x] Upgrade to Bootstrap 5.2 - -[3.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.3) ( Jun 27, 2022 ) -- [x] Stabilization improvements - -[3.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.2) ( May 14, 2022 ) -- [x] Stabilization improvements - -[3.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.1) ( May 3, 2022 ) -- [x] Stabilization improvements - -[3.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v3.1.0) ( Apr 5, 2022 ) -- [x] User account lockout support -- [x] Two factor authentication support -- [x] Per-site configuration of password complexity, lockout criteria -- [x] External login support via OAuth2 / OpenID Connect -- [x] Support for Single Sign On (SSO) via OpenID Connect -- [x] External client support via Jwt tokens -- [x] Downstream API support via Jwt tokens -- [x] CSS resource hierarchy support -- [x] Site structure/content migration -- [x] Event log notifications -- [x] 404 page handling -- [x] Property change component notifications -- [x] Support for ES6 JavaScript modules - -[3.0.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.3) ( Feb 15, 2022 ) -- [x] Url fragment and anchor navigation support -- [x] Meta tag support in page head -- [x] Html/Text content versioning support - -[3.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.2) ( Jan 16, 2022 ) -- [x] Default alias specification, auto alias registration, redirect logic -- [x] Improvements to visitor tracking and url mapping -- [x] Scheduler enhancements for stop/start, weekly and one-time jobs -- [x] Purge job for daily housekeeping of event log and visitors -- [x] Granular security filtering for Settings - -[3.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.1) ( Dec 12, 2021 ) -- [x] Url mapping for broken links, content migration -- [x] Visitor tracking for usage insights, personalization -- [x] User experience improvements in Page and Module management - -[3.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v3.0.0) ( Nov 11, 2021 ) -- [x] Migration to .NET 6 -- [x] Blazor hosting model flexibility per site -- [x] Blazor WebAssembly prerendering support - -[2.3.1](https://github.com/oqtane/oqtane.framework/releases/tag/v2.3.1) ( Sep 27, 2021 ) -- [x] Complete UI migration to Bootstrap 5 and HTML5 form validation -- [x] Improve module/theme installation and add support for commercial extensions -- [x] Replace System.Drawing with ImageSharp -- [x] Image resizing service - -[2.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v2.2.0) ( Jul 6, 2021 ) -- [x] Bootstrap 5 Upgrade -- [x] Package Service integration -- [x] Default and Shared Resource File inclusion -- [x] Startup Error logging -- [x] API Controller Validation and Logging - -[2.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v2.1.0) ( Jun 4, 2021 ) -- [x] Cross Platform Database Support ( ie. LocalDB, SQL Server, SQLite, MySQL, PostgreSQL ) - see [#964](https://github.com/oqtane/oqtane.framework/discussions/964) -- [x] Utilize EF Core Migrations - see [#964](https://github.com/oqtane/oqtane.framework/discussions/964) -- [x] Public Content Folder support -- [x] Multi-tenant Infrastructure improvements -- [x] User Authorization optimization -- [x] Consolidation of Package Management -- [x] Blazor Server Pre-rendering -- [x] Translation Package installation support - -[2.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v2.0.2) ( Apr 19, 2021 ) -- [x] Assorted fixes and user experience improvements - -[2.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v2.0.1) ( Feb 27, 2021 ) -- [x] Complete Static Localization of Admin UI - -[2.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v2.0.0) ( Nov 11, 2020 ) -- [x] Migration to .NET 5 -- [x] Static Localization ( ie. labels, help text, etc.. ) -- [x] Improved JavaScript Reference Support -- [x] Performance Optimizations -- [x] Developer Productivity Enhancements - -[1.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v1.0.0) ( May 19, 2020 ) -- [x] Migration to .NET Core 3.2 -- [x] Multi-Tenant ( Shared Database & Isolated Database ) -- [x] Modular Architecture -- [x] Headless API with Swagger Support -- [x] Dynamic Page Compositing Model / Site & Page Management -- [x] Authentication / User Management / Profile Management -- [x] Authorization / Roles Management / Granular Permissions -- [x] Dynamic Routing -- [x] Extensibility via Custom Modules -- [x] Extensibility via Custom Themes -- [x] Event Logging / Audit Trail -- [x] Folder / File Management -- [x] Recycle Bin -- [x] Scheduled Jobs ( Background Processing ) -- [x] Notifications / Email Delivery -- [x] Seamless Upgrade Experience -- [x] Progressive Web Application Support -- [x] JavaScript Lazy Loading -- [x] Dynamic CSS/Lazy Loading - -[POC](https://www.oqtane.org/blog/!/7/announcing-oqtane-a-modular-application-framework-for-blazor) ( May 9, 2019 ) -- [x] Initial public release on GitHub -- [x] .NET Core 3.0 +➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html) # Background Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules. -# Release Announcements - -[Oqtane 5.0](https://www.oqtane.org/blog/!/75/announcing-oqtane-5-0-for-net-8) - -[Oqtane 4.0](https://www.oqtane.org/blog/!/63/announcing-oqtane-4-0-for-net-7) - -[Oqtane 3.4](https://www.oqtane.org/blog/!/56/oqtane-3-4-0-released) - -[Oqtane 3.3](https://www.oqtane.org/blog/!/54/oqtane-3-3-0-released) - -[Oqtane 3.2](https://www.oqtane.org/blog/!/50/oqtane-3-2-for-net-maui-blazor-hybrid) - -[Oqtane 3.1](https://www.oqtane.org/blog/!/41/oqtane-3-1-released) - -[Oqtane 3.0](https://www.oqtane.org/Resources/Blog/PostId/551/announcing-oqtane-30-for-net-6) - -[Oqtane 2.2](https://www.oqtane.org/Resources/Blog/PostId/549/oqtane-22-upgrades-to-bootstrap-5) - -[Oqtane 2.1](https://www.oqtane.org/Resources/Blog/PostId/548/oqtane-21-now-supports-multiple-databases) - -[Oqtane 2.0](https://www.oqtane.org/Resources/Blog/PostId/544/announcing-oqtane-20-for-net-5) - -[Oqtane 1.0](https://www.oqtane.org/Resources/Blog/PostId/540/announcing-oqtane-10-a-modular-application-framework-for-blazor) - -[Oqtane POC](https://www.oqtane.org/Resources/Blog/PostId/520/announcing-oqtane-a-modular-application-framework-for-blazor) - # Reference Implementations [Built On Blazor!](https://builtonblazor.net) - a showcase of sites built on Blazor