From 13a58ed0997cd7b03a7861e2f4990714104e2999 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 18 Feb 2026 13:59:25 -0500 Subject: [PATCH] add new global replace service for bulk updating content --- .../Modules/Admin/GlobalReplace/Index.razor | 117 ++++++++++++ .../Modules/Admin/GlobalReplace/Index.resx | 174 +++++++++++++++++ Oqtane.Client/Resources/SharedResources.resx | 3 + .../Infrastructure/Jobs/GlobalReplaceJob.cs | 178 ++++++++++++++++++ .../Infrastructure/Jobs/NotificationJob.cs | 6 +- Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs | 2 +- .../Infrastructure/Jobs/SearchIndexJob.cs | 2 +- .../Infrastructure/Jobs/SynchronizationJob.cs | 4 +- .../SiteTemplates/AdminSiteTemplate.cs | 28 +++ .../Infrastructure/UpgradeManager.cs | 42 +++++ .../Repository/ModuleDefinitionRepository.cs | 8 +- Oqtane.Shared/Models/GlobalReplace.cs | 43 +++++ 12 files changed, 599 insertions(+), 8 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor create mode 100644 Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx create mode 100644 Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs create mode 100644 Oqtane.Shared/Models/GlobalReplace.cs diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor new file mode 100644 index 00000000..7fad443d --- /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 ISettingService SettingService +@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 settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + settings = SettingService.SetSetting(settings, "GlobalReplace_" + DateTime.UtcNow.ToString("yyyyMMddHHmmss"), JsonSerializer.Serialize(replace)); + await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); + 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/Resources/Modules/Admin/GlobalReplace/Index.resx b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx new file mode 100644 index 00000000..08909f15 --- /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 + + + 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/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index e1441d7c..06ea6782 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -483,4 +483,7 @@ Installed + + Global Replace + \ No newline at end of file diff --git a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs new file mode 100644 index 00000000..008230af --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs @@ -0,0 +1,178 @@ +using System; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class GlobalReplaceJob : HostedServiceBase + { + public GlobalReplaceJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "Global Replace Job"; + Frequency = "m"; // run every minute. + Interval = 1; + IsEnabled = true; + } + + public override async Task ExecuteJobAsync(IServiceProvider provider) + { + string log = ""; + + // get services + var siteRepository = provider.GetRequiredService(); + var settingRepository = provider.GetRequiredService(); + var tenantManager = provider.GetRequiredService(); + var pageRepository = provider.GetRequiredService(); + var pageModuleRepository = provider.GetRequiredService(); + + var sites = siteRepository.GetSites().ToList(); + foreach (var site in sites.Where(item => !item.IsDeleted)) + { + log += $"Processing Site: {site.Name}
"; + + // get global replace items in order by date/time submitted + var globalReplaceSettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId) + .Where(item => item.SettingName.StartsWith("GlobalReplace_")) + .OrderBy(item => item.SettingName); + + if (globalReplaceSettings != null && globalReplaceSettings.Any()) + { + // get first item + var globalReplace = JsonSerializer.Deserialize(globalReplaceSettings.First().SettingValue); + + var find = globalReplace.Find; + var replace = globalReplace.Replace; + var comparisonType = (globalReplace.CaseSensitive) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + + log += $"Replacing: '{find}' With: '{replace}' Case Sensitive: {globalReplace.CaseSensitive}
"; + + var tenantId = tenantManager.GetTenant().TenantId; + tenantManager.SetAlias(tenantId, 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
"; + } + + var pages = pageRepository.GetPages(site.SiteId); + var pageModules = pageModuleRepository.GetPageModules(site.SiteId); + + // 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}
"; + } + + foreach (var pageModule in pageModules.Where(item => item.PageId == page.PageId)) + { + // pagemodule 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.Path}
"; + } + + // 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 + { + // module content + var moduleObject = ActivatorUtilities.CreateInstance(provider, moduleType); + var moduleContent = ((IPortable)moduleObject).ExportModule(pageModule.Module); + if (!string.IsNullOrEmpty(moduleContent) && moduleContent.Contains(find, comparisonType) && globalReplace.Content) + { + moduleContent = moduleContent.Replace(find, replace, comparisonType); + ((IPortable)moduleObject).ImportModule(pageModule.Module, moduleContent, pageModule.Module.ModuleDefinition.Version); + log += $"Module Content Updated: {pageModule.Title} - /{page.Path}
"; + } + } + catch (Exception ex) + { + log += $"Error Processing Module {pageModule.Module.ModuleDefinition.Name} - {ex.Message}
"; + } + } + } + } + } + + // remove global replace setting to prevent reprocessing + settingRepository.DeleteSetting(EntityNames.Site, globalReplaceSettings.First().SettingId); + } + else + { + log += $"No Criteria Provided
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index 48589187..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") @@ -290,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 3bcff41d..06c88ab5 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -36,7 +36,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 Site: " + site.Name + "
"; int count; 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/SynchronizationJob.cs b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs index 71e83467..b59611e7 100644 --- a/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/SynchronizationJob.cs @@ -68,7 +68,7 @@ namespace Oqtane.Infrastructure // get primary site var primarySite = sites.FirstOrDefault(item => item.SiteId == siteGroup.PrimarySiteId); - if (primarySite != null) + if (primarySite != null && !primarySite.IsDeleted) { // update flag to prevent job from processing group again siteGroup.Synchronize = false; @@ -112,7 +112,7 @@ namespace Oqtane.Infrastructure } else { - log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist
"; + log += $"Site Group {siteGroup.Name} Has A PrimarySiteId {siteGroup.PrimarySiteId} Which Does Not Exist Or Is Deleted
"; } } 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/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/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs index aa48d232..fd1e11e7 100644 --- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs +++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs @@ -354,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" : "") }; } @@ -434,6 +434,12 @@ 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(); 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; } + } +}