diff --git a/Directory.Build.props b/Directory.Build.props index 72d3c1d5..e1f15325 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ net10.0 Debug;Release - 10.0.4 + 10.1.0 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/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 https://github.com/oqtane/oqtane.framework Git diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index 69df010a..8874d9db 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -12,10 +12,10 @@ - - - - + + + + @@ -23,7 +23,7 @@ - + diff --git a/Oqtane.Application/Client/_Imports.razor b/Oqtane.Application/Client/_Imports.razor index 47e1b8f5..edb658c6 100644 --- a/Oqtane.Application/Client/_Imports.razor +++ b/Oqtane.Application/Client/_Imports.razor @@ -9,6 +9,7 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.Extensions.Localization @using Microsoft.JSInterop +@using Microsoft.AspNetCore.Authorization @using Oqtane @using Oqtane.Models diff --git a/Oqtane.Application/Oqtane.Application.Template.nuspec b/Oqtane.Application/Oqtane.Application.Template.nuspec index e6c2bdb6..9ba1ed74 100644 --- a/Oqtane.Application/Oqtane.Application.Template.nuspec +++ b/Oqtane.Application/Oqtane.Application.Template.nuspec @@ -2,7 +2,7 @@ Oqtane.Application.Template - 10.0.4 + 10.1.0 Oqtane Application Template For Blazor Shaun Walker false diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 9aed8cfc..3526407a 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -22,9 +22,9 @@ - - - + + + @@ -33,7 +33,7 @@ - + diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor index 59badca4..a9406963 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -50,7 +50,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -81,14 +81,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor index af8a4839..b53e5d23 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs similarity index 74% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs index d433d698..ec451c42 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/[Module]Service.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Services/Client[Module]Service.cs @@ -7,22 +7,10 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public interface I[Module]Service + + public class Client[Module]Service : ServiceBase, I[Module]Service { - Task> Get[Module]sAsync(int ModuleId); - - Task Get[Module]Async(int [Module]Id, int ModuleId); - - Task Add[Module]Async(Models.[Module] [Module]); - - Task Update[Module]Async(Models.[Module] [Module]); - - Task Delete[Module]Async(int [Module]Id, int ModuleId); - } - - public class [Module]Service : ServiceBase, I[Module]Service - { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/[Module]Service.cs rename to Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Server/Services/Server[Module]Service.cs diff --git a/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs new file mode 100644 index 00000000..8c75bf44 --- /dev/null +++ b/Oqtane.Application/Server/wwwroot/Modules/Templates/Internal/Shared/Interfaces/I[Module]Service.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace [Owner].Module.[Module].Services +{ + public interface I[Module]Service + { + Task> Get[Module]sAsync(int ModuleId); + + Task Get[Module]Async(int [Module]Id, int ModuleId); + + Task Add[Module]Async(Models.[Module] [Module]); + + Task Update[Module]Async(Models.[Module] [Module]); + + Task Delete[Module]Async(int [Module]Id, int ModuleId); + } +} diff --git a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj index fb8f617b..7689e461 100644 --- a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj +++ b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj @@ -11,7 +11,7 @@ - + diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 82186385..dca72f70 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -57,6 +57,9 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); // providers diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor new file mode 100644 index 00000000..dff7e341 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor @@ -0,0 +1,117 @@ +@namespace Oqtane.Modules.Admin.GlobalReplace +@using System.Text.Json +@inherits ModuleBase +@inject ISiteTaskService SiteTaskService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+

+ +

+ +@code { + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override string Title => "Global Replace"; + + private string _find; + private string _replace; + private string _caseSensitive = "True"; + private string _site = "True"; + private string _pages = "True"; + private string _modules = "True"; + private string _content = "True"; + + private async Task Save() + { + try + { + if (!string.IsNullOrEmpty(_find) && !string.IsNullOrEmpty(_replace)) + { + var replace = new GlobalReplace + { + Find = _find, + Replace = _replace, + CaseSensitive = bool.Parse(_caseSensitive), + Site = bool.Parse(_site), + Pages = bool.Parse(_pages), + Modules = bool.Parse(_modules), + Content = bool.Parse(_content) + }; + + var siteTask = new SiteTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace)); + await SiteTaskService.AddSiteTaskAsync(siteTask); + + AddModuleMessage(Localizer["Success.Save"], MessageType.Success); + } + else + { + AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving Global Replace Settings {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Save"], MessageType.Error); + } + } + +} \ No newline at end of file diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index e9caa80f..dde0540e 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -46,7 +46,8 @@ -
+
+
@@ -97,7 +98,13 @@ }
- +
+ +
+ +
+
+

@@ -231,6 +238,7 @@ private string _url = ""; private string _contact = ""; private string _license = ""; + private string _fingerprint = ""; private List _permissions = null; private string _createdby; private DateTime _createdon; @@ -266,6 +274,7 @@ _url = moduleDefinition.Url; _contact = moduleDefinition.Contact; _license = moduleDefinition.License; + _fingerprint = moduleDefinition.Fingerprint; _permissions = moduleDefinition.PermissionList; _createdby = moduleDefinition.CreatedBy; _createdon = moduleDefinition.CreatedOn; diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index 297a365a..bde62f80 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -73,27 +73,29 @@
- +
- - } - else + + } + else + { + + + }
@@ -161,6 +163,7 @@ private string _pane; private string _containerType; private string _allPages = "false"; + private bool _isShared = false; private string _header = ""; private string _footer = ""; private string _permissionNames = ""; @@ -207,6 +210,7 @@ _expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate); _allPages = pagemodule.Module.AllPages.ToString(); + _isShared = pagemodule.Module.IsShared; createdby = pagemodule.Module.CreatedBy; createdon = pagemodule.Module.CreatedOn; modifiedby = pagemodule.Module.ModifiedBy; @@ -276,15 +280,39 @@ var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { - if (!string.IsNullOrEmpty(_title)) { if (!Utilities.ValidateEffectiveExpiryDates(_effectivedate, _expirydate)) { AddModuleMessage(SharedLocalizer["Message.EffectiveExpiryDateError"], MessageType.Warning); return; - } + } + + // update module settings first + if (_moduleSettingsType != null) + { + if (_moduleSettings is ISettingsControl moduleSettingsControl) + { + // module settings updated using explicit interface + await moduleSettingsControl.UpdateSettings(); + } + else + { + // legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component ) + _moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null); + } + } + + // update container settings + if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl) + { + await containerSettingsControl.UpdateSettings(); + } + + // update page module var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); + var pageId = pagemodule.PageId; // preserve + var pane = pagemodule.Pane; // preserve pagemodule.PageId = int.Parse(_pageId); pagemodule.Title = _title; pagemodule.Pane = _pane; @@ -302,33 +330,21 @@ pagemodule.Header = _header; pagemodule.Footer = _footer; await PageModuleService.UpdatePageModuleAsync(pagemodule); - await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); + // update page module order if page or pane changed + if (pageId != pagemodule.PageId || pane != pagemodule.Pane) + { + await PageModuleService.UpdatePageModuleOrderAsync(pageId, pane); // old page/pane + await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); // new page/pane + } + + // update module var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId); module.AllPages = bool.Parse(_allPages); module.PageModuleId = ModuleState.PageModuleId; module.PermissionList = _permissionGrid.GetPermissionList(); await ModuleService.UpdateModuleAsync(module); - if (_moduleSettingsType != null) - { - if (_moduleSettings is ISettingsControl moduleSettingsControl) - { - // module settings updated using explicit interface - await moduleSettingsControl.UpdateSettings(); - } - else - { - // legacy support - module settings updated by convention ( ie. by calling a public method named "UpdateSettings" in settings component ) - _moduleSettings?.GetType().GetMethod("UpdateSettings")?.Invoke(_moduleSettings, null); - } - } - - if (_containerSettingsType != null && _containerSettings is ISettingsControl containerSettingsControl) - { - await containerSettingsControl.UpdateSettings(); - } - NavigationManager.NavigateTo(PageState.ReturnUrl); } else diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index acf99d1e..96ec6395 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -46,7 +46,7 @@
- +
- - -
+ @if (!_copy) + { + + +
    @Localizer["ModuleTitle"] @Localizer["ModuleDefinition"] -
- - - - @context.Title - @context.ModuleDefinition?.Name - -
-
- @if (_themeSettingsType != null) - { - - @_themeSettingsComponent -
- - +
+ + + + @context.Title + @context.ModuleDefinition?.Name + +
+ @if (_themeSettingsType != null) + { + + @_themeSettingsComponent +
+ + +
+ } + } } @@ -349,6 +356,7 @@ private List _containers = new List(); private List _pages; private int _pageId; + private bool _copy = false; private string _name; private string _currentparentid; private string _parentid = "-1"; @@ -394,6 +402,10 @@ { _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _pageId = Int32.Parse(PageState.QueryString["id"]); + if (PageState.QueryString.ContainsKey("copy")) + { + _copy = bool.Parse(PageState.QueryString["copy"]); + } _page = await PageService.GetPageAsync(_pageId); _icons = await SystemService.GetIconsAsync(); _iconresources = Utilities.GetFullTypeName(typeof(IconResources).AssemblyQualifiedName); @@ -413,7 +425,7 @@ _children = new List(); foreach (Page p in _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)) + if ((p.PageId != _pageId || _copy) && UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, p.PermissionList)) { _children.Add(p); } @@ -440,6 +452,12 @@ _expirydate = Utilities.UtcAsLocalDate(_page.ExpiryDate); _ispersonalizable = _page.IsPersonalizable.ToString(); + if (_copy) + { + _insert = ">"; + _childid = _page.PageId; + } + // appearance _title = _page.Title; _themetype = _page.ThemeType; @@ -470,6 +488,19 @@ // permissions _permissions = _page.PermissionList; _updatemodulepermissions = "True"; + if (_copy) + { + _permissions = _page.PermissionList.Select(item => new Permission + { + SiteId = item.SiteId, + EntityName = item.EntityName, + EntityId = -1, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + }).ToList(); + } // page modules var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); @@ -484,6 +515,13 @@ _deletedon = _page.DeletedOn; ThemeSettings(); + + if (_copy) + { + _name = ""; + _path = ""; + } + _initialized = true; } else @@ -554,7 +592,7 @@ builder.OpenComponent(0, _themeSettingsType); builder.AddAttribute(1, "RenderModeBoundary", RenderModeBoundary); builder.AddComponentReferenceCapture(2, inst => { _themeSettings = Convert.ChangeType(inst, _themeSettingsType); }); - + builder.CloseComponent(); }; } @@ -576,10 +614,18 @@ await ScrollToPageTop(); return; } + if (!string.IsNullOrEmpty(_themetype) && _containertype != "-") { string currentPath = _page.Path; + if (_copy) + { + _page = new Page(); + _page.SiteId = PageState.Site.SiteId; + currentPath = ""; + } + _page.Name = _name; if (_parentid == "-1") @@ -635,6 +681,13 @@ return; } + // update theme settings + if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl) + { + await themeSettingsControl.UpdateSettings(); + } + + // default page properties if (_insert != "=") { Page child; @@ -688,7 +741,21 @@ _page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions); } - _page = await PageService.UpdatePageAsync(_page); + if (_copy) + { + // create page + _page = await PageService.AddPageAsync(_page); + await PageService.CopyPageAsync(_pageId, _page.PageId, bool.Parse(_updatemodulepermissions)); + await logger.LogInformation("Page Added {Page}", _page); + } + else + { + // update page + _page = await PageService.UpdatePageAsync(_page); + await logger.LogInformation("Page Saved {Page}", _page); + } + + // update page order await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, _page.ParentId); if (_currentparentid == string.Empty) { @@ -699,12 +766,6 @@ await PageService.UpdatePageOrderAsync(_page.SiteId, _page.PageId, int.Parse(_currentparentid)); } - if (_themeSettingsType != null && _themeSettings is ISettingsControl themeSettingsControl) - { - await themeSettingsControl.UpdateSettings(); - } - - await logger.LogInformation("Page Saved {Page}", _page); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) { NavigationManager.NavigateTo(PageState.ReturnUrl, true); // redirect to page being edited and reload diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 2f7c47e7..4f99e453 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -3,10 +3,11 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IUserService UserService -@inject IStringLocalizer Localizer -@inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer @if (_initialized) { @@ -71,6 +72,18 @@ +
+ +
+ +
+

@@ -95,6 +108,8 @@ @code { private bool _initialized = false; private List _timezones; + private IEnumerable _languages; + private string _passwordrequirements; private string _username = string.Empty; private ElementReference form; @@ -106,6 +121,7 @@ private string _email = string.Empty; private string _displayname = string.Empty; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private bool _userCreated = false; private bool _allowsitelogin = true; @@ -113,10 +129,13 @@ protected override async Task OnInitializedAsync() { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.Site.TimeZoneId; + _culturecode = PageState.Site.CultureCode; _initialized = true; } @@ -147,6 +166,7 @@ Email = _email, DisplayName = (_displayname == string.Empty ? _username : _displayname), TimeZoneId = _timezoneid, + CultureCode = _culturecode, PhotoFileId = null }; user = await UserService.AddUserAsync(user); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index d49d90ed..9b94e0c0 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -11,12 +11,15 @@ @inject IThemeService ThemeService @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILocalizationService LocalizationService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @inject IJobService JobService @inject IStringLocalizer SharedLocalizer @inject IOutputCacheService CacheService +@inject ISiteGroupService SiteGroupService +@inject ISiteGroupMemberService SiteGroupMemberService @if (_initialized) { @@ -29,22 +32,7 @@
- -
- -
-
-
- +
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
@@ -333,57 +333,6 @@
@if (_aliases != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { -
-
-
- -
- - -
-   -   - @Localizer["AliasName"] - @Localizer["AliasDefault"] -
- - @if (context.AliasId != _aliasid) - { - - @if (_aliasid == -1) - { - - } - - - @if (_aliasid == -1) - { - - } - - @context.Name - @((context.IsDefault) ? SharedLocalizer["Yes"] : SharedLocalizer["No"]) - } - else - { - - - - - - - - - } - -
-
-
-
-
@@ -409,7 +358,7 @@
}
- +
+ + @foreach (var alias in _aliases) + { + + } + + @if (!_addAlias) + { + + } +
+
+
+ } + @if (_aliasid != -1 || _addAlias) + { +
+ +
+ +
+
+
+ +
+ +
+
+ + } +
+
+
+ @if (_aliasid != -1 || _addAlias) + { + + } + @if (_aliasid != -1 && !_addAlias) + { + + } + @if (_addAlias) + { + + } +
+
+ +
+
+
+ @if (!_addSiteGroup) + { +
+ +
+
+ + @if (!_addSiteGroup) + { + + } +
+
+
+ } + @if (_siteGroupId != -1 || _addSiteGroup) + { +
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_siteGroupId != -1) + { +
+ +
+
+ + @if (!_addSiteGroupMember) + { + + } + else + { + + } +
+
+
+ } + @if (_siteGroupId != -1 && _siteId != -1) + { +
+ +
+ +
+
+ @if (_primary == "False" && !string.IsNullOrEmpty(_synchronized) && (_groupType == SiteGroupTypes.Synchronization || _groupType == SiteGroupTypes.ChangeDetection)) + { +
+ +
+
+ + @if (!string.IsNullOrEmpty(_synchronized)) + { + + } +
+
+
+ } + } +
+
+
+ @if ((_siteGroupId != -1 || _addSiteGroup)) + { + + } + @if (_siteGroupId != -1 && !_addSiteGroup && _siteId != -1 && !_addSiteGroupMember) + { + + } + @if (_addSiteGroup) + { + + } +
+
+
+
@@ -478,10 +601,11 @@ private List _containers = new List(); private List _pages; private List _timezones; + private IEnumerable _cultures; private string _name = string.Empty; - private string _homepageid = "-"; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _isdeleted; private string _sitemap = ""; private string _siteguid = ""; @@ -527,17 +651,29 @@ private int _pwasplashiconfileid = -1; private FileManager _pwasplashiconfilemanager; - private List _aliases; - private int _aliasid = -1; - private string _aliasname; - private string _defaultalias; - private string _rendermode = RenderModes.Interactive; private string _enhancednavigation = "True"; private string _runtime = Runtimes.Server; private string _prerender = "True"; private string _hybrid = "False"; + private List _aliases; + private int _aliasid = -1; + private string _aliasname; + private string _defaultalias; + private bool _addAlias = false; + + private List _siteGroups = new List(); + private List _sites = new List(); + private int _siteGroupId = -1; + private int _siteId; + private string _groupName = string.Empty; + private string _groupType = SiteGroupTypes.Synchronization; + private string _primary = "True"; + private string _synchronized = string.Empty; + private bool _addSiteGroup = false; + private bool _addSiteGroupMember = false; + private string _tenant = string.Empty; private string _database = string.Empty; private string _connectionstring = string.Empty; @@ -564,16 +700,14 @@ if (site != null) { _timezones = TimeZoneService.GetTimeZones(); + _cultures = await LocalizationService.GetNeutralCulturesAsync(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _name = site.Name; _timezoneid = site.TimeZoneId; - if (site.HomePageId != null) - { - _homepageid = site.HomePageId.Value.ToString(); - } + _culturecode = site.CultureCode; _isdeleted = site.IsDeleted.ToString(); _sitemap = PageState.Alias.Protocol + PageState.Alias.Name + "/sitemap.xml"; _siteguid = site.SiteGuid; @@ -652,7 +786,7 @@ } // aliases - await GetAliases(); + await LoadAliases(); // hosting model _rendermode = site.RenderMode; @@ -676,6 +810,12 @@ } } + // site groups + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + await LoadSiteGroups(); + } + // audit _createdby = site.CreatedBy; _createdon = site.CreatedOn; @@ -743,7 +883,7 @@ { site.Name = _name; site.TimeZoneId = _timezoneid; - site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); + site.CultureCode = _culturecode; site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); // appearance @@ -992,95 +1132,100 @@ } } - private async Task GetAliases() + private async Task LoadAliases() { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + _aliases = await AliasService.GetAliasesAsync(); + _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); + _aliasid = -1; + _addAlias = false; + } + + private async void AliasChanged(ChangeEventArgs e) + { + _aliasid = int.Parse(e.Value.ToString()); + if (_aliasid != -1) { - _aliases = await AliasService.GetAliasesAsync(); - _aliases = _aliases.Where(item => item.SiteId == PageState.Site.SiteId && item.TenantId == PageState.Alias.TenantId).OrderBy(item => item.AliasId).ToList(); + var alias = _aliases.FirstOrDefault(item => item.AliasId == _aliasid); + if (alias != null) + { + _aliasname = alias.Name; + _defaultalias = alias.IsDefault.ToString(); + } } + else + { + _aliasname = ""; + _defaultalias = "False"; + } + StateHasChanged(); } private void AddAlias() { - _aliases.Add(new Alias { AliasId = 0, Name = "", IsDefault = false }); - _aliasid = 0; + _aliasid = -1; _aliasname = ""; _defaultalias = "False"; + _addAlias = true; StateHasChanged(); } - private void EditAlias(Alias alias) + private async Task DeleteAlias() { - _aliasid = alias.AliasId; - _aliasname = alias.Name; - _defaultalias = alias.IsDefault.ToString(); + await AliasService.DeleteAliasAsync(_aliasid); + await LoadAliases(); StateHasChanged(); } - private async Task DeleteAlias(Alias alias) - { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) - { - await AliasService.DeleteAliasAsync(alias.AliasId); - await GetAliases(); - StateHasChanged(); - } - } - private async Task SaveAlias() { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + if (!string.IsNullOrEmpty(_aliasname)) { - if (!string.IsNullOrEmpty(_aliasname)) + var aliases = await AliasService.GetAliasesAsync(); + + int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); + if (protocolIndex != -1) { - var aliases = await AliasService.GetAliasesAsync(); + _aliasname = _aliasname.Substring(protocolIndex + 3); + } - int protocolIndex = _aliasname.IndexOf("://", StringComparison.OrdinalIgnoreCase); - if (protocolIndex != -1) + var alias = aliases.FirstOrDefault(item => item.Name == _aliasname); + + bool unique = (alias == null || alias.AliasId == _aliasid); + + if (unique) + { + if (_aliasid == 0) { - _aliasname = _aliasname.Substring(protocolIndex + 3); + alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; + await AliasService.AddAliasAsync(alias); } - - var alias = aliases.FirstOrDefault(item => item.Name == _aliasname); - - bool unique = (alias == null || alias.AliasId == _aliasid); - - if (unique) + else { - if (_aliasid == 0) + alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid); + if (alias != null) { - alias = new Alias { SiteId = PageState.Site.SiteId, TenantId = PageState.Alias.TenantId, Name = _aliasname, IsDefault = bool.Parse(_defaultalias) }; - await AliasService.AddAliasAsync(alias); - } - else - { - alias = _aliases.SingleOrDefault(item => item.AliasId == _aliasid); - if (alias != null) - { - alias.Name = _aliasname; - alias.IsDefault = bool.Parse(_defaultalias); - await AliasService.UpdateAliasAsync(alias); - } + alias.Name = _aliasname; + alias.IsDefault = bool.Parse(_defaultalias); + await AliasService.UpdateAliasAsync(alias); } + } - await GetAliases(); - _aliasid = -1; - _aliasname = ""; - StateHasChanged(); - } - else // Duplicate alias - { - AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); - await ScrollToPageTop(); - } + await LoadAliases(); + _aliasid = -1; + _aliasname = ""; + StateHasChanged(); + } + else // Duplicate alias + { + AddModuleMessage(Localizer["Message.Aliases.Taken"], MessageType.Warning); + await ScrollToPageTop(); } } } private async Task CancelAlias() { - await GetAliases(); + await LoadAliases(); _aliasid = -1; _aliasname = ""; StateHasChanged(); @@ -1090,4 +1235,202 @@ await CacheService.EvictByTag(Constants.SitemapOutputCacheTag); AddModuleMessage(Localizer["Success.SiteMap.CacheEvicted"], MessageType.Success); } + + private async Task LoadSiteGroups() + { + _siteGroups = await SiteGroupService.GetSiteGroupsAsync(); + _siteGroupId = -1; + _addSiteGroup = false; + StateHasChanged(); + } + + private async Task SiteGroupChanged(ChangeEventArgs e) + { + _siteGroupId = int.Parse(e.Value.ToString()); + if (_siteGroupId != -1) + { + var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); + if (group != null) + { + _groupName = group.Name; + _groupType = group.Type; + _siteId = -1; + _primary = "False"; + _addSiteGroupMember = false; + + await LoadSites(); + } + } + StateHasChanged(); + } + + private async Task LoadSites() + { + var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId); + + _sites = await SiteService.GetSitesAsync(); + if (_addSiteGroupMember) + { + // include sites which are not members + _sites = _sites.ExceptBy(siteGroupMembers.Select(item => item.SiteId), item => item.SiteId).ToList(); + } + else + { + // include sites which are members + _sites = _sites.Where(item => siteGroupMembers.Any(item2 => item2.SiteId == item.SiteId)).ToList(); + var group = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); + foreach (var site in _sites) + { + if (group.PrimarySiteId == site.SiteId) + { + site.Name += $" ({Localizer["Primary"]})"; + } + else + { + site.Name += $" ({Localizer["Secondary"]})"; + } + } + + var siteGroupMember = siteGroupMembers.FirstOrDefault(item => item.SiteId == _siteId); + if (siteGroupMember != null) + { + _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; + _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); + } + } + } + + private async Task SiteChanged(ChangeEventArgs e) + { + _siteId = int.Parse(e.Value.ToString()); + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, _siteGroupId); + if (siteGroupMember != null) + { + _primary = (siteGroupMember.SiteGroup.PrimarySiteId == _siteId) ? "True" : "False"; + _synchronized = UtcToLocal(siteGroupMember.SynchronizedOn).ToString(); + } + StateHasChanged(); + } + + private async Task AddSiteGroup() + { + _groupName = ""; + _siteId = PageState.Site.SiteId; + _primary = "True"; + _synchronized = ""; + _addSiteGroup = true; + } + + private async Task AddSiteGroupMember() + { + _addSiteGroupMember = !_addSiteGroupMember; + _siteId = -1; + await LoadSites(); + } + + private async Task SaveSiteGroupMember() + { + if (string.IsNullOrEmpty(_groupName)) + { + AddModuleMessage(Localizer["Message.Required.GroupName"], MessageType.Warning); + await ScrollToPageTop(); + return; + } + + SiteGroup siteGroup = null; + + if (_siteGroupId == -1) + { + siteGroup = new SiteGroup + { + Name = _groupName, + Type = _groupType, + PrimarySiteId = _siteId, + Synchronize = false + }; + siteGroup = await SiteGroupService.AddSiteGroupAsync(siteGroup); + } + else + { + siteGroup = _siteGroups.FirstOrDefault(item => item.SiteGroupId == _siteGroupId); + if (siteGroup != null) + { + siteGroup.Name = _groupName; + siteGroup.Type = _groupType; + siteGroup.PrimarySiteId = (_primary == "True") ? _siteId : siteGroup.PrimarySiteId; + siteGroup = await SiteGroupService.UpdateSiteGroupAsync(siteGroup); + } + else + { + siteGroup = null; + } + } + + if (siteGroup != null) + { + if (_siteId != -1) + { + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(_siteId, siteGroup.SiteGroupId); + if (siteGroupMember == null) + { + siteGroupMember = new SiteGroupMember + { + SiteGroupId = siteGroup.SiteGroupId, + SiteId = _siteId + }; + await SiteGroupMemberService.AddSiteGroupMemberAsync(siteGroupMember); + } + else + { + siteGroupMember.SynchronizedOn = string.IsNullOrEmpty(_synchronized) ? null : siteGroupMember.SynchronizedOn; + await SiteGroupMemberService.UpdateSiteGroupMemberAsync(siteGroupMember); + } + } + + if (siteGroup.Type == SiteGroupTypes.Synchronization) + { + // enable synchronization job if it is not enabled already + var jobs = await JobService.GetJobsAsync(); + var job = jobs.FirstOrDefault(item => item.JobType == "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server"); + if (job != null && !job.IsEnabled) + { + job.IsEnabled = true; + await JobService.UpdateJobAsync(job); + } + } + + await LoadSiteGroups(); + } + } + + private async Task CancelSiteGroupMember() + { + _groupName = ""; + await LoadSiteGroups(); + } + + private async Task DeleteSiteGroupMember() + { + if (_siteGroupId != -1) + { + var siteGroupMember = await SiteGroupMemberService.GetSiteGroupMemberAsync(PageState.Site.SiteId, _siteGroupId); + if (siteGroupMember != null) + { + await SiteGroupMemberService.DeleteSiteGroupMemberAsync(siteGroupMember.SiteGroupId); + } + + var siteGroupMembers = await SiteGroupMemberService.GetSiteGroupMembersAsync(-1, _siteGroupId); + if (!siteGroupMembers.Any()) + { + await SiteGroupService.DeleteSiteGroupAsync(_siteGroupId); + } + + await LoadSiteGroups(); + } + } + + private async Task ResetSiteGroupMember() + { + _synchronized = ""; + } } diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 383a839e..e97aadfc 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -6,7 +6,6 @@ @inject ITenantService TenantService @inject IAliasService AliasService @inject ISiteService SiteService -@inject IThemeService ThemeService @inject ISiteTemplateService SiteTemplateService @inject IUserService UserService @inject IInstallationService InstallationService @@ -29,33 +28,9 @@ else
- +
- -
-
-
- -
- -
-
-
- -
- +
@@ -70,26 +45,6 @@ else
-
- -
- -
-
-
- -
- -
-
@@ -186,9 +141,6 @@ else private bool _showConnectionString = false; private string _connectionString = string.Empty; - private List _themeList; - private List _themes = new List(); - private List _containers = new List(); private List _siteTemplates; private List _tenants; private string _tenantid = "-"; @@ -200,11 +152,7 @@ else private string _name = string.Empty; private string _urls = string.Empty; - private string _themetype = "-"; - private string _containertype = "-"; private string _sitetemplatetype = "-"; - private string _rendermode = RenderModes.Static; - private string _runtime = Runtimes.Server; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -215,19 +163,11 @@ else { _tenantid = _tenants.First(item => item.Name == TenantNames.Master).TenantId.ToString(); } - _urls = PageState.Alias.Name; - _themeList = await ThemeService.GetThemesAsync(PageState.Site.SiteId); - _themes = ThemeService.GetThemeControls(_themeList); - if (_themes.Any(item => item.TypeName == Constants.DefaultTheme)) - { - _themetype = Constants.DefaultTheme; - _containers = ThemeService.GetContainerControls(_themeList, _themetype); - _containertype = _containers.First().TypeName; - } + _urls = PageState.Alias.Name + "/sitename"; _siteTemplates = await SiteTemplateService.GetSiteTemplatesAsync(); - if (_siteTemplates.Any(item => item.TypeName == Constants.DefaultSiteTemplate)) + if (_siteTemplates.Any(item => item.TypeName == Constants.EmptySiteTemplate)) { - _sitetemplatetype = Constants.DefaultSiteTemplate; + _sitetemplatetype = Constants.EmptySiteTemplate; } _databases = await DatabaseService.GetDatabasesAsync(); @@ -281,37 +221,13 @@ else StateHasChanged(); } - private async void ThemeChanged(ChangeEventArgs e) - { - try - { - _themetype = (string)e.Value; - if (_themetype != "-") - { - _containers = ThemeService.GetContainerControls(_themeList, _themetype); - _containertype = _containers.First().TypeName; - } - else - { - _containers = new List(); - _containertype = "-"; - } - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Loading Containers For Theme {ThemeType} {Error}", _themetype, ex.Message); - AddModuleMessage(Localizer["Error.Theme.LoadContainers"], MessageType.Error); - } - } - private async Task SaveSite() { validated = true; var interop = new Interop(JSRuntime); if (await interop.FormValid(form)) { - if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _themetype != "-" && _containertype != "-" && _sitetemplatetype != "-") + if (_tenantid != "-" && _name != string.Empty && _urls != string.Empty && _sitetemplatetype != "-") { _urls = Regex.Replace(_urls, @"\r\n?|\n", ","); var duplicates = new List(); @@ -399,12 +315,12 @@ else { config.SiteName = _name; config.Aliases = _urls; - config.DefaultTheme = _themetype; - config.DefaultContainer = _containertype; + config.DefaultTheme = Constants.DefaultTheme; + config.DefaultContainer = Constants.DefaultContainer; config.DefaultAdminContainer = ""; config.SiteTemplate = _sitetemplatetype; - config.RenderMode = _rendermode; - config.Runtime = _runtime; + config.RenderMode = RenderModes.Static; + config.Runtime = Runtimes.Server; config.Register = false; ShowProgressIndicator(); diff --git a/Oqtane.Client/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor index 4e7560e5..21e21a3c 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -30,6 +30,7 @@
+
@@ -81,6 +82,12 @@ }
+
+ +
+ +
+

@@ -117,6 +124,7 @@ private string _url = ""; private string _contact = ""; private string _license = ""; + private string _fingerprint = ""; private List _permissions = null; private string _createdby; private DateTime _createdon; @@ -143,6 +151,7 @@ _url = theme.Url; _contact = theme.Contact; _license = theme.License; + _fingerprint = theme.Fingerprint; _permissions = theme.PermissionList; _createdby = theme.CreatedBy; _createdon = theme.CreatedOn; diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 705de755..cf8bac43 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -10,6 +10,7 @@ @inject IFileService FileService @inject IFolderService FolderService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IJSRuntime jsRuntime @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @@ -58,6 +59,18 @@ +
+ +
+ +
+
@@ -448,6 +461,9 @@ @code { public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; + private List _timezones; + private IEnumerable _languages; + private bool _initialized = false; private bool _allowtwofactor = false; private bool _allowpasskeys = false; @@ -458,8 +474,8 @@ private string _displayname = string.Empty; private FileManager _filemanager; private int _folderid = -1; - private List _timezones; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private int _photofileid = -1; private File _photo = null; private string _imagefiles = string.Empty; @@ -493,12 +509,15 @@ if (PageState.User != null) { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + // identity section _username = PageState.User.Username; _email = PageState.User.Email; _displayname = PageState.User.DisplayName; - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.User.TimeZoneId; + _culturecode = PageState.User.CultureCode; var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); if (folder != null) { @@ -572,6 +591,7 @@ user.Email = _email; user.DisplayName = (_displayname == string.Empty ? _username : _displayname); user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; user.PhotoFileId = _filemanager.GetFileId(); if (user.PhotoFileId == -1) { diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 6e19d832..0eea6f43 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -6,6 +6,7 @@ @inject IProfileService ProfileService @inject ISettingService SettingService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -55,6 +56,18 @@
+
+ +
+ +
+
@@ -129,12 +142,15 @@ @code { private List _timezones; + private IEnumerable _languages; + private bool _initialized = false; private string _username = string.Empty; private string _email = string.Empty; private string _confirmed = "True"; private string _displayname = string.Empty; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _notify = "True"; private List _profiles; private Dictionary _settings; @@ -147,6 +163,11 @@ try { _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + + _timezoneid = PageState.Site.TimeZoneId; + _culturecode = PageState.Site.CultureCode; + _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); foreach (var profile in _profiles) { @@ -157,8 +178,9 @@ profile.Options = string.Join(",", options.OrderBy(item => item.Value).Select(kvp => $"{kvp.Key}:{kvp.Value}")); } } + _settings = new Dictionary(); - _timezoneid = PageState.Site.TimeZoneId; + _initialized = true; } catch (Exception ex) @@ -194,6 +216,7 @@ user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; user.PhotoFileId = null; user.SuppressNotification = !bool.Parse(_notify); diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index db0ea36b..eed0111b 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -7,6 +7,7 @@ @inject ISettingService SettingService @inject IFileService FileService @inject ITimeZoneService TimeZoneService +@inject ILanguageService LanguageService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -55,6 +56,18 @@
+
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
@@ -211,7 +224,7 @@ { } - @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _candelete) { } @@ -224,17 +237,21 @@ private bool _allowpasskeys = false; private bool _allowexternallogin = false; + private List _timezones; + private IEnumerable _languages; + private int _userid; private string _username = string.Empty; private string _email = string.Empty; private string _confirmed = string.Empty; private string _displayname = string.Empty; - private List _timezones; private string _timezoneid = string.Empty; + private string _culturecode = string.Empty; private string _isdeleted; private string _lastlogin; private string _lastipaddress; private bool _ishost = false; + private bool _candelete = false; private string _passwordrequirements; private string _password = string.Empty; @@ -270,13 +287,17 @@ var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId); if (user != null) { + _timezones = TimeZoneService.GetTimeZones(); + _languages = await LanguageService.GetLanguagesAsync(PageState.Site.SiteId); + _username = user.Username; _email = user.Email; _confirmed = user.EmailConfirmed.ToString(); _displayname = user.DisplayName; - _timezones = TimeZoneService.GetTimeZones(); _timezoneid = PageState.User.TimeZoneId; + _culturecode = PageState.User.CultureCode; _isdeleted = user.IsDeleted.ToString(); + _candelete = user.IsDeleted; _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn)); _lastipaddress = user.LastIPAddress; _ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); @@ -344,6 +365,7 @@ user.EmailConfirmed = bool.Parse(_confirmed); user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; user.TimeZoneId = _timezoneid; + user.CultureCode = _culturecode; if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { user.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); diff --git a/Oqtane.Client/Modules/Admin/Users/Users.razor b/Oqtane.Client/Modules/Admin/Users/Users.razor index a3ac65d3..41e0912f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Users.razor +++ b/Oqtane.Client/Modules/Admin/Users/Users.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.Users @inherits ModuleBase @inject NavigationManager NavigationManager -@inject IUserService UserService +@inject ISiteTaskService SiteTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -43,17 +43,9 @@ var fileid = _filemanager.GetFileId(); if (fileid != -1) { - ShowProgressIndicator(); - var results = await UserService.ImportUsersAsync(PageState.Site.SiteId, fileid, bool.Parse(_notify)); - if (bool.Parse(results["Success"])) - { - AddModuleMessage(string.Format(Localizer["Message.Import.Success"], results["Users"]), MessageType.Success); - } - else - { - AddModuleMessage(Localizer["Message.Import.Failure"], MessageType.Error); - } - HideProgressIndicator(); + var siteTask = new SiteTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}"); + await SiteTaskService.AddSiteTaskAsync(siteTask); + AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success); } else { diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 4bd49824..ea60ac22 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -61,6 +61,12 @@ { } + @if (MaxUploadFileSize > 0) + { +
+ @string.Format(Localizer["File.MaxSize"], MaxUploadFileSize) +
+ }
@@ -163,6 +169,9 @@ [Parameter] public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB + [Parameter] + public int MaxUploadFileSize { get; set; } = -1; // optional - maximum upload file size in MB + [Parameter] public EventCallback OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded @@ -381,16 +390,39 @@ if (uploads.Length > 0) { string restricted = ""; + string tooLarge = ""; foreach (var upload in uploads) { - var filename = upload.Split(':')[0]; + var fileparts = upload.Split(':'); + var filename = fileparts[0]; + + if (MaxUploadFileSize > 0) + { + var filesizeBytes = long.Parse(fileparts[1]); + var filesizeMB = (double)filesizeBytes / (1024 * 1024); + if (filesizeMB > MaxUploadFileSize) + { + tooLarge += (tooLarge == "" ? "" : ",") + filename; + } + } + var extension = (filename.LastIndexOf(".") != -1) ? filename.Substring(filename.LastIndexOf(".") + 1) : ""; if (!PageState.Site.UploadableFiles.Split(',').Contains(extension.ToLower())) { restricted += (restricted == "" ? "" : ",") + extension; } } - if (restricted == "") + if (restricted != "") + { + _message = string.Format(Localizer["Message.File.Restricted"], restricted); + _messagetype = MessageType.Warning; + } + else if (tooLarge != "") + { + _message = string.Format(Localizer["Message.File.TooLarge"], tooLarge, MaxUploadFileSize); + _messagetype = MessageType.Warning; + } + else { CancellationTokenSource tokenSource = new CancellationTokenSource(); @@ -490,11 +522,6 @@ tokenSource.Dispose(); } } - else - { - _message = string.Format(Localizer["Message.File.Restricted"], restricted); - _messagetype = MessageType.Warning; - } } else { diff --git a/Oqtane.Client/Modules/HtmlText/Edit.razor b/Oqtane.Client/Modules/HtmlText/Edit.razor index 9ad9628b..7f8ed55a 100644 --- a/Oqtane.Client/Modules/HtmlText/Edit.razor +++ b/Oqtane.Client/Modules/HtmlText/Edit.razor @@ -31,8 +31,8 @@       - @SharedLocalizer["CreatedOn"] - @SharedLocalizer["CreatedBy"] + @Localizer["CreatedOn"] + @Localizer["CreatedBy"] @@ -122,13 +122,9 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); - if (htmltext != null) - { - _view = htmltext.Content; - _view = Utilities.FormatContent(_view, PageState.Alias, "render"); - StateHasChanged(); - } + _view = htmltext.Content; + _view = Utilities.FormatContent(_view, PageState.Alias, "render"); + StateHasChanged(); } catch (Exception ex) { @@ -141,19 +137,15 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); - if (htmltext != null) - { - var content = htmltext.Content; - htmltext = new HtmlText(); - htmltext.ModuleId = ModuleState.ModuleId; - htmltext.Content = content; - await HtmlTextService.AddHtmlTextAsync(htmltext); - await logger.LogInformation("Content Restored {HtmlText}", htmltext); - AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success); - await LoadContent(); - StateHasChanged(); - } + var content = htmltext.Content; + htmltext = new HtmlText(); + htmltext.ModuleId = ModuleState.ModuleId; + htmltext.Content = content; + await HtmlTextService.AddHtmlTextAsync(htmltext); + await logger.LogInformation("Content Restored {HtmlText}", htmltext); + AddModuleMessage(Localizer["Message.Content.Restored"], MessageType.Success); + await LoadContent(); + StateHasChanged(); } catch (Exception ex) { @@ -166,15 +158,11 @@ { try { - htmltext = await HtmlTextService.GetHtmlTextAsync(htmltext.HtmlTextId, ModuleState.ModuleId); - if (htmltext != null) - { - await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); - await logger.LogInformation("Content Deleted {HtmlText}", htmltext); - AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success); - await LoadContent(); - StateHasChanged(); - } + await HtmlTextService.DeleteHtmlTextAsync(htmltext.HtmlTextId, htmltext.ModuleId); + await logger.LogInformation("Content Deleted {HtmlText}", htmltext); + AddModuleMessage(Localizer["Message.Content.Deleted"], MessageType.Success); + await LoadContent(); + StateHasChanged(); } catch (Exception ex) { diff --git a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs index f2f0674c..324cfedb 100644 --- a/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Client/Modules/HtmlText/Services/HtmlTextService.cs @@ -7,6 +7,18 @@ using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Services { + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] + public interface IHtmlTextService + { + Task> GetHtmlTextsAsync(int moduleId); + + Task GetHtmlTextAsync(int moduleId); + + Task AddHtmlTextAsync(Models.HtmlText htmltext); + + Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); + } + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextService : ServiceBase, IHtmlTextService, IClientService { @@ -24,11 +36,6 @@ namespace Oqtane.Modules.HtmlText.Services return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{moduleId}", EntityNames.Module, moduleId)); } - public async Task GetHtmlTextAsync(int htmlTextId, int moduleId) - { - return await GetJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}/{htmlTextId}/{moduleId}", EntityNames.Module, moduleId)); - } - public async Task AddHtmlTextAsync(Models.HtmlText htmlText) { return await PostJsonAsync(CreateAuthorizationPolicyUrl($"{ApiUrl}", EntityNames.Module, htmlText.ModuleId), htmlText); diff --git a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs b/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs deleted file mode 100644 index 838d5240..00000000 --- a/Oqtane.Client/Modules/HtmlText/Services/IHtmlTextService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Oqtane.Documentation; - -namespace Oqtane.Modules.HtmlText.Services -{ - [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public interface IHtmlTextService - { - Task> GetHtmlTextsAsync(int moduleId); - - Task GetHtmlTextAsync(int moduleId); - - Task GetHtmlTextAsync(int htmlTextId, int moduleId); - - Task AddHtmlTextAsync(Models.HtmlText htmltext); - - Task DeleteHtmlTextAsync(int htmlTextId, int moduleId); - } -} diff --git a/Oqtane.Client/Modules/HtmlText/Settings.razor b/Oqtane.Client/Modules/HtmlText/Settings.razor index 783e1180..eaf30bc0 100644 --- a/Oqtane.Client/Modules/HtmlText/Settings.razor +++ b/Oqtane.Client/Modules/HtmlText/Settings.razor @@ -16,6 +16,12 @@
+
+ +
+ +
+
@@ -26,12 +32,14 @@ private bool validated = false; private string _dynamictokens; + private string _versions = "5"; protected override void OnInitialized() { try { _dynamictokens = SettingService.GetSetting(ModuleState.Settings, "DynamicTokens", "false"); + _versions = SettingService.GetSetting(ModuleState.Settings, "Versions", "5"); } catch (Exception ex) { @@ -45,6 +53,10 @@ { var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId); settings = SettingService.SetSetting(settings, "DynamicTokens", _dynamictokens); + if (int.TryParse(_versions, out int versions) && versions >= 0 && versions <= 9) + { + settings = SettingService.SetSetting(settings, "Versions", versions.ToString()); + } await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId); } catch (Exception ex) diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index e4249072..c1db2ae1 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx new file mode 100644 index 00000000..e5339e4d --- /dev/null +++ b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Match Case? + + + Specify if the replacement operation should be case sensitive + + + Module Content? + + + Specify if module content should be updated + + + Page Properties? + + + Specify if page properties should be updated (ie. name, title, headcontent, bodycontent) + + + Site Properties? + + + Specify if site properties should be updated (ie. name, headcontent, bodycontent) + + + Replace With: + + + Specify the replacement content + + + Module Properties? + + + Specify if module properties should be updated (ie. title, header, footer) + + + Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient. + + + Error Saving Global Replace + + + Specify the content which needs to be replaced + + + Find What: + + + Global Replace + + + This Operation is Permanent. Are You Sure You Wish To Proceed? + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index eb32861b..a3c86886 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -234,4 +234,10 @@ Pages + + Fingerprint: + + + A unique identifier for the module's static resources. This value can be changed by clicking the Save option below (ie. cache busting). + \ 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 4f8f895c..0cf90297 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx @@ -130,7 +130,7 @@ Indicate if this module should be displayed on all pages - The page that the module is located on + The page that the module is located on. Please note that shared modules cannot be moved to other pages. Title: diff --git a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx index 47e95add..8ebb5968 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx @@ -186,4 +186,10 @@ Your time zone + + Language: + + + Your preferred language. Note that you will only be able to choose from languages supported on this site. + \ 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 3d41770b..b7c269f3 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -319,16 +319,16 @@ The default alias for the site. Requests for non-default aliases will be redirected to the default alias. - Default Alias: + Default? Site Urls - - Url + + Url: - - Default? + + Default Are You Sure You Wish To Delete {0}? @@ -400,7 +400,7 @@ Hybrid Enabled? - The render mode for UI components which require interactivity + The hosting model for UI components which require interactivity Interactivity: @@ -495,4 +495,76 @@ Use TLS When Available + + A url for this site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder). + + + Group: + + + The site groups in this tenant (database) + + + Primary? + + + Indicates if the selected member is the primary site of the site group + + + Name: + + + Name of the site group + + + Primary + + + Secondary + + + Compare + + + Update + + + Delete Site Group Member + + + Are You Sure You Wish To Delete This Member From The Site Group? + + + Site Group Name Is Required + + + Site Submitted For Synchronization + + + Members: + + + The sites which are members of this site group + + + Synchronized: + + + Type: + + + Defines the specific behavior of the site group + + + The date/time when the site was last synchronized + + + Synchronization + + + Localization + + + Change Detection + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx index d32522e9..83b1e332 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Sites/Add.resx @@ -123,29 +123,14 @@ SQL Server - - Select Container - - - Default Container: - - - Select Theme - - The urls for the site (comman delimited). This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder). - - - Select the default container for the site + The primary url for the site. This can be a domain name (ie. domain.com), subdomain (ie. sub.domain.com) or a virtual folder (ie. domain.com/folder). Database: - Urls: - - - Default Theme: + Url: Select Site Template @@ -177,9 +162,6 @@ Enter the name of the site - - Select the default theme for the site - Select the site template @@ -222,12 +204,6 @@ Error loading Database Configuration Control - - The default render mode for the site - - - Render Mode: - Enter a complete connection string including all parameters and delimiters @@ -240,10 +216,4 @@ Enter Connection String - - The render mode for UI components which require interactivity - - - Interactivity: - \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index 27b70ddd..bf155a07 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -186,4 +186,10 @@ Permissions + + Fingerprint: + + + A unique identifier for the theme's static resources. This value can be changed by clicking the Save option below (ie. cache busting). + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx index b725fd7f..0486bed8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UserProfile/Index.resx @@ -288,4 +288,10 @@ Passkey Could Not Be Created + + Language: + + + Your preferred language. Note that you will only be able to choose from languages supported on this site. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx index faf88ee8..f3e4bada 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Add.resx @@ -168,4 +168,10 @@ Indicates if the user's email is verified + + Language: + + + + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index 807b932d..3e80cf7c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -252,4 +252,10 @@ You Do Not Have Any External Logins For This Site + + Language: + + + The user's preferred language. Note that you will only be able to choose from languages supported on this site. + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx index 02e73f23..1b293eaa 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Users.resx @@ -129,11 +129,8 @@ Import - - User Import Failed. Please Review Your Event Log For More Detailed Information. - - User Import Successful. {0} Users Imported. + Your User Import Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient. You Must Specify A User File For Import diff --git a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx index c2ccac05..2759abb5 100644 --- a/Oqtane.Client/Resources/Modules/Controls/FileManager.resx +++ b/Oqtane.Client/Resources/Modules/Controls/FileManager.resx @@ -144,4 +144,10 @@ Files With Extension Of {0} Are Restricted From Upload. Please Contact Your Administrator For More Information. + + Maximum upload file size: {0} MB + + + File(s) {0} exceed(s) the maximum upload size of {1} MB + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx index 68a539c0..f72cd586 100644 --- a/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx +++ b/Oqtane.Client/Resources/Modules/HtmlText/Settings.resx @@ -123,4 +123,10 @@ Dynamic Tokens? + + The number of content versions to preserve (note that zero means unlimited) + + + Versions: + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index b4b3ccfa..06ea6782 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -204,6 +204,9 @@ System Update + + Setting Management + Download @@ -480,4 +483,7 @@ Installed + + Global Replace + \ 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 15c567fc..1ef062f4 100644 --- a/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx +++ b/Oqtane.Client/Resources/Themes/Controls/ControlPanelInteractive.resx @@ -174,9 +174,6 @@ Title: - - Check For System Updates - Visibility: @@ -200,5 +197,11 @@ Copy Existing Module - + + + Synchronize Site + + + Copy Page + \ No newline at end of file diff --git a/Oqtane.Client/Services/LocalizationCookieService.cs b/Oqtane.Client/Services/LocalizationCookieService.cs index 6ebbdc76..2d6b258f 100644 --- a/Oqtane.Client/Services/LocalizationCookieService.cs +++ b/Oqtane.Client/Services/LocalizationCookieService.cs @@ -14,8 +14,9 @@ namespace Oqtane.Services /// Set the localization cookie /// /// + /// /// - Task SetLocalizationCookieAsync(string culture); + Task SetLocalizationCookieAsync(string culture, string uiCulture); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -23,7 +24,7 @@ namespace Oqtane.Services { public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { } - public Task SetLocalizationCookieAsync(string culture) + public Task SetLocalizationCookieAsync(string culture, string uiCulture) { return Task.CompletedTask; // only used in server side rendering } diff --git a/Oqtane.Client/Services/LocalizationService.cs b/Oqtane.Client/Services/LocalizationService.cs index ee537d60..53eb3abf 100644 --- a/Oqtane.Client/Services/LocalizationService.cs +++ b/Oqtane.Client/Services/LocalizationService.cs @@ -13,10 +13,16 @@ namespace Oqtane.Services public interface ILocalizationService { /// - /// Returns a collection of supported cultures + /// Returns a collection of supported or installed cultures /// /// Task> GetCulturesAsync(bool installed); + + /// + /// Returns a collection of neutral cultures + /// + /// + Task> GetNeutralCulturesAsync(); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -26,6 +32,14 @@ namespace Oqtane.Services private string Apiurl => CreateApiUrl("Localization"); - public async Task> GetCulturesAsync(bool installed) => await GetJsonAsync>($"{Apiurl}?installed={installed}"); + public async Task> GetCulturesAsync(bool installed) + { + return await GetJsonAsync>($"{Apiurl}?installed={installed}"); + } + + public async Task> GetNeutralCulturesAsync() + { + return await GetJsonAsync>($"{Apiurl}/neutral"); + } } } diff --git a/Oqtane.Client/Services/PageService.cs b/Oqtane.Client/Services/PageService.cs index e3f940c6..4beefcd0 100644 --- a/Oqtane.Client/Services/PageService.cs +++ b/Oqtane.Client/Services/PageService.cs @@ -71,6 +71,15 @@ namespace Oqtane.Services /// /// Task DeletePageAsync(int pageId); + + /// + /// Copies the modules from one page to another + /// + /// + /// + /// + /// + Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions); } [PrivateApi("Don't show in the documentation, as everything should use the Interface")] @@ -129,5 +138,10 @@ namespace Oqtane.Services { await DeleteAsync($"{Apiurl}/{pageId}"); } + + public async Task CopyPageAsync(int fromPageId, int toPageId, bool usePagePermissions) + { + await PostAsync($"{Apiurl}/{fromPageId}/{toPageId}/{usePagePermissions}"); + } } } diff --git a/Oqtane.Client/Services/SiteGroupMemberService.cs b/Oqtane.Client/Services/SiteGroupMemberService.cs new file mode 100644 index 00000000..22bee97f --- /dev/null +++ b/Oqtane.Client/Services/SiteGroupMemberService.cs @@ -0,0 +1,104 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface ISiteGroupMemberService + { + /// + /// Get all s + /// + /// + Task> GetSiteGroupMembersAsync(int siteId, int siteGroupId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetSiteGroupMemberAsync(int siteGroupMemberId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// ID-reference of a + /// + Task GetSiteGroupMemberAsync(int siteId, int siteGroupId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteSiteGroupMemberAsync(int siteGroupMemberId); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteGroupMemberService : ServiceBase, ISiteGroupMemberService + { + public SiteGroupMemberService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteGroupMember"); + + public async Task> GetSiteGroupMembersAsync(int siteId, int siteGroupId) + { + return await GetJsonAsync>($"{Apiurl}?siteid={siteId}&groupid={siteGroupId}", Enumerable.Empty().ToList()); + } + + public async Task GetSiteGroupMemberAsync(int siteGroupMemberId) + { + return await GetJsonAsync($"{Apiurl}/{siteGroupMemberId}"); + } + + public async Task GetSiteGroupMemberAsync(int siteId, int siteGroupId) + { + var siteGroupMembers = await GetSiteGroupMembersAsync(siteId, siteGroupId); + if (siteGroupMembers != null && siteGroupMembers.Count > 0) + { + return siteGroupMembers[0]; + } + else + { + return null; + } + } + + public async Task AddSiteGroupMemberAsync(SiteGroupMember siteGroupMember) + { + return await PostJsonAsync(Apiurl, siteGroupMember); + } + + public async Task UpdateSiteGroupMemberAsync(SiteGroupMember siteGroupMember) + { + return await PutJsonAsync($"{Apiurl}/{siteGroupMember.SiteGroupId}", siteGroupMember); + } + + public async Task DeleteSiteGroupMemberAsync(int siteGroupMemberId) + { + await DeleteAsync($"{Apiurl}/{siteGroupMemberId}"); + } + } +} diff --git a/Oqtane.Client/Services/SiteGroupService.cs b/Oqtane.Client/Services/SiteGroupService.cs new file mode 100644 index 00000000..83a63242 --- /dev/null +++ b/Oqtane.Client/Services/SiteGroupService.cs @@ -0,0 +1,94 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Collections.Generic; +using Oqtane.Documentation; +using Oqtane.Shared; +using System.Linq; + +namespace Oqtane.Services +{ + /// + /// Service to manage s on a + /// + public interface ISiteGroupService + { + /// + /// Get all s + /// + /// + Task> GetSiteGroupsAsync(); + + /// + /// Get all s + /// + /// + Task> GetSiteGroupsAsync(int primarySiteId); + + /// + /// Get one specific + /// + /// ID-reference of a + /// + Task GetSiteGroupAsync(int siteGroupId); + + /// + /// Add / save a new to the database. + /// + /// + /// + Task AddSiteGroupAsync(SiteGroup siteGroup); + + /// + /// Update a in the database. + /// + /// + /// + Task UpdateSiteGroupAsync(SiteGroup siteGroup); + + /// + /// Delete a in the database. + /// + /// ID-reference of a + /// + Task DeleteSiteGroupAsync(int siteGroupId); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteGroupService : ServiceBase, ISiteGroupService + { + public SiteGroupService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteGroup"); + + public async Task> GetSiteGroupsAsync() + { + return await GetSiteGroupsAsync(-1); + } + + public async Task> GetSiteGroupsAsync(int primarySiteId) + { + return await GetJsonAsync>($"{Apiurl}?siteid={primarySiteId}", Enumerable.Empty().ToList()); + } + + public async Task GetSiteGroupAsync(int siteGroupId) + { + return await GetJsonAsync($"{Apiurl}/{siteGroupId}"); + } + + public async Task AddSiteGroupAsync(SiteGroup siteGroup) + { + return await PostJsonAsync(Apiurl, siteGroup); + } + + public async Task UpdateSiteGroupAsync(SiteGroup siteGroup) + { + return await PutJsonAsync($"{Apiurl}/{siteGroup.SiteGroupId}", siteGroup); + } + + public async Task DeleteSiteGroupAsync(int siteGroupId) + { + await DeleteAsync($"{Apiurl}/{siteGroupId}"); + } + } +} diff --git a/Oqtane.Client/Services/SiteTaskService.cs b/Oqtane.Client/Services/SiteTaskService.cs new file mode 100644 index 00000000..b0c68859 --- /dev/null +++ b/Oqtane.Client/Services/SiteTaskService.cs @@ -0,0 +1,46 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using Oqtane.Documentation; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + /// + /// Service to manage tasks () + /// + public interface ISiteTaskService + { + /// + /// Return a specific task + /// + /// + /// + Task GetSiteTaskAsync(int siteTaskId); + + /// + /// Adds a new task + /// + /// + /// + Task AddSiteTaskAsync(SiteTask siteTask); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class SiteTaskService : ServiceBase, ISiteTaskService + { + public SiteTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("SiteTask"); + + public async Task GetSiteTaskAsync(int siteTaskId) + { + return await GetJsonAsync($"{Apiurl}/{siteTaskId}"); + } + + public async Task AddSiteTaskAsync(SiteTask siteTask) + { + return await PostJsonAsync(Apiurl, siteTask); + } + } +} diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index 049a93d5..bc20f9b2 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -161,15 +161,6 @@ namespace Oqtane.Services /// Task GetPasswordRequirementsAsync(int siteId); - /// - /// Bulk import of users - /// - /// ID of a - /// ID of a - /// Indicates if new users should be notified by email - /// - Task> ImportUsersAsync(int siteId, int fileId, bool notify); - /// /// Get passkeys for a user /// @@ -351,11 +342,6 @@ namespace Oqtane.Services return string.Format(passwordValidationCriteriaTemplate, minimumlength, uniquecharacters, digitRequirement, uppercaseRequirement, lowercaseRequirement, punctuationRequirement); } - public async Task> ImportUsersAsync(int siteId, int fileId, bool notify) - { - return await PostJsonAsync>($"{Apiurl}/import?siteid={siteId}&fileid={fileId}¬ify={notify}", null); - } - public async Task> GetPasskeysAsync(int userId) { return await GetJsonAsync>($"{Apiurl}/passkey?id={userId}"); diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor index 762e457a..7fcf2542 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanel.razor @@ -97,6 +97,7 @@ Alias = PageState.Alias, Site = new Site { + SiteId = PageState.Site.SiteId, DefaultContainerType = PageState.Site.DefaultContainerType, Settings = PageState.Site.Settings, Themes = PageState.Site.Themes diff --git a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor index b702ad8d..ffd54afa 100644 --- a/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor +++ b/Oqtane.Client/Themes/Controls/Theme/ControlPanelInteractive.razor @@ -11,6 +11,7 @@ @inject ILogService logger @inject ISettingService SettingService @inject IJSRuntime jsRuntime +@inject ISiteGroupService SiteGroupService @inject IServiceProvider ServiceProvider @inject ILogService LoggingService @inject IStringLocalizer Localizer @@ -34,6 +35,11 @@ + @if (_siteGroups.Any(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId)) + { +
+ + }
} @if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) @@ -53,18 +59,29 @@ -
-
- @if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone)) - { - - } - else - { - - } + @if (PageState.Page.UserId == null) + { +
+
+ +
-
+ } + @if (!PageState.Page.Path.StartsWith("admin/")) + { +
+
+ @if (UserSecurity.ContainsRole(PageState.Page.PermissionList, PermissionNames.View, RoleNames.Everyone)) + { + + } + else + { + + } +
+
+ }
@if (_deleteConfirmation) @@ -149,7 +166,7 @@ } - @foreach (Module module in _modules) { @@ -257,6 +274,7 @@ private List _pages = new List(); private List _modules = new List(); private List _containers = new List(); + private List _siteGroups = new List(); private string _category = "Common"; private string _pane = ""; @@ -287,6 +305,7 @@ _allModuleDefinitions = await ModuleDefinitionService.GetModuleDefinitionsAsync(PageState.Page.SiteId); _moduleDefinitions = _allModuleDefinitions.Where(item => item.Categories.Contains(_category)).ToList(); _categories = _allModuleDefinitions.SelectMany(m => m.Categories.Split(',', StringSplitOptions.RemoveEmptyEntries)).Distinct().Where(item => item != "Headless").ToList(); + _siteGroups = await SiteGroupService.GetSiteGroupsAsync(PageState.Site.SiteId); } } @@ -339,6 +358,20 @@ StateHasChanged(); } + private async Task PageModuleChanged(ChangeEventArgs e) + { + _moduleId = (string)e.Value; + if (_moduleId != "-") + { + _title = _modules.First(item => item.ModuleId == int.Parse(_moduleId)).Title; + } + else + { + _title = ""; + } + StateHasChanged(); + } + private async Task AddModule() { if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, PageState.Page.PermissionList)) @@ -488,7 +521,12 @@ case "Edit": // get page management moduleid moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]); - NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, location, $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); + NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); + break; + case "Copy": + // get page management moduleid + moduleId = int.Parse(PageState.Site.Settings[Constants.PageManagementModule]); + NavigationManager.NavigateTo(Utilities.EditUrl(PageState.Alias.Path, "admin/pages", moduleId, "Edit", $"id={PageState.Page.PageId}©=true&returnurl={WebUtility.UrlEncode(PageState.Route.PathAndQuery)}")); break; } } @@ -631,4 +669,14 @@ { _message = ""; } + + private async Task SynchronizeSite() + { + foreach (var group in _siteGroups.Where(item => (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection) && item.PrimarySiteId == PageState.Site.SiteId)) + { + group.Synchronize = true; + await SiteGroupService.UpdateSiteGroupAsync(group); + } + NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, ""), true); + } } diff --git a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor index 03cdcbaa..98420061 100644 --- a/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor +++ b/Oqtane.Client/Themes/Controls/Theme/LanguageSwitcher.razor @@ -6,22 +6,29 @@ @inject ILocalizationCookieService LocalizationCookieService @inject NavigationManager NavigationManager -@if (_supportedCultures?.Count() > 1) +@if (PageState.Site.Languages.Count() > 1) {
@@ -29,7 +36,7 @@ } @code{ - private IEnumerable _supportedCultures; + private bool _contentLocalization; private string MenuAlignment = string.Empty; [Parameter] @@ -41,14 +48,15 @@ { MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; - _supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name }); + // if AliasName is populated it means the site is using content localization + _contentLocalization = PageState.Languages.Any(item => !string.IsNullOrEmpty(item.AliasName)); if (PageState.QueryString.ContainsKey("culture")) { var culture = PageState.QueryString["culture"]; - if (_supportedCultures.Any(item => item.Name == culture)) + if (PageState.Site.Languages.Any(item => item.Code == culture)) { - await LocalizationCookieService.SetLocalizationCookieAsync(culture); + await LocalizationCookieService.SetLocalizationCookieAsync(PageState.Site.CultureCode, culture); } NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", "")); } @@ -58,7 +66,7 @@ { if (culture != CultureInfo.CurrentUICulture.Name) { - var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(PageState.Site.CultureCode, culture)); var interop = new Interop(JSRuntime); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax"); NavigationManager.NavigateTo(NavigationManager.Uri, true); diff --git a/Oqtane.Client/UI/SiteRouter.razor b/Oqtane.Client/UI/SiteRouter.razor index fb7d4fc3..275a8829 100644 --- a/Oqtane.Client/UI/SiteRouter.razor +++ b/Oqtane.Client/UI/SiteRouter.razor @@ -233,15 +233,8 @@ if (page == null && route.PagePath == "") // naked path refers to site home page { - if (site.HomePageId != null) - { - page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId); - } - if (page == null) - { - // fallback to use the first page in the collection - page = site.Pages.FirstOrDefault(); - } + // fallback to use the first page in the collection + page = site.Pages.FirstOrDefault(); } if (page == null) { @@ -632,11 +625,11 @@ { if (resource.ResourceType == ResourceType.Stylesheet || resource.Level != ResourceLevel.Site) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); } - if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) + if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { resource.Url = alias.BaseUrl + resource.Url; } diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index d22b21b4..3797dd05 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -227,38 +227,4 @@ } return stylesheets; } - - private string ManageScripts(List resources, Alias alias) - { - var scripts = ""; - if (resources != null) - { - foreach (var resource in resources.Where(item => item.ResourceType == ResourceType.Script && item.Location == ResourceLocation.Head)) - { - var script = CreateScript(resource, alias); - if (!scripts.Contains(script, StringComparison.OrdinalIgnoreCase)) - { - scripts += script + Environment.NewLine; - } - } - } - return scripts; - } - - private string CreateScript(Resource resource, Alias alias) - { - if (!string.IsNullOrEmpty(resource.Url)) - { - var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; - return ""; - } - else - { - // inline script - return ""; - } - } } diff --git a/Oqtane.Client/_Imports.razor b/Oqtane.Client/_Imports.razor index 86d19622..4f1f37c8 100644 --- a/Oqtane.Client/_Imports.razor +++ b/Oqtane.Client/_Imports.razor @@ -11,6 +11,7 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.Extensions.Localization @using Microsoft.JSInterop +@using Microsoft.AspNetCore.Authorization @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Oqtane.Client diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj index 6c64032d..18f449d8 100644 --- a/Oqtane.Maui/Oqtane.Maui.csproj +++ b/Oqtane.Maui/Oqtane.Maui.csproj @@ -18,7 +18,7 @@ com.oqtane.maui - 10.0.4 + 10.1.0 1 @@ -54,11 +54,11 @@ - - - - - + + + + + diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 708e1d4d..42696e16 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,18 +12,18 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane - - - - - - + + + + + + diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 83edf33a..25f116ad 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/download/v10.1.0/Oqtane.Framework.10.1.0.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index f03cd1d9..4caf53a3 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,29 +12,29 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane - - - - - - + + + + + + - + - - - + + + - - - + + + diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 6338ba42..b1374aea 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 10.0.4 + 10.1.0 Shaun Walker .NET Foundation Oqtane Framework @@ -12,14 +12,14 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane - - + + diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index 5cdb39ae..840f3de0 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 10.0.4 + 10.1.0 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/v10.0.4 + https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 39124413..1849738e 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Install.zip" -Force diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index eb0e0a31..dba48f9b 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.4.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.1.0.Upgrade.zip" -Force diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index fe668866..a55b28a1 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -79,6 +79,7 @@ @((MarkupString)_scripts) @((MarkupString)_bodyResources) + @((MarkupString)_bodyContent) @if (_renderMode == RenderModes.Static) { @@ -107,6 +108,7 @@ private string _language = "en"; private string _headResources = ""; private string _bodyResources = ""; + private string _bodyContent = ""; private string _styleSheets = ""; private string _scripts = ""; private string _message = ""; @@ -157,15 +159,8 @@ var page = site.Pages.FirstOrDefault(item => item.Path.Equals(route.PagePath, StringComparison.OrdinalIgnoreCase)); if (page == null && route.PagePath == "") // naked path refers to site home page { - if (site.HomePageId != null) - { - page = site.Pages.FirstOrDefault(item => item.PageId == site.HomePageId); - } - if (page == null) - { - // fallback to use the first page in the collection - page = site.Pages.FirstOrDefault(); - } + // fallback to use the first page in the collection + page = site.Pages.FirstOrDefault(); } if (page == null) { @@ -193,43 +188,20 @@ await GetJwtToken(alias); } - // includes resources + // include resources var resources = await GetPageResources(alias, site, page, modules, int.Parse(route.ModuleId, CultureInfo.InvariantCulture), route.Action); ManageStyleSheets(resources); ManageScripts(resources, alias); + AddBodyContent(site.BodyContent); + AddBodyContent(page.BodyContent); + ManageLocalization(site); - // generate scripts + // PWA script if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) { _scripts += CreatePWAScript(alias, site, route); } - // set culture if not specified - string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName]; - if (cultureCookie == null) - { - // get default language for site - if (site.Languages.Any()) - { - // use default language if specified otherwise use first language in collection - cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code; - } - else - { - // fallback language - cultureCookie = LocalizationManager.GetDefaultCulture(); - } - // convert language code to culture cookie format (ie. "c=en|uic=en") - cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie)); - SetLocalizationCookie(cultureCookie); - } - - // set language for page - if (!string.IsNullOrEmpty(cultureCookie)) - { - _language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name; - } - // create initial PageState _pageState = new PageState { @@ -550,8 +522,6 @@ } else { - var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; - var dataAttributes = ""; if (!resource.DataAttributes.ContainsKey("data-reload")) { @@ -573,12 +543,24 @@ } } - return ""; + if (!string.IsNullOrEmpty(resource.Url)) + { + var url = (string.IsNullOrEmpty(resource.Url) || resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; + + return ""; + } + else + { + return "" + resource.Content + ""; + } } } @@ -751,11 +733,11 @@ { if (rendermode == RenderModes.Static || resource.ResourceType == ResourceType.Stylesheet || resource.Level == ResourceLevel.Site) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/" + type + "/" + name + "/").Replace("//", "/"); } - if (!resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) + if (!string.IsNullOrEmpty(resource.Url) && !resource.Url.Contains("://") && alias.BaseUrl != "" && !resource.Url.StartsWith(alias.BaseUrl)) { resource.Url = alias.BaseUrl + resource.Url; } @@ -803,4 +785,71 @@ } } } + + private void AddBodyContent(string content) + { + if (!string.IsNullOrEmpty(content)) + { + var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries); + foreach (var element in elements) + { + if (_renderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script"))) + { + if (!_bodyContent.Contains("<" + element) || element.StartsWith("/")) + { + _bodyContent += "<" + element; + } + } + } + _bodyContent += "\n"; + } + } + + private void ManageLocalization(Site site) + { + // get user culture (ui localization) + var uiCulture = site?.User?.CultureCode; + if (string.IsNullOrEmpty(uiCulture)) + { + // get default language for site + if (site.Languages.Any()) + { + // use default language if specified otherwise use first language in collection + uiCulture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code; + } + else + { + // fallback language + uiCulture = LocalizationManager.GetDefaultCulture(); + } + } + + // get site culture (content localization) + var culture = site?.CultureCode; + if (string.IsNullOrEmpty(culture)) + { + culture = uiCulture; + } + _language = culture; // html element language attribute + + // get culture cookie + string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName]; + if (cultureCookie != null) + { + // verify culture cookie (which could be inaccurate when using subfolders ie. domain.com/fr as cookies are based on domain) + var requestCulture = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie); + if (culture != requestCulture.Culture.Name || uiCulture != requestCulture.UICulture.Name) + { + // convert to culture cookie format (ie. "c=en|uic=en") and save cookie + cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture)); + SetLocalizationCookie(cultureCookie); + } + } + else + { + // convert to culture cookie format (ie. "c=en|uic=en") and save cookie + cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(culture, uiCulture)); + SetLocalizationCookie(cultureCookie); + } + } } diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 12a9c3fa..e456c61f 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -43,7 +43,7 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList()); + var hierarchy = _folders.GetFolders(SiteId).ToList(); foreach (Folder folder in hierarchy) { // note that Browse permission is used for this method @@ -281,47 +281,5 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } - - private static List GetFoldersHierarchy(List folders) - { - List hierarchy = new List(); - Action, Folder> getPath = null; - getPath = (folderList, folder) => - { - IEnumerable children; - int level; - if (folder == null) - { - level = -1; - children = folders.Where(item => item.ParentId == null); - } - else - { - level = folder.Level; - children = folders.Where(item => item.ParentId == folder.FolderId); - } - - foreach (Folder child in children) - { - child.Level = level + 1; - child.HasChildren = folders.Any(item => item.ParentId == child.FolderId); - hierarchy.Add(child); - getPath(folderList, child); - } - }; - folders = folders.OrderBy(item => item.Name).ToList(); - getPath(folders, null); - - // add any non-hierarchical items to the end of the list - foreach (Folder folder in folders) - { - if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null) - { - hierarchy.Add(folder); - } - } - - return hierarchy; - } } } diff --git a/Oqtane.Server/Controllers/LocalizationController.cs b/Oqtane.Server/Controllers/LocalizationController.cs index 5a572ba2..9f52c714 100644 --- a/Oqtane.Server/Controllers/LocalizationController.cs +++ b/Oqtane.Server/Controllers/LocalizationController.cs @@ -19,26 +19,54 @@ namespace Oqtane.Controllers _localizationManager = localizationManager; } - // GET: api/localization + // GET: api/localization?installed=true/false [HttpGet()] public IEnumerable Get(bool installed) { - string[] culturecodes; + string[] cultureCodes; if (installed) { - culturecodes = _localizationManager.GetInstalledCultures(); + cultureCodes = _localizationManager.GetInstalledCultures(); } else { - culturecodes = _localizationManager.GetSupportedCultures(); + cultureCodes = _localizationManager.GetSupportedCultures(); } - var cultures = culturecodes.Select(c => new Culture + + var cultures = cultureCodes.Select(c => new Culture { Name = CultureInfo.GetCultureInfo(c).Name, DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, IsDefault = _localizationManager.GetDefaultCulture() .Equals(CultureInfo.GetCultureInfo(c).Name, StringComparison.OrdinalIgnoreCase) - }); + }).ToList(); + + if (cultures.Count == 0) + { + cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = true }); + } + + return cultures.OrderBy(item => item.DisplayName); + } + + // GET: api/localization/neutral + [HttpGet("neutral")] + public IEnumerable Get() + { + var cultureCodes = _localizationManager.GetNeutralCultures(); + + var cultures = cultureCodes.Select(c => new Culture + { + Name = CultureInfo.GetCultureInfo(c).Name, + DisplayName = CultureInfo.GetCultureInfo(c).DisplayName, + IsDefault = false + }).ToList(); + + if (cultures.Count == 0) + { + cultures.Add(new Culture { Name = "en", DisplayName = "English", IsDefault = false }); + } + return cultures.OrderBy(item => item.DisplayName); } } diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs index 139da8e2..229b419f 100644 --- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs +++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs @@ -164,6 +164,7 @@ namespace Oqtane.Controllers { _moduleDefinitions.UpdateModuleDefinition(moduleDefinition); _syncManager.AddSyncEvent(_alias, EntityNames.ModuleDefinition, moduleDefinition.ModuleDefinitionId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed _logger.Log(LogLevel.Information, this, LogFunction.Update, "Module Definition Updated {ModuleDefinition}", moduleDefinition); } else diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 69bed0bc..a23ba365 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -1,16 +1,15 @@ using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Oqtane.Models; -using Oqtane.Shared; using System.Linq; -using Oqtane.Security; using System.Net; +using System.Security; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; using Oqtane.Enums; using Oqtane.Infrastructure; +using Oqtane.Models; using Oqtane.Repository; -using System.Xml.Linq; -using Microsoft.AspNetCore.Diagnostics; +using Oqtane.Security; +using Oqtane.Shared; namespace Oqtane.Controllers { @@ -498,6 +497,91 @@ namespace Oqtane.Controllers HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; } } - } + // POST api//5/6 + [HttpPost("{fromPageId}/{toPageId}/{usePagePermissions}")] + [Authorize(Roles = RoleNames.Registered)] + public void Post(int fromPageId, int toPageId, bool usePagePermissions) + { + var fromPage = _pages.GetPage(fromPageId); + if (fromPage != null && fromPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, fromPage.PermissionList)) + { + var toPage = _pages.GetPage(toPageId); + if (toPage != null && toPage.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, toPage.PermissionList)) + { + // copy modules + List pageModules = _pageModules.GetPageModules(fromPage.SiteId).ToList(); + foreach (PageModule pm in pageModules.Where(item => item.PageId == fromPage.PageId && !item.Module.AllPages && !item.IsDeleted)) + { + Module module; + + // determine if module is a shared instance (ie. exists on other pages) + if (!pageModules.Any(item => item.ModuleId == pm.ModuleId && item.PageId != fromPage.PageId)) + { + // create new module + module = new Module(); + module.SiteId = fromPage.SiteId; + module.PageId = toPageId; + module.ModuleDefinitionName = pm.Module.ModuleDefinitionName; + module.AllPages = false; + if (usePagePermissions) + { + module.PermissionList = toPage.PermissionList; + } + else + { + module.PermissionList = pm.Module.PermissionList; + } + module.PermissionList = module.PermissionList.Select(item => new Permission + { + SiteId = item.SiteId, + EntityName = EntityNames.Module, + EntityId = -1, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + }).ToList(); + + module = _modules.AddModule(module); + string content = _modules.ExportModule(pm.ModuleId); + if (content != "") + { + _modules.ImportModule(module.ModuleId, content); + } + } + else + { + // use existing module + module = pm.Module; + } + + PageModule pageModule = new PageModule(); + pageModule.PageId = toPageId; + pageModule.ModuleId = module.ModuleId; + pageModule.Title = pm.Title; + pageModule.Pane = pm.Pane; + pageModule.Order = pm.Order; + pageModule.ContainerType = pm.ContainerType; + pageModule.EffectiveDate = pm.EffectiveDate; + pageModule.ExpiryDate = pm.ExpiryDate; + pageModule.Header = pm.Header; + pageModule.Footer = pm.Footer; + + _pageModules.AddPageModule(pageModule); + } + + _syncManager.AddSyncEvent(_alias, EntityNames.Site, fromPage.SiteId, SyncEventActions.Refresh); + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } } diff --git a/Oqtane.Server/Controllers/SiteGroupController.cs b/Oqtane.Server/Controllers/SiteGroupController.cs new file mode 100644 index 00000000..61e2a174 --- /dev/null +++ b/Oqtane.Server/Controllers/SiteGroupController.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Security.Policy; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteGroupController : Controller + { + private readonly ISiteGroupRepository _siteGroupRepository; + private readonly ISyncManager _syncManager; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteGroupController(ISiteGroupRepository siteGroupRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + { + _siteGroupRepository = siteGroupRepository; + _syncManager = syncManager; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x + [HttpGet] + [Authorize(Roles = RoleNames.Admin)] + public IEnumerable Get(string siteid) + { + if (User.IsInRole(RoleNames.Host) || (int.TryParse(siteid, out int SiteId) && SiteId == _alias.SiteId)) + { + var siteGroups = _siteGroupRepository.GetSiteGroups(); + if (!User.IsInRole(RoleNames.Host)) + { + siteGroups = siteGroups.Where(item => item.PrimarySiteId == _alias.SiteId); + } + return siteGroups.ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Get Attempt {SiteId}", siteid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroup Get(int id) + { + var group = _siteGroupRepository.GetSiteGroup(id); + if (group != null) + { + return group; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public SiteGroup Post([FromBody] SiteGroup siteGroup) + { + if (ModelState.IsValid) + { + siteGroup = _siteGroupRepository.AddSiteGroup(siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Create); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Added {Group}", siteGroup); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Post Attempt {Group}", siteGroup); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroup = null; + } + return siteGroup; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public SiteGroup Put(int id, [FromBody] SiteGroup siteGroup) + { + if (ModelState.IsValid && siteGroup.SiteGroupId == id) + { + if (!User.IsInRole(RoleNames.Host) && siteGroup.Synchronize) + { + // admins can only update the synchronize field + siteGroup = _siteGroupRepository.GetSiteGroup(siteGroup.SiteGroupId, false); + siteGroup.Synchronize = true; + } + siteGroup = _siteGroupRepository.UpdateSiteGroup(siteGroup); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Update); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Updated {Group}", siteGroup); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Put Attempt {Group}", siteGroup); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroup = null; + } + return siteGroup; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Host)] + public void Delete(int id) + { + var siteGroup = _siteGroupRepository.GetSiteGroup(id); + if (siteGroup != null) + { + _siteGroupRepository.DeleteSiteGroup(id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroup, siteGroup.SiteGroupId, SyncEventActions.Delete); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Deleted {siteGroupId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Delete Attempt {siteGroupId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Controllers/SiteGroupMemberController.cs b/Oqtane.Server/Controllers/SiteGroupMemberController.cs new file mode 100644 index 00000000..2cb8442c --- /dev/null +++ b/Oqtane.Server/Controllers/SiteGroupMemberController.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteGroupMemberController : Controller + { + private readonly ISiteGroupMemberRepository _siteGroupMemberRepository; + private readonly ISyncManager _syncManager; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteGroupMemberController(ISiteGroupMemberRepository siteGroupMemberRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + { + _siteGroupMemberRepository = siteGroupMemberRepository; + _syncManager = syncManager; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET: api/?siteid=x&groupid=y + [HttpGet] + [Authorize(Roles = RoleNames.Host)] + public IEnumerable Get(string siteid, string groupid) + { + if (int.TryParse(siteid, out int SiteId) && int.TryParse(groupid, out int SiteGroupId)) + { + return _siteGroupMemberRepository.GetSiteGroupMembers(SiteId, SiteGroupId).ToList(); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Get Attempt for SiteId {SiteId} And SiteGroupId {SiteGroupId}", siteid, groupid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Get(int id) + { + var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id); + if (siteGroupMember != null) + { + return siteGroupMember; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Post([FromBody] SiteGroupMember siteGroupMember) + { + if (ModelState.IsValid) + { + siteGroupMember = _siteGroupMemberRepository.AddSiteGroupMember(siteGroupMember); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Create); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Group Member Added {SiteGroupMember}", siteGroupMember); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Post Attempt {SiteGroupMember}", siteGroupMember); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupMember = null; + } + return siteGroupMember; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = RoleNames.Host)] + public SiteGroupMember Put(int id, [FromBody] SiteGroupMember siteGroupMember) + { + if (ModelState.IsValid && siteGroupMember.SiteGroupId == id && _siteGroupMemberRepository.GetSiteGroupMember(siteGroupMember.SiteGroupId, false) != null) + { + siteGroupMember = _siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Update, "Site Group Member Updated {SiteGroupMember}", siteGroupMember); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Put Attempt {SiteGroupMember}", siteGroupMember); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteGroupMember = null; + } + return siteGroupMember; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = RoleNames.Host)] + public void Delete(int id) + { + var siteGroupMember = _siteGroupMemberRepository.GetSiteGroupMember(id); + if (siteGroupMember != null) + { + _siteGroupMemberRepository.DeleteSiteGroupMember(id); + _syncManager.AddSyncEvent(_alias, EntityNames.SiteGroupMember, siteGroupMember.SiteGroupMemberId, SyncEventActions.Delete); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, siteGroupMember.SiteId, SyncEventActions.Refresh); + _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Site Group Member Deleted {SiteGroupMemberId}", id); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Group Member Delete Attempt {SiteGroupMemberId}", id); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + } + } +} diff --git a/Oqtane.Server/Controllers/SiteTaskController.cs b/Oqtane.Server/Controllers/SiteTaskController.cs new file mode 100644 index 00000000..61842f84 --- /dev/null +++ b/Oqtane.Server/Controllers/SiteTaskController.cs @@ -0,0 +1,63 @@ +using System.Net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class SiteTaskController : Controller + { + private readonly ISiteTaskRepository _siteTasks; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public SiteTaskController(ISiteTaskRepository siteTasks, ILogManager logger, ITenantManager tenantManager) + { + _siteTasks = siteTasks; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public SiteTask Get(int id) + { + var siteTask = _siteTasks.GetSiteTask(id); + if (siteTask.SiteId == _alias.SiteId) + { + return siteTask; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Admin)] + public SiteTask Post([FromBody] SiteTask siteTask) + { + if (ModelState.IsValid && siteTask.SiteId == _alias.SiteId) + { + siteTask.IsCompleted = false; + siteTask = _siteTasks.AddSiteTask(siteTask); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Site Task Added {SiteTask}", siteTask); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Task Post Attempt {SiteTask}", siteTask); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + siteTask = null; + } + return siteTask; + } + } +} diff --git a/Oqtane.Server/Controllers/ThemeController.cs b/Oqtane.Server/Controllers/ThemeController.cs index 3f025b40..20547e1b 100644 --- a/Oqtane.Server/Controllers/ThemeController.cs +++ b/Oqtane.Server/Controllers/ThemeController.cs @@ -117,6 +117,7 @@ namespace Oqtane.Controllers { _themes.UpdateTheme(theme); _syncManager.AddSyncEvent(_alias, EntityNames.Theme, theme.ThemeId, SyncEventActions.Update); + _syncManager.AddSyncEvent(_alias, EntityNames.Site, _alias.SiteId, SyncEventActions.Refresh); // fingerprint changed _logger.Log(LogLevel.Information, this, LogFunction.Update, "Theme Updated {Theme}", theme); } else diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index aa6aa909..8d466356 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -418,42 +418,6 @@ namespace Oqtane.Controllers return requirements; } - // POST api//import?siteid=x&fileid=y¬ify=z - [HttpPost("import")] - [Authorize(Roles = RoleNames.Admin)] - public async Task> Import(string siteid, string fileid, string notify) - { - if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId && int.TryParse(fileid, out int FileId) && bool.TryParse(notify, out bool Notify)) - { - var file = _files.GetFile(FileId); - if (file != null) - { - if (_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) - { - return await _userManager.ImportUsers(SiteId, _files.GetFilePath(file), Notify); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Import File Does Not Exist {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; - return null; - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Import Attempt {SiteId} {FileId}", siteid, fileid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - // GET: api//passkey?id=x [HttpGet("passkey")] [Authorize] diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 8d0c3f75..fceb8f8f 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -233,6 +233,9 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); @@ -282,6 +285,9 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // managers services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 87d6aab0..69d37b3f 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -594,7 +594,8 @@ namespace Oqtane.Infrastructure Prerender = (rendermode == RenderModes.Interactive), Hybrid = false, EnhancedNavigation = true, - TenantId = tenant.TenantId + CultureCode = "en", + TenantId = tenant.TenantId // required for site creation }; site = sites.AddSite(site); diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index 8da91ec6..4340db89 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -22,7 +22,6 @@ namespace Oqtane.Infrastructure { void InstallPackages(); bool UninstallPackage(string PackageName); - int RegisterAssemblies(); Task UpgradeFramework(bool backup); void RestartApplication(); } @@ -61,10 +60,6 @@ namespace Oqtane.Infrastructure Directory.CreateDirectory(sourceFolder); } - // read assembly log - var assemblyLogPath = Path.Combine(sourceFolder, "assemblies.log"); - var assemblies = GetAssemblyLog(assemblyLogPath); - // install Nuget packages in secure Packages folder var packages = Directory.GetFiles(sourceFolder, "*.nupkg"); foreach (string packagename in packages) @@ -162,27 +157,6 @@ namespace Oqtane.Infrastructure { manifest = true; } - - // register assembly - if (Path.GetExtension(filename) == ".dll") - { - // do not register licensing assemblies - if (!Path.GetFileName(filename).StartsWith("Oqtane.Licensing.")) - { - // if package version was not installed previously - if (!File.Exists(Path.Combine(sourceFolder, name + ".log"))) - { - if (assemblies.ContainsKey(Path.GetFileName(filename))) - { - assemblies[Path.GetFileName(filename)] += 1; - } - else - { - assemblies.Add(Path.GetFileName(filename), 1); - } - } - } - } } } @@ -212,12 +186,6 @@ namespace Oqtane.Infrastructure File.Delete(packagename); } - if (packages.Length != 0) - { - // save assembly log - SetAssemblyLog(assemblyLogPath, assemblies); - } - return errors; } @@ -270,10 +238,6 @@ namespace Oqtane.Infrastructure { if (!string.IsNullOrEmpty(PackageName)) { - // read assembly log - var assemblyLogPath = Path.Combine(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), "assemblies.log"); - var assemblies = GetAssemblyLog(assemblyLogPath); - // get manifest with highest version string packagename = ""; string[] packages = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder), PackageName + "*.log"); @@ -298,23 +262,7 @@ namespace Oqtane.Infrastructure // do not remove licensing assemblies if (!Path.GetFileName(filepath).StartsWith("Oqtane.Licensing.")) { - // use assembly log to determine if assembly is used in other packages - if (assemblies.ContainsKey(Path.GetFileName(filepath))) - { - if (assemblies[Path.GetFileName(filepath)] == 1) - { - DeleteFile(filepath); - assemblies.Remove(Path.GetFileName(filepath)); - } - else - { - assemblies[Path.GetFileName(filepath)] -= 1; - } - } - else // does not exist in assembly log - { - DeleteFile(filepath); - } + DeleteFile(filepath); } } else // not an assembly @@ -329,9 +277,6 @@ namespace Oqtane.Infrastructure File.Delete(asset); } - // save assembly log - SetAssemblyLog(assemblyLogPath, assemblies); - return true; } } @@ -351,64 +296,6 @@ namespace Oqtane.Infrastructure } } - public int RegisterAssemblies() - { - var assemblyLogPath = GetAssemblyLogPath(); - var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); - - var assemblies = GetAssemblyLog(assemblyLogPath); - - // remove assemblies that no longer exist - foreach (var dll in assemblies) - { - if (!File.Exists(Path.Combine(binFolder, dll.Key))) - { - assemblies.Remove(dll.Key); - } - } - // add assemblies which are not registered - foreach (var dll in Directory.GetFiles(binFolder, "*.dll")) - { - if (!assemblies.ContainsKey(Path.GetFileName(dll))) - { - assemblies.Add(Path.GetFileName(dll), 1); - } - } - - SetAssemblyLog(assemblyLogPath, assemblies); - - return assemblies.Count; - } - - private string GetAssemblyLogPath() - { - string packagesFolder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); - if (!Directory.Exists(packagesFolder)) - { - Directory.CreateDirectory(packagesFolder); - } - return Path.Combine(packagesFolder, "assemblies.log"); - } - - private static Dictionary GetAssemblyLog(string assemblyLogPath) - { - Dictionary assemblies = new Dictionary(); - if (File.Exists(assemblyLogPath)) - { - assemblies = JsonSerializer.Deserialize>(File.ReadAllText(assemblyLogPath)); - } - return assemblies; - } - - private static void SetAssemblyLog(string assemblyLogPath, Dictionary assemblies) - { - if (File.Exists(assemblyLogPath)) - { - File.Delete(assemblyLogPath); - } - File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true })); - } - public async Task UpgradeFramework(bool backup) { string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); diff --git a/Oqtane.Server/Infrastructure/Interfaces/ISiteTask.cs b/Oqtane.Server/Infrastructure/Interfaces/ISiteTask.cs new file mode 100644 index 00000000..ab26d820 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/ISiteTask.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public interface ISiteTask + { + string ExecuteTask(IServiceProvider provider, Site site, string parameters); + + Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters); + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 5d0ba275..4f5fbd74 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -38,7 +38,7 @@ namespace Oqtane.Infrastructure // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); - foreach (Site site in sites) + foreach (Site site in sites.Where(item => !item.IsDeleted)) { log += "Processing Notifications For Site: " + site.Name + "
"; @@ -48,7 +48,7 @@ namespace Oqtane.Infrastructure // get site settings var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1); - if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") + if (settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") { bool valid = true; if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") @@ -121,11 +121,23 @@ namespace Oqtane.Infrastructure { if (settingRepository.GetSettingValue(settings, "SMTPAuthentication", "Basic") == "Basic") { - // it is possible to use basic without any authentication (not recommended) if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") { - await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), + try + { + await client.AuthenticateAsync(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), settingRepository.GetSettingValue(settings, "SMTPPassword", "")); + } + catch (Exception ex) + { + log += "SMTP Not Configured Properly In Site Settings - Basic Authentication Failed Using Username And Password - " + ex.Message + "
"; + valid = false; + } + + } + else + { + // it is possible to use basic without any authentication (not recommended) } } else @@ -278,7 +290,7 @@ namespace Oqtane.Infrastructure } else { - log += "Site Deleted Or SMTP Disabled In Site Settings
"; + log += "SMTP Disabled In Site Settings
"; } } else diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 89a2238d..0b9d3288 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -33,11 +33,11 @@ namespace Oqtane.Infrastructure var visitorRepository = provider.GetRequiredService(); var notificationRepository = provider.GetRequiredService(); var urlMappingRepository = provider.GetRequiredService(); - var installationManager = provider.GetRequiredService(); + var siteTaskRepository = provider.GetRequiredService(); // iterate through sites for current tenant List sites = siteRepository.GetSites().ToList(); - foreach (Site site in sites) + foreach (Site site in sites.Where(item => !item.IsDeleted)) { log += "
Processing Site: " + site.Name + "
"; int count; @@ -95,17 +95,18 @@ namespace Oqtane.Infrastructure { log += $"Error Purging Broken Urls - {ex.Message}
"; } - } - // register assemblies - try - { - var assemblies = installationManager.RegisterAssemblies(); - log += "
" + assemblies.ToString() + " Assemblies Registered
"; - } - catch (Exception ex) - { - log += $"
Error Registering Assemblies - {ex.Message}
"; + // purge completed site tasks + retention = 30; // 30 day default + try + { + count = siteTaskRepository.DeleteSiteTasks(site.SiteId, retention); + log += count.ToString() + " Completed Tasks Purged
"; + } + catch (Exception ex) + { + log += $"Error Purging Completed Site Tasks - {ex.Message}
"; + } } return log; diff --git a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs index 9b2d2f83..6549c14d 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SearchIndexJob.cs @@ -40,7 +40,7 @@ namespace Oqtane.Infrastructure var searchService = provider.GetRequiredService(); var sites = siteRepository.GetSites().ToList(); - foreach (var site in sites) + foreach (var site in sites.Where(item => !item.IsDeleted)) { log += $"Indexing Site: {site.Name}
"; diff --git a/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs new file mode 100644 index 00000000..e82ba007 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/SiteTaskJob.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class SiteTaskJob : HostedServiceBase + { + public SiteTaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Site Task Job"; + Frequency = "m"; // run every minute + Interval = 1; + IsEnabled = true; + } + + // job is executed for each tenant in installation + public override async Task ExecuteJobAsync(IServiceProvider provider) + { + var log = ""; + + // resolve services + var tenantManager = provider.GetRequiredService(); + var siteRepository = provider.GetRequiredService(); + var siteTaskRepository = provider.GetRequiredService(); + + var tenant = tenantManager.GetTenant(); + + // iterate through sites for current tenant + var sites = siteRepository.GetSites().ToList(); + foreach (var site in sites.Where(item => !item.IsDeleted)) + { + log += $"Processing Site: {site.Name}
"; + + // get incomplete tasks for site + var tasks = siteTaskRepository.GetSiteTasks(site.SiteId).ToList(); + if (tasks != null && tasks.Any()) + { + foreach (var task in tasks) + { + log += $"Executing Task: {task.Name}
"; + + Type taskType = Type.GetType(task.Type); + if (taskType != null && taskType.GetInterface(nameof(ISiteTask)) != null) + { + try + { + tenantManager.SetAlias(tenant.TenantId, site.SiteId); + + var taskObject = ActivatorUtilities.CreateInstance(provider, taskType); + var taskLog = ((ISiteTask)taskObject).ExecuteTask(provider, site, task.Parameters); + taskLog += await ((ISiteTask)taskObject).ExecuteTaskAsync(provider, site, task.Parameters); + + task.Status = taskLog; + } + catch (Exception ex) + { + task.Status = "Error: " + ex.Message; + } + } + else + { + task.Status = $"Error: Task {task.Name} Has An Invalid Type {task.Type}
"; + } + + // update task + task.IsCompleted = true; + siteTaskRepository.UpdateSiteTask(task); + + log += task.Status + "
"; + } + } + else + { + log += "No Tasks To Execute
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs new file mode 100644 index 00000000..24e3ed19 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -0,0 +1,895 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class SynchronizationJob : HostedServiceBase + { + // JobType = "Oqtane.Infrastructure.SynchronizationJob, Oqtane.Server" + + // synchronization only supports sites in the same tenant (database) + // module title is used as a key to identify module instances on a page + // modules must implement ISynchronizable interface for content synchronization + // change detection does not support deleted items as key values will usually be different due to localization + + // define settings that should not be synchronized (should be extensible in the future) + List excludedSettings = new List() { + new Setting { EntityName = EntityNames.Site, SettingName = "Search_LastIndexedOn" } + }; + + public SynchronizationJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Synchronization Job"; + Frequency = "m"; // minute + Interval = 1; + IsEnabled = false; + } + + // job is executed for each tenant in installation + public override string ExecuteJob(IServiceProvider provider) + { + string log = ""; + + var siteGroupRepository = provider.GetRequiredService(); + var siteGroupMemberRepository = provider.GetRequiredService(); + var siteRepository = provider.GetRequiredService(); + var aliasRepository = provider.GetRequiredService(); + var tenantManager = provider.GetRequiredService(); + var settingRepository = provider.GetRequiredService(); + + List siteGroupMembers = null; + List sites = null; + List aliases = null; + + // get site groups + var siteGroups = siteGroupRepository.GetSiteGroups(); + + // iterate through site groups which need to be synchronized + foreach (var siteGroup in siteGroups.Where(item => item.Synchronize && (item.Type == SiteGroupTypes.Synchronization || item.Type == SiteGroupTypes.ChangeDetection))) + { + // get data + if (siteGroupMembers == null) + { + siteGroupMembers = siteGroupMemberRepository.GetSiteGroupMembers().ToList(); + sites = siteRepository.GetSites().ToList(); + aliases = aliasRepository.GetAliases().ToList(); + } + + var primaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroup.PrimarySiteId && item.IsDefault).Name; + log += (siteGroup.Type == SiteGroupTypes.Synchronization) ? "Synchronizing " : "Comparing "; + log += $"Primary Site: {sites.First(item => item.SiteId == siteGroup.PrimarySiteId).Name} - {CreateLink(primaryAliasName)}
"; + + // get primary site + var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); + if (primarySite != null && !primarySite.IsDeleted) + { + // update flag to prevent job from processing group again + siteGroup.Synchronize = false; + siteGroupRepository.UpdateSiteGroup(siteGroup); + + // iterate through sites in site group + foreach (var siteGroupMember in siteGroupMembers.Where(item => item.SiteGroupId == siteGroup.SiteGroupId && item.SiteId != siteGroup.PrimarySiteId)) + { + // get secondary site + var secondarySite = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId); + if (secondarySite != null) + { + // get default alias for site + var secondaryAliasName = "https://" + aliases.First(item => item.TenantId == tenantManager.GetTenant().TenantId && item.SiteId == siteGroupMember.SiteId && item.IsDefault).Name; + siteGroupMember.AliasName = (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) ? secondaryAliasName : primaryAliasName; + + // initialize SynchronizedOn + if (siteGroupMember.SynchronizedOn == null) + { + siteGroupMember.SynchronizedOn = DateTime.MinValue; + } + + // synchronize site + var siteLog = SynchronizeSite(provider, tenantManager, settingRepository, siteGroupMember, primarySite, secondarySite); + if (string.IsNullOrEmpty(siteLog)) + { + siteLog = (siteGroupMember.SynchronizedOn != DateTime.MinValue) ? "No Changes Identified
" : "Initialization Complete
"; + } + + // set synchronized date/time + siteGroupMember.SynchronizedOn = DateTime.UtcNow; + siteGroupMemberRepository.UpdateSiteGroupMember(siteGroupMember); + + log += $"With Secondary Site: {secondarySite.Name} - {CreateLink(secondaryAliasName)}
" + siteLog; + } + else + { + log += $"Site Group {siteGroup.Name} Has A SiteId {siteGroupMember.SiteId} Which Does Not Exist
"; + } + } + } + else + { + log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist Or Is Deleted
"; + } + } + + if (string.IsNullOrEmpty(log)) + { + log = "No Site Groups Require Synchronization
"; + } + + return log; + } + + private string SynchronizeSite(IServiceProvider provider, ITenantManager tenantManager, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, Site primarySite, Site secondarySite) + { + var log = ""; + + // synchronize roles + log += SynchronizeRoles(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + + // synchronize folders/files + log += SynchronizeFolders(provider, settingRepository, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + + // synchronize pages/modules + log += SynchronizePages(provider, settingRepository, tenantManager, siteGroupMember, primarySite.SiteId, secondarySite.SiteId); + + // synchronize site + if (primarySite.ModifiedOn > siteGroupMember.SynchronizedOn) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + secondarySite.TimeZoneId = primarySite.TimeZoneId; + secondarySite.CultureCode = primarySite.CultureCode; + if (secondarySite.LogoFileId != primarySite.LogoFileId) + { + secondarySite.LogoFileId = ResolveFileId(provider, primarySite.LogoFileId, secondarySite.SiteId); + } + if (secondarySite.FaviconFileId != primarySite.FaviconFileId) + { + secondarySite.FaviconFileId = ResolveFileId(provider, primarySite.FaviconFileId, secondarySite.SiteId); ; + } + secondarySite.DefaultThemeType = primarySite.DefaultThemeType; + secondarySite.DefaultContainerType = primarySite.DefaultContainerType; + secondarySite.AdminContainerType = primarySite.AdminContainerType; + secondarySite.PwaIsEnabled = primarySite.PwaIsEnabled; + if (secondarySite.PwaAppIconFileId != primarySite.PwaAppIconFileId) + { + secondarySite.PwaAppIconFileId = ResolveFileId(provider, primarySite.PwaAppIconFileId, secondarySite.SiteId); ; + } + if (secondarySite.PwaSplashIconFileId != primarySite.PwaSplashIconFileId) + { + secondarySite.PwaSplashIconFileId = ResolveFileId(provider, primarySite.PwaSplashIconFileId, secondarySite.SiteId); ; + } + secondarySite.AllowRegistration = primarySite.AllowRegistration; + secondarySite.VisitorTracking = primarySite.VisitorTracking; + secondarySite.CaptureBrokenUrls = primarySite.CaptureBrokenUrls; + secondarySite.SiteGuid = primarySite.SiteGuid; + secondarySite.RenderMode = primarySite.RenderMode; + secondarySite.Runtime = primarySite.Runtime; + secondarySite.Prerender = primarySite.Prerender; + secondarySite.Hybrid = primarySite.Hybrid; + secondarySite.EnhancedNavigation = primarySite.EnhancedNavigation; + secondarySite.Version = primarySite.Version; + secondarySite.HeadContent = primarySite.HeadContent; + secondarySite.BodyContent = primarySite.BodyContent; + secondarySite.CreatedBy = primarySite.CreatedBy; + secondarySite.CreatedOn = primarySite.CreatedOn; + secondarySite.ModifiedBy = primarySite.ModifiedBy; + secondarySite.ModifiedOn = primarySite.ModifiedOn; + secondarySite.IsDeleted = primarySite.IsDeleted; + secondarySite.DeletedBy = primarySite.DeletedBy; + secondarySite.DeletedOn = primarySite.DeletedOn; + + var siteRepository = provider.GetRequiredService(); + siteRepository.UpdateSite(secondarySite); + log += Log(siteGroupMember, $"Site Updated: {secondarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); + } + else // change detection + { + log += Log(siteGroupMember, $"Site Updated: {primarySite.Name} - {CreateLink(siteGroupMember.AliasName)}"); + } + } + + // site settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Site, primarySite.SiteId, secondarySite.SiteId); + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log))) + { + // clear cache for secondary site if any content was Synchronized + var syncManager = provider.GetRequiredService(); + var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySite.SiteId }; + syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySite.SiteId, SyncEventActions.Refresh); + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.ChangeDetection && !string.IsNullOrEmpty(log)) + { + // send change log to administrators + log += SendNotifications(provider, siteGroupMember, secondarySite.SiteId, secondarySite.Name, log); + } + + return log; + } + + private int? ResolveFileId(IServiceProvider provider, int? fileId, int siteId) + { + if (fileId != null) + { + var fileRepository = provider.GetRequiredService(); + var file = fileRepository.GetFile(fileId.Value); + fileId = fileRepository.GetFile(siteId, file.Folder.Path, file.Name).FileId; + } + return fileId; + } + + private string SynchronizeRoles(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + { + // get roles + var roleRepository = provider.GetRequiredService(); + var primaryRoles = roleRepository.GetRoles(primarySiteId); + var secondaryRoles = roleRepository.GetRoles(secondarySiteId).ToList(); + var log = ""; + + foreach (var primaryRole in primaryRoles) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var role = secondaryRoles.FirstOrDefault(item => item.Name == primaryRole.Name); + + var secondaryRole = role; + if (secondaryRole == null) + { + secondaryRole = new Role(); + secondaryRole.SiteId = secondarySiteId; + } + + if (role == null || primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryRole.Name = primaryRole.Name; + secondaryRole.Description = primaryRole.Description; + secondaryRole.IsAutoAssigned = primaryRole.IsAutoAssigned; + secondaryRole.IsSystem = primaryRole.IsSystem; + + if (role == null) + { + roleRepository.AddRole(secondaryRole); + log += Log(siteGroupMember, $"Role Added: {secondaryRole.Name}"); + } + else + { + roleRepository.UpdateRole(secondaryRole); + log += Log(siteGroupMember, $"Role Updated: {secondaryRole.Name}"); + } + } + + if (role != null) + { + secondaryRoles.Remove(role); + } + } + else // change detection + { + if (primaryRole.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Role Updated: {primaryRole.Name}"); + } + } + + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove roles in the secondary site which do not exist in the primary site + foreach (var secondaryRole in secondaryRoles) + { + roleRepository.DeleteRole(secondaryRole.RoleId); + log += Log(siteGroupMember, $"Role Deleted: {secondaryRole.Name}"); + } + } + + // settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Role, primarySiteId, secondarySiteId); + + return log; + } + + private string SynchronizeFolders(IServiceProvider provider, ISettingRepository settingRepository, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + { + var folderRepository = provider.GetRequiredService(); + var fileRepository = provider.GetRequiredService(); + var log = ""; + + // get folders (ignore personalized) + var primaryFolders = folderRepository.GetFolders(primarySiteId).Where(item => !item.Path.StartsWith("Users/")); + var secondaryFolders = folderRepository.GetFolders(secondarySiteId).Where(item => !item.Path.StartsWith("Users/")).ToList(); + + // iterate through folders + foreach (var primaryFolder in primaryFolders) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var folder = secondaryFolders.FirstOrDefault(item => item.Path == primaryFolder.Path); + + var secondaryFolder = folder; + if (secondaryFolder == null) + { + secondaryFolder = new Folder(); + secondaryFolder.SiteId = secondarySiteId; + } + + if (folder == null || primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryFolder.ParentId = null; + if (primaryFolder.ParentId != null) + { + var parentFolder = folderRepository.GetFolder(secondarySiteId, primaryFolders.First(item => item.FolderId == primaryFolder.ParentId).Path); + if (parentFolder != null) + { + secondaryFolder.ParentId = parentFolder.FolderId; + } + } + secondaryFolder.Type = primaryFolder.Type; + secondaryFolder.Name = primaryFolder.Name; + secondaryFolder.Order = primaryFolder.Order; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.Capacity = primaryFolder.Capacity; + secondaryFolder.ImageSizes = primaryFolder.ImageSizes; + secondaryFolder.IsSystem = primaryFolder.IsSystem; + secondaryFolder.PermissionList = SynchronizePermissions(primaryFolder.PermissionList, secondarySiteId); + + if (folder == null) + { + folderRepository.AddFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Added: {secondaryFolder.Path}"); + } + else + { + folderRepository.UpdateFolder(secondaryFolder); + log += Log(siteGroupMember, $"Folder Updated: {secondaryFolder.Path}"); + } + } + + if (folder != null) + { + secondaryFolders.Remove(folder); + } + + // folder settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, secondaryFolder.FolderId); + + // files + log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, secondaryFolder); + } + else // change detection + { + if (primaryFolder.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Folder Updated: {primaryFolder.Path}"); + + // folder settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Folder, primaryFolder.FolderId, -1); + + // files + log += SynchronizeFiles(provider, folderRepository, fileRepository, siteGroupMember, primaryFolder, null); + } + } + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove folders in the secondary site which do not exist in the primary site + foreach (var secondaryFolder in secondaryFolders) + { + folderRepository.DeleteFolder(secondaryFolder.FolderId); + log += Log(siteGroupMember, $"Folder Deleted: {secondaryFolder.Path}"); + } + } + + return log; + } + + private string SynchronizeFiles(IServiceProvider provider, IFolderRepository folderRepository, IFileRepository fileRepository, SiteGroupMember siteGroupMember, Folder primaryFolder, Folder secondaryFolder) + { + var log = ""; + + // get files for folder + var primaryFiles = fileRepository.GetFiles(primaryFolder.FolderId); + var secondaryFiles = new List(); + if (secondaryFolder != null) + { + secondaryFiles = fileRepository.GetFiles(secondaryFolder.FolderId).ToList(); + } + + foreach (var primaryFile in primaryFiles) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var file = secondaryFiles.FirstOrDefault(item => item.Name == primaryFile.Name); + + var secondaryFile = file; + if (secondaryFile == null) + { + secondaryFile = new Models.File(); + secondaryFile.FolderId = secondaryFolder.FolderId; + secondaryFile.Name = primaryFile.Name; + } + + if (file == null || primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryFile.Extension = primaryFile.Extension; + secondaryFile.Size = primaryFile.Size; + secondaryFile.ImageHeight = primaryFile.ImageHeight; + secondaryFile.ImageWidth = primaryFile.ImageWidth; + secondaryFile.Description = primaryFile.Description; + + if (file == null) + { + fileRepository.AddFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + log += Log(siteGroupMember, $"File Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); + } + else + { + fileRepository.UpdateFile(secondaryFile); + SynchronizeFile(folderRepository, primaryFolder, primaryFile, secondaryFolder, secondaryFile); + log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); + } + } + + if (file != null) + { + secondaryFiles.Remove(file); + } + } + else // change detection + { + if (primaryFile.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"File Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryFolder.Path + primaryFile.Name)}"); + } + } + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove files in the secondary site which do not exist in the primary site + foreach (var secondaryFile in secondaryFiles) + { + fileRepository.DeleteFile(secondaryFile.FileId); + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + System.IO.File.Delete(secondaryPath); + log += Log(siteGroupMember, $"File Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryFolder.Path + secondaryFile.Name)}"); + } + } + + return log; + } + + private void SynchronizeFile(IFolderRepository folderRepository, Folder primaryFolder, Models.File primaryFile, Folder secondaryFolder, Models.File secondaryFile) + { + var primaryPath = Path.Combine(folderRepository.GetFolderPath(primaryFolder), primaryFile.Name); + if (System.IO.File.Exists(primaryPath)) + { + var secondaryPath = Path.Combine(folderRepository.GetFolderPath(secondaryFolder), secondaryFile.Name); + if (!Directory.Exists(Path.GetDirectoryName(secondaryPath))) + { + Directory.CreateDirectory(Path.GetDirectoryName(secondaryPath)); + } + System.IO.File.Copy(primaryPath, secondaryPath, true); + } + } + + private string SynchronizePages(IServiceProvider provider, ISettingRepository settingRepository, ITenantManager tenantManager, SiteGroupMember siteGroupMember, int primarySiteId, int secondarySiteId) + { + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + var moduleRepository = provider.GetRequiredService(); + var log = ""; + + int tenantId = tenantManager.GetTenant().TenantId; + tenantManager.SetAlias(tenantId, primarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + var primaryPageModules = pageModuleRepository.GetPageModules(primarySiteId).ToList(); + tenantManager.SetAlias(tenantId, secondarySiteId); // required by ModuleDefinitionRepository.LoadModuleDefinitions() + var secondaryPageModules = pageModuleRepository.GetPageModules(secondarySiteId).ToList(); + + // get pages (ignore personalized) + var primaryPages = pageRepository.GetPages(primarySiteId).Where(item => item.UserId == null); + var secondaryPages = pageRepository.GetPages(secondarySiteId).Where(item => item.UserId == null).ToList(); + + // iterate through primary pages + foreach (var primaryPage in primaryPages) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var page = secondaryPages.FirstOrDefault(item => item.Path == primaryPage.Path); + + var secondaryPage = page; + if (secondaryPage == null) + { + secondaryPage = new Page(); + secondaryPage.SiteId = secondarySiteId; + } + + if (page == null || primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryPage.Path = primaryPage.Path; + secondaryPage.Name = primaryPage.Name; + secondaryPage.ParentId = null; + if (primaryPage.ParentId != null) + { + var parentPage = pageRepository.GetPage(primaryPages.First(item => item.PageId == primaryPage.ParentId).Path, secondarySiteId); + if (parentPage != null) + { + secondaryPage.ParentId = parentPage.PageId; + } + } + secondaryPage.Title = primaryPage.Title; + secondaryPage.Order = primaryPage.Order; + secondaryPage.Url = primaryPage.Url; + secondaryPage.ThemeType = primaryPage.ThemeType; + secondaryPage.DefaultContainerType = primaryPage.DefaultContainerType; + secondaryPage.HeadContent = primaryPage.HeadContent; + secondaryPage.BodyContent = primaryPage.BodyContent; + secondaryPage.Icon = primaryPage.Icon; + secondaryPage.IsNavigation = primaryPage.IsNavigation; + secondaryPage.IsClickable = primaryPage.IsClickable; + secondaryPage.UserId = null; + secondaryPage.IsPersonalizable = primaryPage.IsPersonalizable; + secondaryPage.EffectiveDate = primaryPage.EffectiveDate; + secondaryPage.ExpiryDate = primaryPage.ExpiryDate; + secondaryPage.CreatedBy = primaryPage.CreatedBy; + secondaryPage.CreatedOn = primaryPage.CreatedOn; + secondaryPage.ModifiedBy = primaryPage.ModifiedBy; + secondaryPage.ModifiedOn = primaryPage.ModifiedOn; + secondaryPage.DeletedBy = primaryPage.DeletedBy; + secondaryPage.DeletedOn = primaryPage.DeletedOn; + secondaryPage.IsDeleted = primaryPage.IsDeleted; + secondaryPage.PermissionList = SynchronizePermissions(primaryPage.PermissionList, secondarySiteId); + + if (page == null) + { + secondaryPage = pageRepository.AddPage(secondaryPage); + log += Log(siteGroupMember, $"Page Added: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + else + { + secondaryPage = pageRepository.UpdatePage(secondaryPage); + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + } + + if (page != null) + { + secondaryPages.Remove(page); + } + + // page settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, secondaryPage.PageId); + + // modules + log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, secondaryPage, secondarySiteId); + } + else // change detection + { + if (primaryPage.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Page Updated: {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + + // page settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Page, primaryPage.PageId, -1); + + // modules + log += SynchronizeModules(provider, settingRepository, pageModuleRepository, moduleRepository, siteGroupMember, primaryPageModules, secondaryPageModules, primaryPage, null, secondarySiteId); + } + + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove pages in the secondary site which do not exist in the primary site + foreach (var secondaryPage in secondaryPages) + { + pageRepository.DeletePage(secondaryPage.PageId); + log += Log(siteGroupMember, $"Page Deleted: {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization && (siteGroupMember.SynchronizedOn == DateTime.MinValue || !string.IsNullOrEmpty(log))) + { + // clear cache for secondary site if any content was Synchronized + var syncManager = provider.GetRequiredService(); + var alias = new Alias { TenantId = tenantManager.GetTenant().TenantId, SiteId = secondarySiteId }; + syncManager.AddSyncEvent(alias, EntityNames.Site, secondarySiteId, SyncEventActions.Refresh); + } + + return log; + } + + private string SynchronizeModules(IServiceProvider provider, ISettingRepository settingRepository, IPageModuleRepository pageModuleRepository, IModuleRepository moduleRepository, SiteGroupMember siteGroupMember, List primaryPageModules, List secondaryPageModules, Page primaryPage, Page secondaryPage, int secondarySiteId) + { + var log = ""; + var removePageModules = secondaryPageModules.Where(item => item.PageId == secondaryPage.PageId).ToList(); + + // iterate through primary modules on primary page + foreach (var primaryPageModule in primaryPageModules.Where(item => item.PageId == primaryPage.PageId)) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var pageModule = secondaryPageModules.FirstOrDefault(item => item.PageId == secondaryPage.PageId && item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower()); + + var secondaryPageModule = pageModule; + if (secondaryPageModule == null) + { + secondaryPageModule = new PageModule(); + secondaryPageModule.PageId = secondaryPage.PageId; + secondaryPageModule.Module = new Module(); + secondaryPageModule.Module.SiteId = secondarySiteId; + secondaryPageModule.Module.ModuleDefinitionName = primaryPageModule.Module.ModuleDefinitionName; + } + + if (pageModule == null || primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn || primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) + { + // set all properties + secondaryPageModule.Title = primaryPageModule.Title; + secondaryPageModule.Pane = primaryPageModule.Pane; + secondaryPageModule.Order = primaryPageModule.Order; + secondaryPageModule.ContainerType = primaryPageModule.ContainerType; + secondaryPageModule.Header = primaryPageModule.Header; + secondaryPageModule.Footer = primaryPageModule.Footer; + secondaryPageModule.IsDeleted = primaryPageModule.IsDeleted; + secondaryPageModule.Module.PermissionList = SynchronizePermissions(primaryPageModule.Module.PermissionList, secondarySiteId); + secondaryPageModule.Module.AllPages = false; + secondaryPageModule.Module.IsDeleted = false; + + if (pageModule == null) + { + // check if module exists in site (ie. a shared instance) + var module = secondaryPageModules.FirstOrDefault(item => item.Module.ModuleDefinitionName == primaryPageModule.Module.ModuleDefinitionName && item.Title.ToLower() == primaryPageModule.Title.ToLower())?.Module; + if (module == null) + { + module = moduleRepository.AddModule(secondaryPageModule.Module); + log += Log(siteGroupMember, $"Module Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + if (module != null) + { + secondaryPageModule.ModuleId = module.ModuleId; + secondaryPageModule.Module = null; // remove tracking + secondaryPageModule = pageModuleRepository.AddPageModule(secondaryPageModule); + log += Log(siteGroupMember, $"Module Instance Added: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + secondaryPageModule.Module = module; + } + } + else + { + // update existing module + if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) + { + moduleRepository.UpdateModule(secondaryPageModule.Module); + log += Log(siteGroupMember, $"Module Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) + { + secondaryPageModule = pageModuleRepository.UpdatePageModule(secondaryPageModule); + log += Log(siteGroupMember, $"Module Instance Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + } + } + + if (pageModule != null) + { + removePageModules.Remove(pageModule); + } + + // module settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, secondaryPageModule.ModuleId); + + // module content + log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, secondaryPageModule, primaryPage, secondaryPage); + } + else // change detection + { + if (primaryPageModule.Module.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Module Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + if (primaryPageModule.ModifiedOn > siteGroupMember.SynchronizedOn) + { + log += Log(siteGroupMember, $"Module Instance Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + + // module settings + log += SynchronizeSettings(settingRepository, siteGroupMember, EntityNames.Module, primaryPageModule.ModuleId, -1); + + // module content + log += SynchronizeModuleContent(provider, siteGroupMember, primaryPageModule, null, primaryPage, secondaryPage); + } + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // remove modules on the secondary page which do not exist on the primary page + foreach (var secondaryPageModule in removePageModules) + { + pageModuleRepository.DeletePageModule(secondaryPageModule.PageModuleId); + log += Log(siteGroupMember, $"Module Instance Deleted: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPageModule.Page.Path)}"); + } + } + + return log; + } + + private string SynchronizeModuleContent(IServiceProvider provider, SiteGroupMember siteGroupMember, PageModule primaryPageModule, PageModule secondaryPageModule, Page primaryPage, Page secondaryPage) + { + var log = ""; + + if (primaryPageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(primaryPageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(ISynchronizable)) != null) + { + try + { + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((ISynchronizable)moduleObject).ExtractModule(primaryPageModule.Module, siteGroupMember.SynchronizedOn.Value); + if (!string.IsNullOrEmpty(moduleContent)) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + ((ISynchronizable)moduleObject).LoadModule(secondaryPageModule.Module, moduleContent); + log += Log(siteGroupMember, $"Module Content Updated: {secondaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + secondaryPage.Path)}"); + } + else // change detection + { + log += Log(siteGroupMember, $"Module Content Updated: {primaryPageModule.Title} - {CreateLink(siteGroupMember.AliasName + "/" + primaryPage.Path)}"); + } + } + } + catch + { + // error exporting/importing + } + } + } + + return log; + } + + private List SynchronizePermissions(List permissionList, int siteId) + { + return permissionList.Select(item => new Permission + { + SiteId = siteId, + PermissionName = item.PermissionName, + RoleName = item.RoleName, + UserId = item.UserId, + IsAuthorized = item.IsAuthorized, + CreatedBy = item.CreatedBy, + CreatedOn = item.CreatedOn, + ModifiedBy = item.ModifiedBy, + ModifiedOn = item.ModifiedOn + }).ToList(); + } + + private string SynchronizeSettings(ISettingRepository settingRepository, SiteGroupMember siteGroupMember, string entityName, int primaryEntityId, int secondaryEntityId) + { + var log = ""; + var updated = false; + + var secondarySettings = settingRepository.GetSettings(entityName, secondaryEntityId).ToList(); + foreach (var primarySetting in settingRepository.GetSettings(entityName, primaryEntityId)) + { + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + var secondarySetting = secondarySettings.FirstOrDefault(item => item.SettingName == primarySetting.SettingName); + if (secondarySetting == null) + { + secondarySetting = new Setting(); + secondarySetting.EntityName = primarySetting.EntityName; + secondarySetting.EntityId = secondaryEntityId; + secondarySetting.SettingName = primarySetting.SettingName; + secondarySetting.SettingValue = primarySetting.SettingValue; + secondarySetting.IsPrivate = primarySetting.IsPrivate; + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.AddSetting(secondarySetting); + updated = true; + } + } + else + { + if (secondarySetting.SettingValue != primarySetting.SettingValue || secondarySetting.IsPrivate != primarySetting.IsPrivate) + { + secondarySetting.SettingValue = primarySetting.SettingValue; + secondarySetting.IsPrivate = primarySetting.IsPrivate; + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.UpdateSetting(secondarySetting); + updated = true; + } + } + secondarySettings.Remove(secondarySetting); + } + } + else // change detection + { + if (primarySetting.ModifiedOn > siteGroupMember.SynchronizedOn && !excludedSettings.Any(item => item.EntityName == primarySetting.EntityName && item.SettingName == primarySetting.SettingName)) + { + updated = true; + } + } + } + + if (siteGroupMember.SiteGroup.Type == SiteGroupTypes.Synchronization) + { + // any remaining secondary settings need to be deleted + foreach (var secondarySetting in secondarySettings) + { + if (!excludedSettings.Any(item => item.EntityName == secondarySetting.EntityName && item.SettingName == secondarySetting.SettingName)) + { + settingRepository.DeleteSetting(secondarySetting.EntityName, secondarySetting.SettingId); + updated = true; + } + } + } + + if (updated) + { + log += Log(siteGroupMember, $"{entityName} Settings Updated"); + } + + return log; + } + + private string SendNotifications(IServiceProvider provider, SiteGroupMember siteGroupMember, int siteId, string siteName, string changeLog) + { + var userRoleRepository = provider.GetRequiredService(); + var notificationRepository = provider.GetRequiredService(); + var log = ""; + + // get administrators for site + var userRoles = userRoleRepository.GetUserRoles(RoleNames.Admin, siteId); + if (userRoles != null && userRoles.Any()) + { + foreach (var userRole in userRoles) + { + var notification = new Notification(siteId, userRole.User, $"{siteName} Change Log", changeLog); + notificationRepository.AddNotification(notification); + } + log += Log(siteGroupMember, $"Change Log Sent To Administrators For Secondary Site: {siteName}"); + } + else + { + log += Log(siteGroupMember, $"Error Sending Change Log - Secondary Site {siteName} Does Not Have Any Administrators Defined"); + } + + return log; + } + + private string Log(SiteGroupMember siteGroupMember, string content) + { + // not necessary to log initial synchronization + if (siteGroupMember.SynchronizedOn != DateTime.MinValue) + { + return content + "
"; + } + else + { + return ""; + } + } + + private string CreateLink(string url) + { + return "" + url + ""; + } + } +} diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 3dfc0b54..58c48bd5 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -4,7 +4,6 @@ using System.IO; using System.Linq; using System.Reflection; using Microsoft.Extensions.Options; -using Oqtane.Models; using Oqtane.Shared; namespace Oqtane.Infrastructure @@ -14,6 +13,7 @@ namespace Oqtane.Infrastructure string GetDefaultCulture(); string[] GetSupportedCultures(); string[] GetInstalledCultures(); + string[] GetNeutralCultures(); } public class LocalizationManager : ILocalizationManager @@ -41,7 +41,8 @@ namespace Oqtane.Infrastructure public string[] GetSupportedCultures() { - return CultureInfo.GetCultures(CultureTypes.AllCultures).Select(item => item.Name).OrderBy(c => c).ToArray(); + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Select(item => item.Name).OrderBy(c => c).ToArray(); } public string[] GetInstalledCultures() @@ -49,7 +50,14 @@ namespace Oqtane.Infrastructure return GetSatelliteAssemblyCultures(); } - // method is static as it is called during startup + public string[] GetNeutralCultures() + { + return CultureInfo.GetCultures(CultureTypes.AllCultures) + .Where(item => item.IsNeutralCulture) + .Select(item => item.Name).OrderBy(c => c).ToArray(); + } + + // note: method is public and static as it is called during startup public static string[] GetSatelliteAssemblyCultures() { var cultures = new List(); diff --git a/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs new file mode 100644 index 00000000..c9b1efa6 --- /dev/null +++ b/Oqtane.Server/Infrastructure/SiteTasks/GlobalReplaceTask.cs @@ -0,0 +1,164 @@ +using System; +using System.Linq; +using System.Net; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class GlobalReplaceTask : SiteTaskBase + { + public override string ExecuteTask(IServiceProvider provider, Site site, string parameters) + { + string log = ""; + + // get services + var siteRepository = provider.GetRequiredService(); + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + var TenantManager = provider.GetRequiredService(); + var syncManager = provider.GetRequiredService(); + + if (!string.IsNullOrEmpty(parameters)) + { + // get parameters + var globalReplace = JsonSerializer.Deserialize(parameters); + + var find = globalReplace.Find; + var replace = globalReplace.Replace; + var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + var refresh = false; + + log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; + + // site properties + site = siteRepository.GetSite(site.SiteId); + var changed = false; + if (site.Name != null && site.Name.Contains(find, comparisonType)) + { + site.Name = site.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (site.HeadContent != null && site.HeadContent.Contains(find, comparisonType)) + { + site.HeadContent = site.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (site.BodyContent != null && site.BodyContent.Contains(find, comparisonType)) + { + site.BodyContent = site.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Site) + { + siteRepository.UpdateSite(site); + log += $"Site Updated
"; + refresh = true; + } + + var pages = pageRepository.GetPages(site.SiteId).ToList(); + var pageModules = pageModuleRepository.GetPageModules(site.SiteId).ToList(); + + // iterate pages + foreach (var page in pages) + { + // page properties + changed = false; + if (page.Name != null && page.Name.Contains(find, comparisonType)) + { + page.Name = page.Name.Replace(find, replace, comparisonType); + changed = true; + } + if (page.Title != null && page.Title.Contains(find, comparisonType)) + { + page.Title = page.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (page.HeadContent != null && page.HeadContent.Contains(find, comparisonType)) + { + page.HeadContent = page.HeadContent.Replace(find, replace, comparisonType); + changed = true; + } + if (page.BodyContent != null && page.BodyContent.Contains(find, comparisonType)) + { + page.BodyContent = page.BodyContent.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Pages) + { + pageRepository.UpdatePage(page); + log += $"Page Updated: /{page.Path}
"; + refresh = true; + } + + foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) + { + // module properties + changed = false; + if (pageModule.Title != null && pageModule.Title.Contains(find, comparisonType)) + { + pageModule.Title = pageModule.Title.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Header != null && pageModule.Header.Contains(find, comparisonType)) + { + pageModule.Header = pageModule.Header.Replace(find, replace, comparisonType); + changed = true; + } + if (pageModule.Footer != null && pageModule.Footer.Contains(find, comparisonType)) + { + pageModule.Footer = pageModule.Footer.Replace(find, replace, comparisonType); + changed = true; + } + if (changed && globalReplace.Modules) + { + pageModuleRepository.UpdatePageModule(pageModule); + log += $"Module Updated: {pageModule.Title} Page: /{page.Path}
"; + refresh = true; + } + + // module content + if (pageModule.Module.ModuleDefinition != null && pageModule.Module.ModuleDefinition.ServerManagerType != "") + { + Type moduleType = Type.GetType(pageModule.Module.ModuleDefinition.ServerManagerType); + if (moduleType != null && moduleType.GetInterface(nameof(IPortable)) != null) + { + try + { + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); + if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(WebUtility.HtmlEncode(find), comparisonType) && globalReplace.Content) + { + moduleContent = moduleContent.Replace(WebUtility.HtmlEncode(find), WebUtility.HtmlEncode(replace), comparisonType); + ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); + log += $"Module Content Updated: {pageModule.Title} Page: /{page.Path}
"; + } + } + catch (Exception ex) + { + log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}
"; + } + } + } + } + } + + if (refresh) + { + // clear cache + syncManager.AddSyncEvent(TenantManager.GetAlias(), EntityNames.Site, site.SiteId, SyncEventActions.Refresh); + } + } + else + { + log += $"No Criteria Provided
"; + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs b/Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs new file mode 100644 index 00000000..8da990c9 --- /dev/null +++ b/Oqtane.Server/Infrastructure/SiteTasks/ImportUsersTask.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Managers; +using Oqtane.Models; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class ImportUsersTask : SiteTaskBase + { + public override async Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters) + { + string log = ""; + + if (!string.IsNullOrEmpty(parameters) && parameters.Contains(":")) + { + var fileId = int.Parse(parameters.Split(':')[0]); + var notify = bool.Parse(parameters.Split(':')[1]); + + var fileRepository = provider.GetRequiredService(); + var userManager = provider.GetRequiredService(); + + var file = fileRepository.GetFile(fileId); + if (file != null) + { + var filePath = fileRepository.GetFilePath(file); + log += $"Importing Users From {filePath}
"; + var result = await userManager.ImportUsers(site.SiteId, filePath, notify); + if (result["Success"] == "True") + { + log += $"{result["Users"]} Users Imported
"; + } + else + { + log += $"User Import Failed
"; + } + } + else + { + log += $"Import Users FileId {fileId} Does Not Exist
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs b/Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs new file mode 100644 index 00000000..81cdc80d --- /dev/null +++ b/Oqtane.Server/Infrastructure/SiteTasks/SiteTaskBase.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public class SiteTaskBase : ISiteTask + { + public virtual string ExecuteTask(IServiceProvider provider, Site site, string parameters) + { + return ""; + } + + public virtual Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters) + { + return Task.FromResult(string.Empty); + } + } +} diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index 7229c86d..ab26f5ed 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -607,6 +607,34 @@ namespace Oqtane.Infrastructure.SiteTemplates } } }); + pageTemplates.Add(new PageTemplate + { + Name = "Global Replace", + Parent = "Admin", + Order = 23, + Path = "admin/replace", + Icon = Icons.LoopSquare, + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + Content = "" + } + } + }); // host pages (order starts at 51) pageTemplates.Add(new PageTemplate diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 65bf8c74..a1e1691a 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -49,7 +49,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Welcome To Oqtane...", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Welcome To Oqtane...", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -60,7 +63,10 @@ namespace Oqtane.Infrastructure.SiteTemplates "

Blazor is a modern front-end web framework based on HTML, CSS, and C# that helps you build web applications faster. Blazor provides a component-based architecture with server-side rendering and full client-side interactivity in a single solution, where you can switch between server-side and client-side rendering modes and even mix them in the same web page. For desktop or mobile scenarios, Blazor Hybrid is part of .NET MAUI and uses a Web View to render components natively on all modern devices.

" + "

Blazor is a feature of ASP.NET, the popular cross platform development framework from Microsoft that provides powerful tools and libraries for building modern software applications.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "MIT License", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "MIT License", + Pane = PaneNames.Default, + Order = 3, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -71,7 +77,10 @@ namespace Oqtane.Infrastructure.SiteTemplates "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" }, - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Secure Content", + Pane = PaneNames.Default, + Order = 5, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -96,7 +105,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "Secure Content", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "Secure Content", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Registered, true), new Permission(PermissionNames.View, RoleNames.Admin, true), @@ -121,7 +133,10 @@ namespace Oqtane.Infrastructure.SiteTemplates new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, PageTemplateModules = new List { - new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", Title = "My Page", Pane = PaneNames.Default, + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.HtmlText, Oqtane.Client", + Title = "My Page", + Pane = PaneNames.Default, + Order = 1, PermissionList = new List { new Permission(PermissionNames.View, RoleNames.Everyone, true), new Permission(PermissionNames.View, RoleNames.Admin, true), diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 64e02f03..6b4d15fa 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -93,6 +93,9 @@ namespace Oqtane.Infrastructure case "10.0.4": Upgrade_10_0_4(tenant, scope); break; + case "10.1.0": + Upgrade_10_1_0(tenant, scope); + break; } } } @@ -602,6 +605,45 @@ namespace Oqtane.Infrastructure RemoveAssemblies(tenant, assemblies, "10.0.4"); } + private void Upgrade_10_1_0(Tenant tenant, IServiceScope scope) + { + var pageTemplates = new List + { + new PageTemplate + { + Update = false, + Name = "Global Replace", + Parent = "Admin", + Order = 23, + Path = "admin/replace", + Icon = Icons.LoopSquare, + IsNavigation = false, + IsPersonalizable = false, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + PageTemplateModules = new List + { + new PageTemplateModule + { + ModuleDefinitionName = typeof(Oqtane.Modules.Admin.GlobalReplace.Index).ToModuleDefinitionName(), Title = "Global Replace", Pane = PaneNames.Default, + PermissionList = new List + { + new Permission(PermissionNames.View, RoleNames.Admin, true), + new Permission(PermissionNames.Edit, RoleNames.Admin, true) + }, + Content = "" + } + } + } + }; + + AddPagesToSites(scope, tenant, pageTemplates); + } + + private void AddPagesToSites(IServiceScope scope, Tenant tenant, List pageTemplates) { var tenants = scope.ServiceProvider.GetRequiredService(); diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs new file mode 100644 index 00000000..3af17760 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupEntityBuilder.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteGroupEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteGroup"; + private readonly PrimaryKey _primaryKey = new("PK_SiteGroup", x => x.SiteGroupId); + + public SiteGroupEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + } + + protected override SiteGroupEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteGroupId = AddAutoIncrementColumn(table, "SiteGroupId"); + Name = AddStringColumn(table, "Name", 200); + Type = AddStringColumn(table, "Type", 50); + PrimarySiteId = AddIntegerColumn(table, "PrimarySiteId"); + Synchronize = AddBooleanColumn(table, "Synchronize"); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteGroupId { get; set; } + + public OperationBuilder Name { get; set; } + + public OperationBuilder Type { get; set; } + + public OperationBuilder PrimarySiteId { get; set; } + + public OperationBuilder Synchronize { get; set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs new file mode 100644 index 00000000..d5ff717b --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteGroupMemberEntityBuilder.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteGroupMemberEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteGroupMember"; + private readonly PrimaryKey _primaryKey = new("PK_SiteGroupMember", x => x.SiteGroupMemberId); + private readonly ForeignKey _groupForeignKey = new("FK_SiteGroupMember_SiteGroup", x => x.SiteGroupId, "SiteGroup", "SiteGroupId", ReferentialAction.Cascade); + private readonly ForeignKey _siteForeignKey = new("FK_SiteGroupMember_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public SiteGroupMemberEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_groupForeignKey); + ForeignKeys.Add(_siteForeignKey); + } + + protected override SiteGroupMemberEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteGroupMemberId = AddAutoIncrementColumn(table, "SiteGroupMemberId"); + SiteGroupId = AddIntegerColumn(table, "SiteGroupId"); + SiteId = AddIntegerColumn(table, "SiteId"); + SynchronizedOn = AddDateTimeColumn(table, "SynchronizedOn", true); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteGroupMemberId { get; set; } + + public OperationBuilder SiteGroupId { get; set; } + + public OperationBuilder SiteId { get; set; } + + public OperationBuilder SynchronizedOn { get; set; } + } +} diff --git a/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs new file mode 100644 index 00000000..b0b9df1c --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/SiteTaskEntityBuilder.cs @@ -0,0 +1,53 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class SiteTaskEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "SiteTask"; + private readonly PrimaryKey _primaryKey = new("PK_SiteTask", x => x.SiteTaskId); + private readonly ForeignKey _siteForeignKey = new("FK_SiteTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public SiteTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_siteForeignKey); + } + + protected override SiteTaskEntityBuilder BuildTable(ColumnsBuilder table) + { + SiteTaskId = AddAutoIncrementColumn(table,"SiteTaskId"); + SiteId = AddIntegerColumn(table,"SiteId"); + Name = AddStringColumn(table, "Name", 200); + Type = AddStringColumn(table, "Type", 200); + Parameters = AddMaxStringColumn(table, "Parameters", true); + IsCompleted = AddBooleanColumn(table, "IsCompleted", true); + Status = AddMaxStringColumn(table, "Status", true); + + AddAuditableColumns(table); + + return this; + } + + public OperationBuilder SiteTaskId { get; private set; } + + public OperationBuilder SiteId { get; private set; } + + public OperationBuilder Name { get; private set; } + + public OperationBuilder Type { get; private set; } + + public OperationBuilder Parameters { get; private set; } + + public OperationBuilder IsCompleted { get; private set; } + + public OperationBuilder Status { get; private set; } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs new file mode 100644 index 00000000..f504e86d --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010001_AddSiteGroups.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.01")] + public class AddSiteGroups : MultiDatabaseMigration + { + public AddSiteGroups(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteGroupEntityBuilder = new SiteGroupEntityBuilder(migrationBuilder, ActiveDatabase); + siteGroupEntityBuilder.Create(); + + var siteGroupMemberEntityBuilder = new SiteGroupMemberEntityBuilder(migrationBuilder, ActiveDatabase); + siteGroupMemberEntityBuilder.Create(); + siteGroupMemberEntityBuilder.AddIndex("IX_SiteGroupMember", new[] { "SiteId", "SiteGroupId" }, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs new file mode 100644 index 00000000..81bb9cbf --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010002_AddCultureCode.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.02")] + public class AddCultureCode : MultiDatabaseMigration + { + public AddCultureCode(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddStringColumn("CultureCode", 10, true); + siteEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'"); + + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.AddStringColumn("CultureCode", 10, true); + userEntityBuilder.UpdateData("CultureCode", $"'{Shared.Constants.DefaultCulture}'"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs b/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs new file mode 100644 index 00000000..bbc79ac5 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010003_RemoveHomePageId.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.03")] + public class RemoveHomePageId : MultiDatabaseMigration + { + public RemoveHomePageId(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.DropColumn("HomePageId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs b/Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs new file mode 100644 index 00000000..ece41554 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010004_AddSiteTasks.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Oqtane.Databases.Interfaces; +using Oqtane.Migrations.EntityBuilders; +using Oqtane.Repository; + +namespace Oqtane.Migrations.Tenant +{ + [DbContext(typeof(TenantDBContext))] + [Migration("Tenant.10.01.00.04")] + public class AddSiteTasks : MultiDatabaseMigration + { + public AddSiteTasks(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteTaskEntityBuilder = new SiteTaskEntityBuilder(migrationBuilder, ActiveDatabase); + siteTaskEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs index fb8ef512..c5b6d851 100644 --- a/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs +++ b/Oqtane.Server/Modules/HtmlText/Controllers/HtmlTextController.cs @@ -58,23 +58,6 @@ namespace Oqtane.Modules.HtmlText.Controllers } } - // GET api//5/6 - [HttpGet("{id}/{moduleId}")] - [Authorize(Policy = PolicyNames.ViewModule)] - public async Task Get(int id, int moduleId) - { - if (IsAuthorizedEntityId(EntityNames.Module, moduleId)) - { - return await _htmlTextService.GetHtmlTextAsync(id, moduleId); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {HtmlTextId}", id); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - return null; - } - } - // POST api/ [HttpPost] [Authorize(Policy = PolicyNames.EditModule)] diff --git a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs index 67f05805..14fa9057 100644 --- a/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs +++ b/Oqtane.Server/Modules/HtmlText/Manager/HtmlTextManager.cs @@ -1,92 +1,41 @@ using System; using System.Collections.Generic; -using System.Linq; -using Oqtane.Infrastructure; -using Oqtane.Models; -using Oqtane.Modules.HtmlText.Repository; using System.Net; -using Oqtane.Enums; -using Oqtane.Repository; -using Oqtane.Shared; -using Oqtane.Migrations.Framework; -using Oqtane.Documentation; -using Oqtane.Interfaces; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; +using Oqtane.Documentation; +using Oqtane.Enums; +using Oqtane.Infrastructure; +using Oqtane.Interfaces; +using Oqtane.Migrations.Framework; +using Oqtane.Models; +using Oqtane.Modules.HtmlText.Repository; +using Oqtane.Repository; +using Oqtane.Shared; // ReSharper disable ConvertToUsingDeclaration namespace Oqtane.Modules.HtmlText.Manager { [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISearchable + public class HtmlTextManager : MigratableModuleBase, IInstallable, IPortable, ISynchronizable, ISearchable { - private readonly IHtmlTextRepository _htmlText; + private readonly IHtmlTextRepository _htmlTextRepository; private readonly IDBContextDependencies _DBContextDependencies; private readonly ISqlRepository _sqlRepository; private readonly ITenantManager _tenantManager; private readonly IMemoryCache _cache; - public HtmlTextManager( - IHtmlTextRepository htmlText, - IDBContextDependencies DBContextDependencies, - ISqlRepository sqlRepository, - ITenantManager tenantManager, - IMemoryCache cache) + public HtmlTextManager(IHtmlTextRepository htmlTextRepository, IDBContextDependencies DBContextDependencies, ISqlRepository sqlRepository, ITenantManager tenantManager, IMemoryCache cache) { - _htmlText = htmlText; + _htmlTextRepository = htmlTextRepository; _DBContextDependencies = DBContextDependencies; _sqlRepository = sqlRepository; _tenantManager = tenantManager; _cache = cache; } - public string ExportModule(Module module) - { - string content = ""; - var htmltexts = _htmlText.GetHtmlTexts(module.ModuleId); - if (htmltexts != null && htmltexts.Any()) - { - var htmltext = htmltexts.OrderByDescending(item => item.CreatedOn).First(); - content = WebUtility.HtmlEncode(htmltext.Content); - } - return content; - } - - public Task> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn) - { - var searchContents = new List(); - - var htmltext = _htmlText.GetHtmlTexts(pageModule.ModuleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault(); - if (htmltext != null && htmltext.CreatedOn >= lastIndexedOn) - { - searchContents.Add(new SearchContent - { - Body = htmltext.Content, - ContentModifiedBy = htmltext.CreatedBy, - ContentModifiedOn = htmltext.CreatedOn - }); - } - - return Task.FromResult(searchContents); - } - - public void ImportModule(Module module, string content, string version) - { - content = WebUtility.HtmlDecode(content); - var htmlText = new Models.HtmlText(); - htmlText.ModuleId = module.ModuleId; - htmlText.Content = content; - _htmlText.AddHtmlText(htmlText); - - //clear the cache for the module - var alias = _tenantManager.GetAlias(); - if(alias != null) - { - _cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}"); - } - } - + // IInstallable implementation public bool Install(Tenant tenant, string version) { if (tenant.DBType == Constants.DefaultDBType && version == "1.0.1") @@ -101,5 +50,74 @@ namespace Oqtane.Modules.HtmlText.Manager { return Migrate(new HtmlTextContext(_DBContextDependencies), tenant, MigrationType.Down); } + + // IPortable implementation + public string ExportModule(Module module) + { + string content = ""; + var htmltext = _htmlTextRepository.GetHtmlText(module.ModuleId); + if (htmltext != null) + { + content = WebUtility.HtmlEncode(htmltext.Content); + } + return content; + } + + public void ImportModule(Module module, string content, string version) + { + SaveModuleContent(module, content); + } + + // ISynchronizable implementation + public string ExtractModule(Module module, DateTime lastSynchronizedOn) + { + string content = ""; + var htmltext = _htmlTextRepository.GetHtmlText(module.ModuleId); + if (htmltext != null && htmltext.CreatedOn > lastSynchronizedOn) + { + content = WebUtility.HtmlEncode(htmltext.Content); + } + return content; + } + + public void LoadModule(Module module, string content) + { + SaveModuleContent(module, content); + } + + private void SaveModuleContent(Module module, string content) + { + content = WebUtility.HtmlDecode(content); + var htmlText = new Models.HtmlText(); + htmlText.ModuleId = module.ModuleId; + htmlText.Content = content; + _htmlTextRepository.AddHtmlText(htmlText); + + //clear the cache for the module + var alias = _tenantManager.GetAlias(); + if (alias != null) + { + _cache.Remove($"HtmlText:{alias.SiteKey}:{module.ModuleId}"); + } + } + + // ISearchable implementation + public Task> GetSearchContentsAsync(PageModule pageModule, DateTime lastIndexedOn) + { + var searchContents = new List(); + + var htmltext = _htmlTextRepository.GetHtmlText(pageModule.ModuleId); + if (htmltext != null && htmltext.CreatedOn >= lastIndexedOn) + { + searchContents.Add(new SearchContent + { + Body = htmltext.Content, + ContentModifiedBy = htmltext.CreatedBy, + ContentModifiedOn = htmltext.CreatedOn + }); + } + + return Task.FromResult(searchContents); + } } } diff --git a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs index cfeb8eb3..490242a5 100644 --- a/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs +++ b/Oqtane.Server/Modules/HtmlText/Repository/HtmlTextRepository.cs @@ -1,18 +1,31 @@ -using System.Linq; -using Oqtane.Documentation; using System.Collections.Generic; +using System.Linq; using Microsoft.EntityFrameworkCore; +using Oqtane.Documentation; +using Oqtane.Repository; +using Oqtane.Shared; namespace Oqtane.Modules.HtmlText.Repository { + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] + public interface IHtmlTextRepository + { + IEnumerable GetHtmlTexts(int moduleId); + Models.HtmlText GetHtmlText(int moduleId); + Models.HtmlText AddHtmlText(Models.HtmlText htmlText); + void DeleteHtmlText(int htmlTextId); + } + [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class HtmlTextRepository : IHtmlTextRepository, ITransientService { private readonly IDbContextFactory _factory; + private readonly ISettingRepository _settingRepository; - public HtmlTextRepository(IDbContextFactory factory) + public HtmlTextRepository(IDbContextFactory factory, ISettingRepository settingRepository) { _factory = factory; + _settingRepository = settingRepository; } public IEnumerable GetHtmlTexts(int moduleId) @@ -21,15 +34,27 @@ namespace Oqtane.Modules.HtmlText.Repository return db.HtmlText.Where(item => item.ModuleId == moduleId).ToList(); } - public Models.HtmlText GetHtmlText(int htmlTextId) + public Models.HtmlText GetHtmlText(int moduleId) { using var db = _factory.CreateDbContext(); - return db.HtmlText.Find(htmlTextId); + return db.HtmlText.Where(item => item.ModuleId == moduleId)? + .OrderByDescending(item => item.CreatedOn).FirstOrDefault(); } public Models.HtmlText AddHtmlText(Models.HtmlText htmlText) { using var db = _factory.CreateDbContext(); + + var versions = int.Parse(_settingRepository.GetSettingValue(EntityNames.Module, htmlText.ModuleId, "Versions", "5")); + if (versions > 0) + { + var htmlTexts = db.HtmlText.Where(item => item.ModuleId == htmlText.ModuleId).OrderByDescending(item => item.CreatedOn).ToList(); + for (int i = versions - 1; i < htmlTexts.Count; i++) + { + db.HtmlText.Remove(htmlTexts[i]); + } + } + db.HtmlText.Add(htmlText); db.SaveChanges(); return htmlText; @@ -39,8 +64,11 @@ namespace Oqtane.Modules.HtmlText.Repository { using var db = _factory.CreateDbContext(); Models.HtmlText htmlText = db.HtmlText.FirstOrDefault(item => item.HtmlTextId == htmlTextId); - if (htmlText != null) db.HtmlText.Remove(htmlText); - db.SaveChanges(); + if (htmlText != null) + { + db.HtmlText.Remove(htmlText); + db.SaveChanges(); + } } } } diff --git a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs b/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs deleted file mode 100644 index d586f0ef..00000000 --- a/Oqtane.Server/Modules/HtmlText/Repository/IHtmlTextRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Documentation; - -namespace Oqtane.Modules.HtmlText.Repository -{ - [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] - public interface IHtmlTextRepository - { - IEnumerable GetHtmlTexts(int moduleId); - Models.HtmlText GetHtmlText(int htmlTextId); - Models.HtmlText AddHtmlText(Models.HtmlText htmlText); - void DeleteHtmlText(int htmlTextId); - } -} diff --git a/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs index 49055c4e..8e9150b3 100644 --- a/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs +++ b/Oqtane.Server/Modules/HtmlText/Services/HtmlTextService.cs @@ -17,16 +17,16 @@ namespace Oqtane.Modules.HtmlText.Services [PrivateApi("Mark HtmlText classes as private, since it's not very useful in the public docs")] public class ServerHtmlTextService : IHtmlTextService, ITransientService { - private readonly IHtmlTextRepository _htmlText; + private readonly IHtmlTextRepository _htmlTextRepository; private readonly IUserPermissions _userPermissions; private readonly IMemoryCache _cache; private readonly ILogManager _logger; private readonly IHttpContextAccessor _accessor; private readonly Alias _alias; - public ServerHtmlTextService(IHtmlTextRepository htmlText, IUserPermissions userPermissions, IMemoryCache cache, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) + public ServerHtmlTextService(IHtmlTextRepository htmlTextRepository, IUserPermissions userPermissions, IMemoryCache cache, ITenantManager tenantManager, ILogManager logger, IHttpContextAccessor accessor) { - _htmlText = htmlText; + _htmlTextRepository = htmlTextRepository; _userPermissions = userPermissions; _cache = cache; _logger = logger; @@ -38,7 +38,7 @@ namespace Oqtane.Modules.HtmlText.Services { if (_accessor.HttpContext.User.IsInRole(RoleNames.Registered)) { - return Task.FromResult(GetCachedHtmlTexts(moduleId)); + return Task.FromResult(_htmlTextRepository.GetHtmlTexts(moduleId).ToList()); } else { @@ -51,7 +51,11 @@ namespace Oqtane.Modules.HtmlText.Services { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) { - return Task.FromResult(GetCachedHtmlTexts(moduleId)?.OrderByDescending(item => item.CreatedOn).FirstOrDefault()); + return Task.FromResult(_cache.GetOrCreate($"HtmlText:{_alias.SiteKey}:{moduleId}", entry => + { + entry.SlidingExpiration = TimeSpan.FromMinutes(30); + return _htmlTextRepository.GetHtmlText(moduleId); + })); } else { @@ -60,24 +64,11 @@ namespace Oqtane.Modules.HtmlText.Services } } - public Task GetHtmlTextAsync(int htmlTextId, int moduleId) - { - if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.View)) - { - return Task.FromResult(GetCachedHtmlTexts(moduleId)?.FirstOrDefault(item => item.HtmlTextId == htmlTextId)); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Html/Text Get Attempt {HtmlTextId} {ModuleId}", htmlTextId, moduleId); - return null; - } - } - public Task AddHtmlTextAsync(Models.HtmlText htmlText) { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, htmlText.ModuleId, PermissionNames.Edit)) { - htmlText = _htmlText.AddHtmlText(htmlText); + htmlText = _htmlTextRepository.AddHtmlText(htmlText); ClearCache(htmlText.ModuleId); _logger.Log(LogLevel.Information, this, LogFunction.Create, "Html/Text Added {HtmlText}", htmlText); } @@ -93,7 +84,7 @@ namespace Oqtane.Modules.HtmlText.Services { if (_userPermissions.IsAuthorized(_accessor.HttpContext.User, _alias.SiteId, EntityNames.Module, moduleId, PermissionNames.Edit)) { - _htmlText.DeleteHtmlText(htmlTextId); + _htmlTextRepository.DeleteHtmlText(htmlTextId); ClearCache(moduleId); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Html/Text Deleted {HtmlTextId}", htmlTextId); } @@ -104,15 +95,6 @@ namespace Oqtane.Modules.HtmlText.Services return Task.CompletedTask; } - private List GetCachedHtmlTexts(int moduleId) - { - return _cache.GetOrCreate($"HtmlText:{_alias.SiteKey}:{moduleId}", entry => - { - entry.SlidingExpiration = TimeSpan.FromMinutes(30); - return _htmlText.GetHtmlTexts(moduleId).ToList(); - }); - } - private void ClearCache(int moduleId) { _cache.Remove($"HtmlText:{_alias.SiteKey}:{moduleId}"); diff --git a/Oqtane.Server/Modules/ISynchronizable.cs b/Oqtane.Server/Modules/ISynchronizable.cs new file mode 100644 index 00000000..456d992f --- /dev/null +++ b/Oqtane.Server/Modules/ISynchronizable.cs @@ -0,0 +1,14 @@ +using System; +using Oqtane.Models; + +namespace Oqtane.Modules +{ + public interface ISynchronizable + { + // You Must Set The "ServerManagerType" In Your IModule Interface + + string ExtractModule(Module module, DateTime lastSynchronizedOn); + + void LoadModule(Module module, string content); + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index 4ba5c66c..b5ba8453 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -20,6 +20,7 @@ +
@@ -27,28 +28,28 @@ - - - - + + + + - + - - + + - + - - + + - + @@ -78,4 +79,8 @@ + + + + diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index 47932d7d..8a5a7751 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -134,5 +134,8 @@ namespace Oqtane.Repository public virtual DbSet SearchContentWord { get; set; } public virtual DbSet SearchWord { get; set; } public virtual DbSet MigrationHistory { get; set; } + public virtual DbSet SiteGroup { get; set; } + public virtual DbSet SiteGroupMember { get; set; } + public virtual DbSet SiteTask { get; set; } } } diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs index 3cd3e2bf..873626d2 100644 --- a/Oqtane.Server/Repository/FileRepository.cs +++ b/Oqtane.Server/Repository/FileRepository.cs @@ -121,7 +121,7 @@ namespace Oqtane.Repository var file = db.File.AsNoTracking() .Include(item => item.Folder) .FirstOrDefault(item => item.FolderId == folderId && - item.Name.ToLower() == fileName); + item.Name.ToLower() == fileName.ToLower()); if (file != null) { diff --git a/Oqtane.Server/Repository/FolderRepository.cs b/Oqtane.Server/Repository/FolderRepository.cs index fb68033f..d7f1c43e 100644 --- a/Oqtane.Server/Repository/FolderRepository.cs +++ b/Oqtane.Server/Repository/FolderRepository.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Hosting; @@ -45,7 +46,49 @@ namespace Oqtane.Repository { folder.PermissionList = permissions.Where(item => item.EntityId == folder.FolderId).ToList(); } - return folders; + return GetFoldersHierarchy(folders); + } + + private static List GetFoldersHierarchy(List folders) + { + List hierarchy = new List(); + Action, Folder> getPath = null; + getPath = (folderList, folder) => + { + IEnumerable children; + int level; + if (folder == null) + { + level = -1; + children = folders.Where(item => item.ParentId == null); + } + else + { + level = folder.Level; + children = folders.Where(item => item.ParentId == folder.FolderId); + } + + foreach (Folder child in children) + { + child.Level = level + 1; + child.HasChildren = folders.Any(item => item.ParentId == child.FolderId); + hierarchy.Add(child); + getPath(folderList, child); + } + }; + folders = folders.OrderBy(item => item.Name).ToList(); + getPath(folders, null); + + // add any non-hierarchical items to the end of the list + foreach (Folder folder in folders) + { + if (hierarchy.Find(item => item.FolderId == folder.FolderId) == null) + { + hierarchy.Add(folder); + } + } + + return hierarchy; } public Folder AddFolder(Folder folder) diff --git a/Oqtane.Server/Repository/JobLogRepository.cs b/Oqtane.Server/Repository/JobLogRepository.cs index 523ac356..a3994e11 100644 --- a/Oqtane.Server/Repository/JobLogRepository.cs +++ b/Oqtane.Server/Repository/JobLogRepository.cs @@ -61,8 +61,8 @@ namespace Oqtane.Repository public void DeleteJobLog(int jobLogId) { - JobLog joblog = _db.JobLog.Find(jobLogId); - _db.JobLog.Remove(joblog); + JobLog jobLog = _db.JobLog.Find(jobLogId); + _db.JobLog.Remove(jobLog); _db.SaveChanges(); } } diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index d8bb7f4e..fd1e11e7 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; @@ -111,7 +113,7 @@ namespace Oqtane.Repository ModuleDefinition.Resources = moduleDefinition.Resources; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.PackageName = moduleDefinition.PackageName; - ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm")); + ModuleDefinition.Fingerprint = moduleDefinition.Fingerprint; } return ModuleDefinition; @@ -184,6 +186,7 @@ namespace Oqtane.Repository ModuleDefinition.CreatedOn = moduledefinition.CreatedOn; ModuleDefinition.ModifiedBy = moduledefinition.ModifiedBy; ModuleDefinition.ModifiedOn = moduledefinition.ModifiedOn; + ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduledefinition.ModifiedOn.ToString("yyyyMMddHHmm")); } // any remaining module definitions are orphans @@ -351,7 +354,7 @@ namespace Oqtane.Repository moduledefinition = new ModuleDefinition { Name = Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), - Description = "Manage " + Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), + Description = Utilities.GetTypeNameLastSegment(modulecontroltype.Namespace, 0), Categories = ((qualifiedModuleType.StartsWith("Oqtane.Modules.Admin.")) ? "Admin" : "") }; } @@ -364,7 +367,7 @@ namespace Oqtane.Repository { foreach (var resource in moduledefinition.Resources) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/Modules/" + Utilities.GetTypeName(moduledefinition.ModuleDefinitionName) + "/").Replace("//", "/"); } @@ -420,6 +423,7 @@ namespace Oqtane.Repository } moduledefinition = moduledefinitions[index]; + // actions var modulecontrolobject = Activator.CreateInstance(modulecontroltype) as IModuleControl; string actions = modulecontrolobject.Actions; @@ -430,6 +434,92 @@ namespace Oqtane.Repository moduledefinition.ControlTypeRoutes += (action + "=" + modulecontroltype.FullName + ", " + modulecontroltype.Assembly.GetName().Name + ";"); } } + // module title + if (modulecontroltype.Name == Constants.DefaultAction && !string.IsNullOrEmpty(modulecontrolobject.Title)) + { + moduledefinition.Name = modulecontrolobject.Title; + moduledefinition.Description = "Manage " + moduledefinition.Name; + } + + // check for Page attribute + var routeAttributes = modulecontroltype.GetCustomAttributes(typeof(RouteAttribute), true).Cast(); + if (routeAttributes != null && routeAttributes.Any()) + { + var route = routeAttributes.First().Template; + if (!string.IsNullOrEmpty(route)) + { + // @page "/route" (note that nested routes are not permitted) + var pageTemplate = new PageTemplate(); + pageTemplate.AliasName = "*"; + pageTemplate.Version = "*"; + pageTemplate.Path = route.Substring(1); + pageTemplate.Update = false; + pageTemplate.PageTemplateModules = new List(); + + // check for Authorize attributes + var permissionList = new List(); + var authorizeAttributes = modulecontroltype.GetCustomAttributes(typeof(AuthorizeAttribute), true).Cast(); + if (authorizeAttributes != null && authorizeAttributes.Any()) + { + foreach (var authorizeAttribute in authorizeAttributes) + { + if (string.IsNullOrEmpty(authorizeAttribute.Roles)) + { + // [Authorize] + permissionList.Add(new Permission(PermissionNames.View, RoleNames.Registered, true)); + } + else + { + // [Authorize(Roles = "role1, permission:role2")] + foreach (var role in authorizeAttribute.Roles.Split(',')) + { + var permissionName = PermissionNames.View; + var roleName = role.Trim(); + if (roleName.Contains(":")) + { + permissionName = roleName.Substring(0, roleName.IndexOf(":") - 1); + roleName = roleName.Substring(roleName.IndexOf(":") + 1); + } + permissionList.Add(new Permission(permissionName, roleName, true)); + } + } + } + } + else + { + // view permission + permissionList.Add(new Permission(PermissionNames.View, RoleNames.Everyone, true)); + } + + // assign page permissions + foreach (var permission in permissionList) + { + if (!pageTemplate.PermissionList.Any(item => item.PermissionName == permission.PermissionName && item.RoleName == permission.RoleName)) + { + pageTemplate.PermissionList.Add(permission); + } + } + + // add module instance + var pageTemplateModule = new PageTemplateModule(); + pageTemplateModule.Title = route.Substring(1); + // assign module permissions + foreach (var permission in permissionList) + { + if (!pageTemplateModule.PermissionList.Any(item => item.PermissionName == permission.PermissionName && item.RoleName == permission.RoleName)) + { + pageTemplateModule.PermissionList.Add(permission.Clone()); + } + } + pageTemplate.PageTemplateModules.Add(pageTemplateModule); + + // if PageTemplates was not already defined in IModule + if (moduledefinition.PageTemplates == null) + { + moduledefinition.PageTemplates = new List { pageTemplate }; + } + } + } moduledefinitions[index] = moduledefinition; } diff --git a/Oqtane.Server/Repository/PageModuleRepository.cs b/Oqtane.Server/Repository/PageModuleRepository.cs index 239a9a67..8913f7fc 100644 --- a/Oqtane.Server/Repository/PageModuleRepository.cs +++ b/Oqtane.Server/Repository/PageModuleRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Security.Policy; using Microsoft.EntityFrameworkCore; using Oqtane.Models; using Oqtane.Shared; @@ -98,6 +99,7 @@ namespace Oqtane.Repository var permissions = _permissions.GetPermissions(pagemodule.Module.SiteId, EntityNames.Module).ToList(); pagemodule = GetPageModule(pagemodule, moduledefinitions, permissions); } + pagemodule.Module.IsShared = db.PageModule.Count(item => item.ModuleId == pagemodule.ModuleId) > 1; return pagemodule; } diff --git a/Oqtane.Server/Repository/SiteGroupMemberRepository.cs b/Oqtane.Server/Repository/SiteGroupMemberRepository.cs new file mode 100644 index 00000000..82efb37a --- /dev/null +++ b/Oqtane.Server/Repository/SiteGroupMemberRepository.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteGroupMemberRepository + { + IEnumerable GetSiteGroupMembers(); + IEnumerable GetSiteGroupMembers(int siteId, int siteGroupId); + SiteGroupMember AddSiteGroupMember(SiteGroupMember siteGroupMember); + SiteGroupMember UpdateSiteGroupMember(SiteGroupMember siteGroupMember); + SiteGroupMember GetSiteGroupMember(int siteGroupMemberId); + SiteGroupMember GetSiteGroupMember(int siteGroupMemberId, bool tracking); + void DeleteSiteGroupMember(int siteGroupMemberId); + } + + public class SiteGroupMemberRepository : ISiteGroupMemberRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteGroupMemberRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteGroupMembers() + { + return GetSiteGroupMembers(-1, -1); + } + + public IEnumerable GetSiteGroupMembers(int siteId, int siteGroupId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroupMember + .Where(item => (siteId == -1 || item.SiteId == siteId) && (siteGroupId == -1 || item.SiteGroupId == siteGroupId)) + .Include(item => item.SiteGroup) // eager load + .ToList(); + } + + public SiteGroupMember AddSiteGroupMember(SiteGroupMember siteGroupMember) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteGroupMember.Add(siteGroupMember); + db.SaveChanges(); + return siteGroupMember; + } + + public SiteGroupMember UpdateSiteGroupMember(SiteGroupMember siteGroupMember) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteGroupMember).State = EntityState.Modified; + db.SaveChanges(); + return siteGroupMember; + } + + public SiteGroupMember GetSiteGroupMember(int siteGroupMemberId) + { + return GetSiteGroupMember(siteGroupMemberId, true); + } + + public SiteGroupMember GetSiteGroupMember(int siteGroupMemberId, bool tracking) + { + using var db = _dbContextFactory.CreateDbContext(); + if (tracking) + { + return db.SiteGroupMember + .Include(item => item.SiteGroup) // eager load + .FirstOrDefault(item => item.SiteGroupMemberId == siteGroupMemberId); + } + else + { + return db.SiteGroupMember.AsNoTracking() + .Include(item => item.SiteGroup) // eager load + .FirstOrDefault(item => item.SiteGroupMemberId == siteGroupMemberId); + } + } + + public void DeleteSiteGroupMember(int siteGroupMemberId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteGroupMember SiteGroupMember = db.SiteGroupMember.Find(siteGroupMemberId); + db.SiteGroupMember.Remove(SiteGroupMember); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/SiteGroupRepository.cs b/Oqtane.Server/Repository/SiteGroupRepository.cs new file mode 100644 index 00000000..883974c3 --- /dev/null +++ b/Oqtane.Server/Repository/SiteGroupRepository.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteGroupRepository + { + IEnumerable GetSiteGroups(); + SiteGroup AddSiteGroup(SiteGroup siteGroup); + SiteGroup UpdateSiteGroup(SiteGroup siteGroup); + SiteGroup GetSiteGroup(int siteGroupId); + SiteGroup GetSiteGroup(int siteGroupId, bool tracking); + void DeleteSiteGroup(int siteGroupId); + } + + public class SiteGroupRepository : ISiteGroupRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteGroupRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteGroups() + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteGroup.ToList(); + } + + public SiteGroup AddSiteGroup(SiteGroup siteGroup) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteGroup.Add(siteGroup); + db.SaveChanges(); + return siteGroup; + } + + public SiteGroup UpdateSiteGroup(SiteGroup siteGroup) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteGroup).State = EntityState.Modified; + db.SaveChanges(); + return siteGroup; + } + + public SiteGroup GetSiteGroup(int siteGroupId) + { + return GetSiteGroup(siteGroupId, true); + } + + public SiteGroup GetSiteGroup(int siteGroupId, bool tracking) + { + using var db = _dbContextFactory.CreateDbContext(); + if (tracking) + { + return db.SiteGroup.FirstOrDefault(item => item.SiteGroupId == siteGroupId); + } + else + { + return db.SiteGroup.AsNoTracking().FirstOrDefault(item => item.SiteGroupId == siteGroupId); + } + } + + public void DeleteSiteGroup(int siteGroupId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteGroup group = db.SiteGroup.Find(siteGroupId); + db.SiteGroup.Remove(group); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 7a94737c..4f917a3c 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -359,6 +359,7 @@ namespace Oqtane.Repository } pageTemplate.Path = (parent != null) ? parent.Path + "/" + pageTemplate.Name : pageTemplate.Name; } + // home page path can be specified with "home" or "/" pageTemplate.Path = (pageTemplate.Path.ToLower() == "home") ? "" : pageTemplate.Path; pageTemplate.Path = (pageTemplate.Path == "/") ? "" : pageTemplate.Path; var page = pages.FirstOrDefault(item => item.Path.ToLower() == pageTemplate.Path.ToLower()); diff --git a/Oqtane.Server/Repository/SiteTaskRepository.cs b/Oqtane.Server/Repository/SiteTaskRepository.cs new file mode 100644 index 00000000..87ea15dc --- /dev/null +++ b/Oqtane.Server/Repository/SiteTaskRepository.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface ISiteTaskRepository + { + IEnumerable GetSiteTasks(int siteId); + SiteTask GetSiteTask(int siteTaskId); + SiteTask AddSiteTask(SiteTask siteTask); + SiteTask UpdateSiteTask(SiteTask siteTask); + void DeleteSiteTask(int siteTaskId); + int DeleteSiteTasks(int siteId, int age); + } + + public class SiteTaskRepository : ISiteTaskRepository + { + private readonly IDbContextFactory _dbContextFactory; + + public SiteTaskRepository(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public IEnumerable GetSiteTasks(int siteId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteTask.Where(item => item.SiteId == siteId && !item.IsCompleted) + .OrderBy(item => item.CreatedOn).ToList(); + } + + public SiteTask GetSiteTask(int siteTaskId) + { + using var db = _dbContextFactory.CreateDbContext(); + return db.SiteTask.SingleOrDefault(item => item.SiteTaskId == siteTaskId); + } + + public SiteTask AddSiteTask(SiteTask siteTask) + { + using var db = _dbContextFactory.CreateDbContext(); + db.SiteTask.Add(siteTask); + db.SaveChanges(); + return siteTask; + } + public SiteTask UpdateSiteTask(SiteTask siteTask) + { + using var db = _dbContextFactory.CreateDbContext(); + db.Entry(siteTask).State = EntityState.Modified; + db.SaveChanges(); + return siteTask; + } + + public void DeleteSiteTask(int siteTaskId) + { + using var db = _dbContextFactory.CreateDbContext(); + SiteTask siteTask = db.SiteTask.Find(siteTaskId); + db.SiteTask.Remove(siteTask); + db.SaveChanges(); + } + + public int DeleteSiteTasks(int siteId, int age) + { + using var db = _dbContextFactory.CreateDbContext(); + // delete completed tasks in batches of 100 records + var count = 0; + var purgedate = DateTime.UtcNow.AddDays(-age); + var tasks = db.SiteTask.Where(item => item.SiteId == siteId && item.IsCompleted && item.CreatedOn < purgedate) + .OrderBy(item => item.CreatedOn).Take(100).ToList(); + while (tasks.Count > 0) + { + count += tasks.Count; + db.SiteTask.RemoveRange(tasks); + db.SaveChanges(); + tasks = db.SiteTask.Where(item => item.SiteId == siteId && item.IsCompleted && item.CreatedOn < purgedate) + .OrderBy(item => item.CreatedOn).Take(100).ToList(); + } + return count; + } + } +} diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs index ba901dd9..57d7b7db 100644 --- a/Oqtane.Server/Repository/ThemeRepository.cs +++ b/Oqtane.Server/Repository/ThemeRepository.cs @@ -99,7 +99,7 @@ namespace Oqtane.Repository Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.PackageName = theme.PackageName; Theme.PermissionList = theme.PermissionList; - Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); + Theme.Fingerprint = theme.Fingerprint; Themes.Add(Theme); } @@ -165,6 +165,7 @@ namespace Oqtane.Repository Theme.CreatedOn = theme.CreatedOn; Theme.ModifiedBy = theme.ModifiedBy; Theme.ModifiedOn = theme.ModifiedOn; + Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm")); } // any remaining themes are orphans @@ -329,7 +330,7 @@ namespace Oqtane.Repository { foreach (var resource in theme.Resources) { - if (resource.Url.StartsWith("~")) + if (!string.IsNullOrEmpty(resource.Url) && resource.Url.StartsWith("~")) { resource.Url = resource.Url.Replace("~", "/Themes/" + Utilities.GetTypeName(theme.ThemeName) + "/").Replace("//", "/"); } diff --git a/Oqtane.Server/Services/LocalizationCookieService.cs b/Oqtane.Server/Services/LocalizationCookieService.cs index 1bedfc6f..13a3205b 100644 --- a/Oqtane.Server/Services/LocalizationCookieService.cs +++ b/Oqtane.Server/Services/LocalizationCookieService.cs @@ -16,9 +16,9 @@ namespace Oqtane.Services _accessor = accessor; } - public Task SetLocalizationCookieAsync(string culture) + public Task SetLocalizationCookieAsync(string culture, string uiCulture) { - var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); + var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture, uiCulture)); _accessor.HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index ef7b32d6..3c356fbd 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -21,6 +21,8 @@ namespace Oqtane.Services public class ServerSiteService : ISiteService { private readonly ISiteRepository _sites; + private readonly ISiteGroupMemberRepository _siteGroupMembers; + private readonly IAliasRepository _aliases; private readonly IPageRepository _pages; private readonly IThemeRepository _themes; private readonly IPageModuleRepository _pageModules; @@ -37,9 +39,11 @@ namespace Oqtane.Services private readonly IHttpContextAccessor _accessor; private readonly string _private = "[PRIVATE]"; - public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) + public ServerSiteService(ISiteRepository sites, ISiteGroupMemberRepository siteGroupMembers, IAliasRepository aliases, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserManager userManager, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor) { _sites = sites; + _siteGroupMembers = siteGroupMembers; + _aliases = aliases; _pages = pages; _themes = themes; _pageModules = pageModules; @@ -145,12 +149,7 @@ namespace Oqtane.Services site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString()); // languages - site.Languages = _languages.GetLanguages(site.SiteId).ToList(); - var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); - if (!site.Languages.Exists(item => item.Code == defaultCulture.Name)) - { - site.Languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !site.Languages.Any(l => l.IsDefault) }); - } + site.Languages = GetLanguages(site.SiteId, alias.TenantId); // themes site.Themes = _themes.FilterThemes(_themes.GetThemes(site.SiteId).ToList()); @@ -158,6 +157,7 @@ namespace Oqtane.Services // installation date used for fingerprinting static assets site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"))); + // set tenant site.TenantId = alias.TenantId; } else @@ -311,6 +311,47 @@ namespace Oqtane.Services return modules.OrderBy(item => item.PageId).ThenBy(item => item.Pane).ThenBy(item => item.Order).ToList(); } + private List GetLanguages(int siteId, int tenantId) + { + var languages = new List(); + + var siteGroupMembers = _siteGroupMembers.GetSiteGroupMembers(); + if (siteGroupMembers.Any(item => item.SiteId == siteId && item.SiteGroup.Type == SiteGroupTypes.Localization)) + { + // site is part of a localized site group - get all languages from the site group + var sites = _sites.GetSites().ToList(); + var aliases = _aliases.GetAliases().ToList(); + + foreach (var siteGroupId in siteGroupMembers.Where(item => item.SiteId == siteId && item.SiteGroup.Type == SiteGroupTypes.Localization).Select(item => item.SiteGroupId).Distinct().ToList()) + { + foreach (var siteGroupMember in siteGroupMembers.Where(item => item.SiteGroupId == siteGroupId)) + { + var site = sites.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId); + if (site != null && !string.IsNullOrEmpty(site.CultureCode)) + { + var alias = aliases.FirstOrDefault(item => item.SiteId == siteGroupMember.SiteId && item.TenantId == tenantId && item.IsDefault); + if (alias != null) + { + languages.Add(new Language { Code = site.CultureCode, Name = "", AliasName = alias.Name, IsDefault = false }); + } + } + } + } + } + else + { + // use site languages + languages = _languages.GetLanguages(siteId).ToList(); + var defaultCulture = CultureInfo.GetCultureInfo(Constants.DefaultCulture); + if (!languages.Exists(item => item.Code == defaultCulture.Name)) + { + languages.Add(new Language { Code = defaultCulture.Name, Name = "", Version = Constants.Version, IsDefault = !languages.Any(l => l.IsDefault) }); + } + } + + return languages; + } + [Obsolete("This method is deprecated.", false)] public void SetAlias(Alias alias) { diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor index a3d25ab5..886b0c07 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -4,7 +4,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -55,7 +55,7 @@ if (PageState.Action == "Edit") { _id = Int32.Parse(PageState.QueryString["id"]); - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); if ([Module] != null) { _name = [Module].Name; @@ -86,14 +86,14 @@ [Module] [Module] = new [Module](); [Module].ModuleId = ModuleState.ModuleId; [Module].Name = _name; - [Module] = await [Module]Service.Add[Module]Async([Module]); + [Module] = await Client[Module]Service.Add[Module]Async([Module]); await logger.LogInformation("[Module] Added {[Module]}", [Module]); } else { - [Module] [Module] = await [Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); + [Module] [Module] = await Client[Module]Service.Get[Module]Async(_id, ModuleState.ModuleId); [Module].Name = _name; - await [Module]Service.Update[Module]Async([Module]); + await Client[Module]Service.Update[Module]Async([Module]); await logger.LogInformation("[Module] Updated {[Module]}", [Module]); } NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor index 6f43dcb3..e5d5d4e3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -3,7 +3,7 @@ @namespace [Owner].Module.[Module] @inherits ModuleBase -@inject I[Module]Service [Module]Service +@inject I[Module]Service Client[Module]Service @inject NavigationManager NavigationManager @inject IStringLocalizer Localizer @@ -52,7 +52,7 @@ else { try { - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); } catch (Exception ex) { @@ -65,9 +65,9 @@ else { try { - await [Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); + await Client[Module]Service.Delete[Module]Async([Module].[Module]Id, ModuleState.ModuleId); await logger.LogInformation("[Module] Deleted {[Module]}", [Module]); - _[Module]s = await [Module]Service.Get[Module]sAsync(ModuleState.ModuleId); + _[Module]s = await Client[Module]Service.Get[Module]sAsync(ModuleState.ModuleId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs similarity index 74% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs index d433d698..ec451c42 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/[Module]Service.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Services/Client[Module]Service.cs @@ -7,22 +7,10 @@ using Oqtane.Shared; namespace [Owner].Module.[Module].Services { - public interface I[Module]Service + + public class Client[Module]Service : ServiceBase, I[Module]Service { - Task> Get[Module]sAsync(int ModuleId); - - Task Get[Module]Async(int [Module]Id, int ModuleId); - - Task Add[Module]Async(Models.[Module] [Module]); - - Task Update[Module]Async(Models.[Module] [Module]); - - Task Delete[Module]Async(int [Module]Id, int ModuleId); - } - - public class [Module]Service : ServiceBase, I[Module]Service - { - public [Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } + public Client[Module]Service(HttpClient http, SiteState siteState) : base(http, siteState) { } private string Apiurl => CreateApiUrl("[Module]"); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs index 95ed096c..14170a14 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs @@ -11,7 +11,7 @@ namespace [Owner].Module.[Module].Startup { if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) { - services.AddScoped(); + services.AddScoped(); } } } 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 9197b9b0..fce77d74 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,10 @@ - - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/_Imports.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/_Imports.razor index 5932080a..37443fac 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/_Imports.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/_Imports.razor @@ -9,6 +9,7 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.Extensions.Localization @using Microsoft.JSInterop +@using Microsoft.AspNetCore.Authorization @using Oqtane.Models @using Oqtane.Modules diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/[Module]Service.cs rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/Services/Server[Module]Service.cs 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 45389595..c69fd9cf 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 @@ -20,14 +20,13 @@ - - - - + + + + - diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs new file mode 100644 index 00000000..8c75bf44 --- /dev/null +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/Interfaces/I[Module]Service.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace [Owner].Module.[Module].Services +{ + public interface I[Module]Service + { + Task> Get[Module]sAsync(int ModuleId); + + Task Get[Module]Async(int [Module]Id, int ModuleId); + + Task Add[Module]Async(Models.[Module] [Module]); + + Task Update[Module]Async(Models.[Module] [Module]); + + Task Delete[Module]Async(int [Module]Id, int ModuleId); + } +} diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj index 63175d61..c27c1e59 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Shared/[Owner].Module.[Module].Shared.csproj @@ -8,6 +8,7 @@ [Owner] [Description] [Owner] + true [Owner].Module.[Module].Shared.Oqtane diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx index 74f9bd8a..813952c3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].slnx @@ -1,5 +1,7 @@ - + + + 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 db9ce2c7..3378693d 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 @@ -14,9 +14,9 @@ - - - + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx index 1b1f9c26..9a634bc9 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].slnx @@ -1,5 +1,7 @@ - + + + diff --git a/Oqtane.Shared/Models/GlobalReplace.cs b/Oqtane.Shared/Models/GlobalReplace.cs new file mode 100644 index 00000000..c2a63ed3 --- /dev/null +++ b/Oqtane.Shared/Models/GlobalReplace.cs @@ -0,0 +1,43 @@ +namespace Oqtane.Models +{ + /// + /// Describes a global replace operation + /// + public class GlobalReplace + { + /// + /// text to replace + /// + public string Find { get; set; } + + /// + /// replacement text + /// + public string Replace { get; set; } + + /// + /// case sensitive + /// + public bool CaseSensitive { get; set; } + + /// + /// include site properties + /// + public bool Site { get; set; } + + /// + /// include page properties + /// + public bool Pages { get; set; } + + /// + /// include module properties + /// + public bool Modules { get; set; } + + /// + /// include content + /// + public bool Content { get; set; } + } +} diff --git a/Oqtane.Shared/Models/Job.cs b/Oqtane.Shared/Models/Job.cs index e54e9df4..f505fa0e 100644 --- a/Oqtane.Shared/Models/Job.cs +++ b/Oqtane.Shared/Models/Job.cs @@ -3,7 +3,7 @@ using System; namespace Oqtane.Models { /// - /// Definition of a Job / Task which is run on the server. + /// Definition of a Job which is run on the server /// When Jobs run, they create a /// public class Job : ModelBase diff --git a/Oqtane.Shared/Models/Language.cs b/Oqtane.Shared/Models/Language.cs index 750d8583..306c1eaf 100644 --- a/Oqtane.Shared/Models/Language.cs +++ b/Oqtane.Shared/Models/Language.cs @@ -41,6 +41,12 @@ namespace Oqtane.Models /// public string Version { get; set; } + [NotMapped] + /// + /// The primary alias name for the site with this language + /// + public string AliasName { get; set; } + public Language Clone() { return new Language @@ -50,7 +56,8 @@ namespace Oqtane.Models Name = Name, Code = Code, IsDefault = IsDefault, - Version = Version + Version = Version, + AliasName = AliasName }; } } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index 7775fcf5..1c2a23cb 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -33,6 +33,11 @@ namespace Oqtane.Models /// public bool AllPages { get; set; } + /// + /// indicates if the module is shared across multiple pages (only set in specific scenarios to ensure performance) + /// + [NotMapped] + public bool IsShared { get; set; } /// /// Reference to the used for this module. diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 42ecb5f9..ec0e4477 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -24,7 +24,7 @@ namespace Oqtane.Models get => _url; set { - _url = (value.Contains("://")) ? value : (!value.StartsWith("/") && !value.StartsWith("~") ? "/" : "") + value; + _url = (string.IsNullOrEmpty(value) || value.Contains("://")) ? value : (!value.StartsWith("/") && !value.StartsWith("~") ? "/" : "") + value; } } diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index 4ba4ede5..4c5956cf 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -26,6 +26,11 @@ namespace Oqtane.Models /// public string TimeZoneId { get; set; } + /// + /// The default culture for the site (ie. en-US) + /// + public string CultureCode { get; set; } + /// /// Reference to a which has the Logo for this site. /// Should be an image. @@ -96,7 +101,7 @@ namespace Oqtane.Models public string RenderMode { get; set; } /// - /// The render mode for UI components which require interactivity ie. Server,WebAssembly,Auto + /// The hosting model for UI components which require interactivity ie. Server,WebAssembly,Auto /// public string Runtime { get; set; } @@ -120,11 +125,6 @@ namespace Oqtane.Models /// public string Version { get; set; } - /// - /// The home page of the site - the "/" path will be used by default if no home page is specified - /// - public int? HomePageId { get; set; } - /// /// Content to be included in the head of the page /// @@ -217,6 +217,7 @@ namespace Oqtane.Models SiteId = SiteId, Name = Name, TimeZoneId = TimeZoneId, + CultureCode = CultureCode, LogoFileId = LogoFileId, FaviconFileId = FaviconFileId, DefaultThemeType = DefaultThemeType, @@ -235,7 +236,6 @@ namespace Oqtane.Models Hybrid = Hybrid, EnhancedNavigation = EnhancedNavigation, Version = Version, - HomePageId = HomePageId, HeadContent = HeadContent, BodyContent = BodyContent, IsDeleted = IsDeleted, @@ -262,6 +262,10 @@ namespace Oqtane.Models [NotMapped] [Obsolete("This property is deprecated.", false)] public string DefaultLayoutType { get; set; } + + [NotMapped] + [Obsolete("This property is deprecated.", false)] + public int? HomePageId { get; set; } #endregion } } diff --git a/Oqtane.Shared/Models/SiteGroup.cs b/Oqtane.Shared/Models/SiteGroup.cs new file mode 100644 index 00000000..5d71709b --- /dev/null +++ b/Oqtane.Shared/Models/SiteGroup.cs @@ -0,0 +1,30 @@ +namespace Oqtane.Models +{ + public class SiteGroup : ModelBase + { + /// + /// ID to identify the group + /// + public int SiteGroupId { get; set; } + + /// + /// Name of the group + /// + public string Name { get; set; } + + /// + /// Group type ie. Synchronization, Localization + /// + public string Type { get; set; } + + /// + /// SiteId of the primary site in the group + /// + public int PrimarySiteId { get; set; } + + /// + /// Specifies if the group should be synchronized + /// + public bool Synchronize{ get; set; } + } +} diff --git a/Oqtane.Shared/Models/SiteGroupMember.cs b/Oqtane.Shared/Models/SiteGroupMember.cs new file mode 100644 index 00000000..cb572509 --- /dev/null +++ b/Oqtane.Shared/Models/SiteGroupMember.cs @@ -0,0 +1,39 @@ +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Oqtane.Models +{ + public class SiteGroupMember : ModelBase + { + /// + /// ID to identify the site group + /// + public int SiteGroupMemberId { get; set; } + + /// + /// Reference to the . + /// + public int SiteGroupId { get; set; } + + /// + /// Reference to the . + /// + public int SiteId { get; set; } + + /// + /// The last date/time the site was synchronized + /// + public DateTime? SynchronizedOn { get; set; } + + /// + /// The itself. + /// + public SiteGroup SiteGroup { get; set; } + + /// + /// The primary alias for the site + /// + [NotMapped] + public string AliasName { get; set; } + } +} diff --git a/Oqtane.Shared/Models/SiteTask.cs b/Oqtane.Shared/Models/SiteTask.cs new file mode 100644 index 00000000..63b6d973 --- /dev/null +++ b/Oqtane.Shared/Models/SiteTask.cs @@ -0,0 +1,54 @@ +namespace Oqtane.Models +{ + /// + /// An instance of a SiteTask which is executed by the SiteTaskJob + /// + public class SiteTask : ModelBase + { + /// + /// Internal ID + /// + public int SiteTaskId { get; set; } + + /// + /// Site where the Task should execute + /// + public int SiteId { get; set; } + + /// + /// Name for simple identification + /// + public string Name { get; set; } + + /// + /// Fully qualified type name of the Task + /// + public string Type { get; set; } + + /// + /// Any parameters related to the Task + /// + public string Parameters { get; set; } + + /// + /// Indicates if the Task is completed + /// + public bool IsCompleted { get; set; } + + /// + /// Any status information provided by the Task + /// + public string Status { get; set; } + + // constructors + public SiteTask() { } + + public SiteTask(int siteId, string name, string type, string parameters) + { + SiteId = siteId; + Name = name; + Type = type; + Parameters = parameters; + } + } +} diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index dd3fa320..e84bb723 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -36,6 +36,11 @@ namespace Oqtane.Models /// public string TimeZoneId { get; set; } + /// + /// The default culture for the user (ie. en-US) + /// + public string CultureCode { get; set; } + /// /// Reference to a containing the users photo. /// @@ -140,6 +145,7 @@ namespace Oqtane.Models DisplayName = DisplayName, Email = Email, TimeZoneId = TimeZoneId, + CultureCode = CultureCode, PhotoFileId = PhotoFileId, LastLoginOn = LastLoginOn, LastIPAddress = LastIPAddress, diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index 1ed1fc4a..7b8432a7 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 8f52a8db..2df66252 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 = "10.0.4"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3,10.0.4"; + public static readonly string Version = "10.1.0"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2,10.0.3,10.0.4,10.1.0"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; @@ -36,6 +36,7 @@ namespace Oqtane.Shared public const string AdminSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.AdminSiteTemplate, Oqtane.Server"; public const string DefaultSiteTemplate = "Oqtane.Infrastructure.SiteTemplates.DefaultSiteTemplate, Oqtane.Server"; + public const string EmptySiteTemplate = "Oqtane.Infrastructure.SiteTemplates.EmptySiteTemplate, Oqtane.Server"; public static readonly string[] DefaultHostModuleTypes = new[] { "Upgrade", "Themes", "SystemInfo", "Sql", "Sites", "ModuleDefinitions", "Logs", "Jobs", "ModuleCreator" }; diff --git a/Oqtane.Shared/Shared/EntityNames.cs b/Oqtane.Shared/Shared/EntityNames.cs index 1f9e162d..72571e82 100644 --- a/Oqtane.Shared/Shared/EntityNames.cs +++ b/Oqtane.Shared/Shared/EntityNames.cs @@ -16,6 +16,8 @@ namespace Oqtane.Shared public const string Role = "Role"; public const string Setting = "Setting"; public const string Site = "Site"; + public const string SiteGroup = "SiteGroup"; + public const string SiteGroupMember = "SiteGroupMember"; public const string Tenant = "Tenant"; public const string Theme = "Theme"; public const string UrlMapping = "UrlMapping"; diff --git a/Oqtane.Shared/Shared/SiteGroupTypes.cs b/Oqtane.Shared/Shared/SiteGroupTypes.cs new file mode 100644 index 00000000..e1a56052 --- /dev/null +++ b/Oqtane.Shared/Shared/SiteGroupTypes.cs @@ -0,0 +1,9 @@ +namespace Oqtane.Shared +{ + public class SiteGroupTypes + { + public const string Synchronization = "Synchronization"; + public const string ChangeDetection = "ChangeDetection"; + public const string Localization = "Localization"; + } +} diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 45888f0c..ca710771 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -501,11 +501,11 @@ namespace Oqtane.Shared public static string GetUrlPath(string url) { - if (url.Contains("?")) + if (!string.IsNullOrEmpty(url) && url.Contains("?")) { url = url.Substring(0, url.IndexOf("?")); } - return url; + return url ?? ""; } public static string LogMessage(object @class, string message) diff --git a/README.md b/README.md index 7f5c9ca3..3071a708 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) was released on January 20, 2026 and is a maintenance release including 27 pull requests by 3 different contributors, pushing the total number of project commits all-time over 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[10.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0) was released on February 25, 2026 and is a maintenance release including 72 pull requests by 6 different contributors, pushing the total number of project commits all-time over 7600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. # Try It Now! @@ -111,6 +111,12 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[10.1.0](https://github.com/oqtane/oqtane.framework/releases/tag/v10.1.0) (Feb 25, 2026) +- [x] Site Groups, Content Synchronization, Content Localization +- [x] Global Replace +- [x] Copy Page +- [x] Site Tasks + [10.0.4](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.4) (Jan 20, 2026) - [x] Stabilization improvements diff --git a/azuredeploy.json b/azuredeploy.json index 72478428..2f2ee83c 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -220,7 +220,7 @@ "apiVersion": "2024-04-01", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "properties": { - "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.4/Oqtane.Framework.10.0.4.Install.zip" + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.1.0/Oqtane.Framework.10.1.0.Install.zip" }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"