From 78177f7890320de58c99a60e81dcbfd38c91a3f1 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Thu, 19 Sep 2024 09:41:11 -0400 Subject: [PATCH] use deep cloning to not muttate cache --- Oqtane.Server/Repository/SiteRepository.cs | 2 +- Oqtane.Server/Services/SiteService.cs | 15 ++- Oqtane.Shared/Models/Language.cs | 14 ++- Oqtane.Shared/Models/Module.cs | 122 +++++++++++++++++--- Oqtane.Shared/Models/Page.cs | 124 ++++++++++++++++----- Oqtane.Shared/Models/Permission.cs | 20 ++-- Oqtane.Shared/Models/Site.cs | 76 ++++++------- 7 files changed, 275 insertions(+), 98 deletions(-) diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 8d2d822c..36495c8f 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -441,7 +441,7 @@ namespace Oqtane.Repository pageModule.Module.PermissionList = new List(); foreach (var permission in pageTemplateModule.PermissionList) { - pageModule.Module.PermissionList.Add(permission.Clone(permission)); + pageModule.Module.PermissionList.Add(permission.Clone()); } pageModule.Module.AllPages = false; pageModule.Module.IsDeleted = false; diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 0d607d38..08e64147 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -71,7 +71,7 @@ namespace Oqtane.Services }); // clone object so that cache is not mutated - site = site.Clone(site); + site = site.Clone(); // trim site settings based on user permissions site.Settings = site.Settings @@ -256,25 +256,28 @@ namespace Oqtane.Services public Task> GetModulesAsync(int siteId, int pageId) { var alias = _tenantManager.GetAlias(); - var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry => + var modules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry => { entry.SlidingExpiration = TimeSpan.FromMinutes(30); return GetPageModules(siteId); }); + // clone object so that cache is not mutated + modules = modules.ConvertAll(module => module.Clone()); + // trim modules for current page based on user permissions - var modules = new List(); - foreach (Module module in sitemodules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList))) + var pagemodules = new List(); + foreach (Module module in modules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList))) { if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList)) { module.Settings = module.Settings .Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList)) .ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, "")); - modules.Add(module); + pagemodules.Add(module); } } - return Task.FromResult(modules); + return Task.FromResult(pagemodules); } private List GetPageModules(int siteId) diff --git a/Oqtane.Shared/Models/Language.cs b/Oqtane.Shared/Models/Language.cs index 11f4af39..82a837ce 100644 --- a/Oqtane.Shared/Models/Language.cs +++ b/Oqtane.Shared/Models/Language.cs @@ -1,4 +1,3 @@ -using System; using System.ComponentModel.DataAnnotations.Schema; namespace Oqtane.Models @@ -40,5 +39,18 @@ namespace Oqtane.Models /// Version of the satellite assembly /// public string Version { get; set; } + + public Language Clone() + { + return new Language + { + LanguageId = LanguageId, + SiteId = SiteId, + Name = Name, + Code = Code, + IsDefault = IsDefault, + Version = Version + }; + } } } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index b90e685c..55f31357 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -2,6 +2,7 @@ using Oqtane.Shared; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -28,11 +29,18 @@ namespace Oqtane.Models public string ModuleDefinitionName { get; set; } /// - /// Determines if this Module Instance should be shown on all pages of the current + /// Determines if this module should be shown on all pages of the current /// public bool AllPages { get; set; } - #region IDeletable Properties (note that these are NotMapped and are only used for storing PageModule properties) + + /// + /// Reference to the used for this module. + /// + [NotMapped] + public ModuleDefinition ModuleDefinition { get; set; } + + #region IDeletable Properties [NotMapped] public string DeletedBy { get; set; } @@ -40,17 +48,26 @@ namespace Oqtane.Models public DateTime? DeletedOn { get; set; } [NotMapped] public bool IsDeleted { get; set; } - + #endregion - + + /// + /// list of permissions for this module + /// [NotMapped] public List PermissionList { get; set; } + /// + /// List of settings for this module + /// [NotMapped] public Dictionary Settings { get; set; } #region PageModule properties + /// + /// The id of the PageModule instance + /// [NotMapped] public int PageModuleId { get; set; } @@ -60,24 +77,39 @@ namespace Oqtane.Models [NotMapped] public int PageId { get; set; } + /// + /// Title of the pagemodule instance + /// [NotMapped] public string Title { get; set; } /// - /// The Pane this module is shown in. + /// The pane where this pagemodule instance will be injected on the page /// [NotMapped] public string Pane { get; set; } + /// + /// The order of the pagemodule instance within the Pane + /// [NotMapped] public int Order { get; set; } + /// + /// The container for the pagemodule instance + /// [NotMapped] public string ContainerType { get; set; } + /// + /// Start of when this module is visible. See also + /// [NotMapped] public DateTime? EffectiveDate { get; set; } + /// + /// End of when this module is visible. See also + /// [NotMapped] public DateTime? ExpiryDate { get; set; } @@ -85,38 +117,67 @@ namespace Oqtane.Models #region SiteRouter properties + /// + /// Stores the type name for the module component being rendered + /// [NotMapped] public string ModuleType { get; set; } + + /// + /// The position of the module instance in a pane + /// [NotMapped] public int PaneModuleIndex { get; set; } + + /// + /// The number of modules in a pane + /// [NotMapped] public int PaneModuleCount { get; set; } + + /// + /// A unique id to help determine if a component should be rendered + /// [NotMapped] public Guid RenderId { get; set; } #endregion - #region ModuleDefinition + #region IModuleControl properties + /// - /// Reference to the used for this module. - /// TODO: todoc - unclear if this is always populated + /// The minimum access level to view the component being rendered /// [NotMapped] - public ModuleDefinition ModuleDefinition { get; set; } - - #endregion - - #region IModuleControl properties - [NotMapped] public SecurityAccessLevel SecurityAccessLevel { get; set; } + + /// + /// An optional title for the component + /// [NotMapped] public string ControlTitle { get; set; } + + /// + /// Optional mapping of Url actions to a component + /// [NotMapped] public string Actions { get; set; } + + /// + /// Optionally indicate if a compoent should not be rendered with the default modal admin container + /// [NotMapped] public bool UseAdminContainer { get; set; } + + /// + /// Optionally specify the render mode for the component (overrides the Site setting) + /// [NotMapped] - public string RenderMode{ get; set; } + public string RenderMode { get; set; } + + /// + /// Optionally specify id the component should be prerendered (overrides the Site setting) + /// [NotMapped] public bool? Prerender { get; set; } @@ -140,5 +201,34 @@ namespace Oqtane.Models } #endregion + + public Module Clone() + { + return new Module + { + ModuleId = ModuleId, + SiteId = SiteId, + ModuleDefinitionName = ModuleDefinitionName, + AllPages = AllPages, + PageModuleId = PageModuleId, + PageId = PageId, + Title = Title, + Pane = Pane, + Order = Order, + ContainerType = ContainerType, + EffectiveDate = EffectiveDate, + ExpiryDate = ExpiryDate, + CreatedBy = CreatedBy, + CreatedOn = CreatedOn, + ModifiedBy = ModifiedBy, + ModifiedOn = ModifiedOn, + DeletedBy = DeletedBy, + DeletedOn = DeletedOn, + IsDeleted = IsDeleted, + ModuleDefinition = ModuleDefinition, + PermissionList = PermissionList.ConvertAll(permission => permission.Clone()), + Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value) + }; + } } -} + } diff --git a/Oqtane.Shared/Models/Page.cs b/Oqtane.Shared/Models/Page.cs index a1b6d370..bec0347e 100644 --- a/Oqtane.Shared/Models/Page.cs +++ b/Oqtane.Shared/Models/Page.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; @@ -75,33 +76,68 @@ namespace Oqtane.Models public string BodyContent { get; set; } /// - /// Icon file for this page. - /// TODO: unclear what this is for, and what icon library is used. Probably FontAwesome? + /// Icon class name for this page /// public string Icon { get; set; } + + /// + /// Indicates if this page should be included in navigation menu + /// public bool IsNavigation { get; set; } + + /// + /// Indicates if this page should be clickable in navigation menu + /// public bool IsClickable { get; set; } - public int? UserId { get; set; } + /// - /// Start of when this assignment is valid. See also + /// Indicates if page is personalizable ie. allows users to create custom versions of the page /// - public DateTime? EffectiveDate { get; set; } - /// - /// End of when this assignment is valid. See also - /// - public DateTime? ExpiryDate { get; set; } public bool IsPersonalizable { get; set; } - #region IDeletable Properties - - public string DeletedBy { get; set; } - public DateTime? DeletedOn { get; set; } - public bool IsDeleted { get; set; } - - #endregion + /// + /// Reference to the user who owns the personalized page + /// + public int? UserId { get; set; } /// - /// List of Pane-names which this Page has. + /// Start of when this page is visible. See also + /// + public DateTime? EffectiveDate { get; set; } + + /// + /// End of when this page is visible. See also + /// + public DateTime? ExpiryDate { get; set; } + + /// + /// The hierarchical level of the page + /// + [NotMapped] + public int Level { get; set; } + + /// + /// Determines if there are sub-pages. True if this page has sub-pages. + /// + [NotMapped] + public bool HasChildren { get; set; } + + /// + /// List of permissions for this page + /// + [NotMapped] + public List PermissionList { get; set; } + + /// + /// List of settings for this page + /// + [NotMapped] + public Dictionary Settings { get; set; } + + #region SiteRouter properties + + /// + /// List of Pane names for the Theme assigned to this page /// [NotMapped] public List Panes { get; set; } @@ -112,20 +148,15 @@ namespace Oqtane.Models [NotMapped] public List Resources { get; set; } - [NotMapped] - public List PermissionList { get; set; } + #endregion - [NotMapped] - public Dictionary Settings { get; set; } + #region IDeletable Properties - [NotMapped] - public int Level { get; set; } + public string DeletedBy { get; set; } + public DateTime? DeletedOn { get; set; } + public bool IsDeleted { get; set; } - /// - /// Determines if there are sub-pages. True if this page has sub-pages. - /// - [NotMapped] - public bool HasChildren { get; set; } + #endregion #region Deprecated Properties @@ -152,5 +183,42 @@ namespace Oqtane.Models } #endregion + + public Page Clone() + { + return new Page + { + PageId = PageId, + SiteId = SiteId, + Path = Path, + ParentId = ParentId, + Name = Name, + Title = Title, + Order = Order, + Url = Url, + ThemeType = ThemeType, + DefaultContainerType = DefaultContainerType, + HeadContent = HeadContent, + BodyContent = BodyContent, + Icon = Icon, + IsNavigation = IsNavigation, + IsClickable = IsClickable, + UserId = UserId, + IsPersonalizable = IsPersonalizable, + EffectiveDate = EffectiveDate, + ExpiryDate = ExpiryDate, + Level = Level, + HasChildren = HasChildren, + CreatedBy = CreatedBy, + CreatedOn = CreatedOn, + ModifiedBy = ModifiedBy, + ModifiedOn = ModifiedOn, + DeletedBy = DeletedBy, + DeletedOn = DeletedOn, + IsDeleted = IsDeleted, + PermissionList = PermissionList.ConvertAll(permission => permission.Clone()), + Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value) + }; + } } } diff --git a/Oqtane.Shared/Models/Permission.cs b/Oqtane.Shared/Models/Permission.cs index 1448e039..c83f1234 100644 --- a/Oqtane.Shared/Models/Permission.cs +++ b/Oqtane.Shared/Models/Permission.cs @@ -101,17 +101,21 @@ namespace Oqtane.Models IsAuthorized = isAuthorized; } - public Permission Clone(Permission permission) + public Permission Clone() { return new Permission { - SiteId = permission.SiteId, - EntityName = permission.EntityName, - EntityId = permission.EntityId, - PermissionName = permission.PermissionName, - RoleName = permission.RoleName, - UserId = permission.UserId, - IsAuthorized = permission.IsAuthorized + SiteId = SiteId, + EntityName = EntityName, + EntityId = EntityId, + PermissionName = PermissionName, + RoleName = RoleName, + UserId = UserId, + IsAuthorized = IsAuthorized, + CreatedBy = CreatedBy, + CreatedOn = CreatedOn, + ModifiedBy = ModifiedBy, + ModifiedOn = ModifiedOn }; } diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index d5508858..b108cea2 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -187,47 +187,47 @@ namespace Oqtane.Models [NotMapped] public List Themes { get; set; } - public Site Clone(Site site) + public Site Clone() { return new Site { - SiteId = site.SiteId, - TenantId = site.TenantId, - Name = site.Name, - LogoFileId = site.LogoFileId, - FaviconFileId = site.FaviconFileId, - DefaultThemeType = site.DefaultThemeType, - DefaultContainerType = site.DefaultContainerType, - AdminContainerType = site.AdminContainerType, - PwaIsEnabled = site.PwaIsEnabled, - PwaAppIconFileId = site.PwaAppIconFileId, - PwaSplashIconFileId = site.PwaSplashIconFileId, - AllowRegistration = site.AllowRegistration, - VisitorTracking = site.VisitorTracking, - CaptureBrokenUrls = site.CaptureBrokenUrls, - SiteGuid = site.SiteGuid, - RenderMode = site.RenderMode, - Runtime = site.Runtime, - Prerender = site.Prerender, - Hybrid = site.Hybrid, - Version = site.Version, - HomePageId = site.HomePageId, - HeadContent = site.HeadContent, - BodyContent = site.BodyContent, - IsDeleted = site.IsDeleted, - DeletedBy = site.DeletedBy, - DeletedOn = site.DeletedOn, - ImageFiles = site.ImageFiles, - UploadableFiles = site.UploadableFiles, - SiteTemplateType = site.SiteTemplateType, - CreatedBy = site.CreatedBy, - CreatedOn = site.CreatedOn, - ModifiedBy = site.ModifiedBy, - ModifiedOn = site.ModifiedOn, - Settings = site.Settings.ToDictionary(), - Pages = site.Pages.ToList(), - Languages = site.Languages.ToList(), - Themes = site.Themes.ToList() + SiteId = SiteId, + TenantId = TenantId, + Name = Name, + LogoFileId = LogoFileId, + FaviconFileId = FaviconFileId, + DefaultThemeType = DefaultThemeType, + DefaultContainerType = DefaultContainerType, + AdminContainerType = AdminContainerType, + PwaIsEnabled = PwaIsEnabled, + PwaAppIconFileId = PwaAppIconFileId, + PwaSplashIconFileId = PwaSplashIconFileId, + AllowRegistration = AllowRegistration, + VisitorTracking = VisitorTracking, + CaptureBrokenUrls = CaptureBrokenUrls, + SiteGuid = SiteGuid, + RenderMode = RenderMode, + Runtime = Runtime, + Prerender = Prerender, + Hybrid = Hybrid, + Version = Version, + HomePageId = HomePageId, + HeadContent = HeadContent, + BodyContent = BodyContent, + IsDeleted = IsDeleted, + DeletedBy = DeletedBy, + DeletedOn = DeletedOn, + ImageFiles = ImageFiles, + UploadableFiles = UploadableFiles, + SiteTemplateType = SiteTemplateType, + CreatedBy = CreatedBy, + CreatedOn = CreatedOn, + ModifiedBy = ModifiedBy, + ModifiedOn = ModifiedOn, + Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value), + Pages = Pages.ConvertAll(page => page.Clone()), + Languages = Languages.ConvertAll(language => language.Clone()), + Themes = Themes }; }