diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index 4cb6c220..f699d7fe 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -41,6 +41,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor index 7fad443d..7ac71a70 100644 --- a/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor +++ b/Oqtane.Client/Modules/Admin/GlobalReplace/Index.razor @@ -1,7 +1,7 @@ @namespace Oqtane.Modules.Admin.GlobalReplace @using System.Text.Json @inherits ModuleBase -@inject ISettingService SettingService +@inject IJobTaskService JobTaskService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -97,9 +97,9 @@ 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); + var jobTask = new JobTask(PageState.Site.SiteId, "Global Replace", "Oqtane.Infrastructure.GlobalReplaceTask, Oqtane.Server", JsonSerializer.Serialize(replace)); + await JobTaskService.AddJobTaskAsync(jobTask); + AddModuleMessage(Localizer["Success.Save"], MessageType.Success); } else diff --git a/Oqtane.Client/Modules/Admin/Users/Users.razor b/Oqtane.Client/Modules/Admin/Users/Users.razor index a3ac65d3..0a357a57 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 IJobTaskService JobTaskService @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 jobTask = new JobTask(PageState.Site.SiteId, "Import Users", "Oqtane.Infrastructure.ImportUsersTask, Oqtane.Server", $"{fileid}:{_notify}"); + await JobTaskService.AddJobTaskAsync(jobTask); + AddModuleMessage(Localizer["Message.Import.Success"], MessageType.Success); } else { diff --git a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx index 08909f15..e5339e4d 100644 --- a/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/GlobalReplace/Index.resx @@ -154,7 +154,7 @@ Specify if module properties should be updated (ie. title, header, footer) - Your Global Replace Request Has Been Submitted And Will Be Executed Shortly + Your Global Replace Request Has Been Submitted And Will Be Executed Shortly. Please Be Patient. Error Saving Global Replace 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/Services/JobTaskService.cs b/Oqtane.Client/Services/JobTaskService.cs new file mode 100644 index 00000000..4e596a52 --- /dev/null +++ b/Oqtane.Client/Services/JobTaskService.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 IJobTaskService + { + /// + /// Return a specific task + /// + /// + /// + Task GetJobTaskAsync(int jobTaskId); + + /// + /// Adds a new task + /// + /// + /// + Task AddJobTaskAsync(JobTask jobTask); + } + + [PrivateApi("Don't show in the documentation, as everything should use the Interface")] + public class JobTaskService : ServiceBase, IJobTaskService + { + public JobTaskService(HttpClient http, SiteState siteState) : base(http, siteState) { } + + private string Apiurl => CreateApiUrl("JobTask"); + + public async Task GetJobTaskAsync(int jobTaskId) + { + return await GetJsonAsync($"{Apiurl}/{jobTaskId}"); + } + + public async Task AddJobTaskAsync(JobTask jobTask) + { + return await PostJsonAsync(Apiurl, jobTask); + } + } +} 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.Server/Controllers/JobTaskController.cs b/Oqtane.Server/Controllers/JobTaskController.cs new file mode 100644 index 00000000..5e02f229 --- /dev/null +++ b/Oqtane.Server/Controllers/JobTaskController.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 JobTaskController : Controller + { + private readonly IJobTaskRepository _jobTasks; + private readonly ILogManager _logger; + private readonly Alias _alias; + + public JobTaskController(IJobTaskRepository jobTasks, ILogManager logger, ITenantManager tenantManager) + { + _jobTasks = jobTasks; + _logger = logger; + _alias = tenantManager.GetAlias(); + } + + // GET api//5 + [HttpGet("{id}")] + [Authorize(Roles = RoleNames.Admin)] + public JobTask Get(int id) + { + var jobTask = _jobTasks.GetJobTask(id); + if (jobTask.SiteId == _alias.SiteId) + { + return jobTask; + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return null; + } + } + + // POST api/ + [HttpPost] + [Authorize(Roles = RoleNames.Admin)] + public JobTask Post([FromBody] JobTask jobTask) + { + if (ModelState.IsValid && jobTask.SiteId == _alias.SiteId) + { + jobTask.IsCompleted = false; + jobTask = _jobTasks.AddJobTask(jobTask); + _logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Task Added {JobTask}", jobTask); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Job Task Post Attempt {JobTask}", jobTask); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + jobTask = null; + } + return jobTask; + } + } +} 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 8249560a..a26768ee 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -214,6 +214,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -274,6 +275,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs new file mode 100644 index 00000000..c2941553 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Interfaces/IJobTask.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public interface IJobTask + { + string ExecuteTask(IServiceProvider provider, Site site, string parameters); + + Task ExecuteTaskAsync(IServiceProvider provider, Site site, string parameters); + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs b/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs deleted file mode 100644 index 008230af..00000000 --- a/Oqtane.Server/Infrastructure/Jobs/GlobalReplaceJob.cs +++ /dev/null @@ -1,178 +0,0 @@ -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/TaskJob.cs b/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs new file mode 100644 index 00000000..bcbf7125 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/TaskJob.cs @@ -0,0 +1,83 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class TaskJob : HostedServiceBase + { + public TaskJob(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + Name = "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 = ""; + + var tenantManager = provider.GetRequiredService(); + var tenant = tenantManager.GetTenant(); + + // iterate through sites for current tenant + var siteRepository = provider.GetRequiredService(); + 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 jobTaskRepository = provider.GetRequiredService(); + var tasks = jobTaskRepository.GetJobTasks(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(IJobTask)) != null) + { + try + { + tenantManager.SetAlias(tenant.TenantId, site.SiteId); + + var taskObject = ActivatorUtilities.CreateInstance(provider, taskType); + var taskLog = ((IJobTask)taskObject).ExecuteTask(provider, site, task.Parameters); + taskLog += await ((IJobTask)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; + jobTaskRepository.UpdateJobTask(task); + + log += task.Status + "
"; + } + } + else + { + log += "No Tasks To Execute
"; + } + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs b/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs new file mode 100644 index 00000000..6f8ae05b --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/GlobalReplaceTask.cs @@ -0,0 +1,150 @@ +using System; +using System.Linq; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Modules; +using Oqtane.Repository; + +namespace Oqtane.Infrastructure +{ + public class GlobalReplaceTask : JobTaskBase + { + 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(); + + 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; + + 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
"; + } + + 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}
"; + } + + 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.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 + { + 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}
"; + } + } + } + } + } + } + else + { + log += $"No Criteria Provided
"; + } + + return log; + } + } +} diff --git a/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs b/Oqtane.Server/Infrastructure/Tasks/ImportUsersTask.cs new file mode 100644 index 00000000..28dd843d --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/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 : JobTaskBase + { + 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/Tasks/JobTaskBase.cs b/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs new file mode 100644 index 00000000..af359c7a --- /dev/null +++ b/Oqtane.Server/Infrastructure/Tasks/JobTaskBase.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Oqtane.Models; + +namespace Oqtane.Infrastructure +{ + public class JobTaskBase : IJobTask + { + 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/Migrations/EntityBuilders/JobTaskEntityBuilder.cs b/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs new file mode 100644 index 00000000..393e6df9 --- /dev/null +++ b/Oqtane.Server/Migrations/EntityBuilders/JobTaskEntityBuilder.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations.Operations; +using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; +using Oqtane.Databases.Interfaces; +using Oqtane.Interfaces; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global + +namespace Oqtane.Migrations.EntityBuilders +{ + public class JobTaskEntityBuilder : AuditableBaseEntityBuilder + { + private const string _entityTableName = "JobTask"; + private readonly PrimaryKey _primaryKey = new("PK_JobTask", x => x.JobTaskId); + private readonly ForeignKey _siteForeignKey = new("FK_JobTask_Site", x => x.SiteId, "Site", "SiteId", ReferentialAction.Cascade); + + public JobTaskEntityBuilder(MigrationBuilder migrationBuilder, IDatabase database) : base(migrationBuilder, database) + { + EntityTableName = _entityTableName; + PrimaryKey = _primaryKey; + ForeignKeys.Add(_siteForeignKey); + } + + protected override JobTaskEntityBuilder BuildTable(ColumnsBuilder table) + { + JobTaskId = AddAutoIncrementColumn(table,"JobTaskId"); + 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 JobTaskId { 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/10010004_AddJobTasks.cs b/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.cs new file mode 100644 index 00000000..ef0ab492 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/10010004_AddJobTasks.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 AddJobTasks : MultiDatabaseMigration + { + public AddJobTasks(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var jobTaskEntityBuilder = new JobTaskEntityBuilder(migrationBuilder, ActiveDatabase); + jobTaskEntityBuilder.Create(); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Repository/Context/TenantDBContext.cs b/Oqtane.Server/Repository/Context/TenantDBContext.cs index f6325fd9..57fffa14 100644 --- a/Oqtane.Server/Repository/Context/TenantDBContext.cs +++ b/Oqtane.Server/Repository/Context/TenantDBContext.cs @@ -136,5 +136,6 @@ namespace Oqtane.Repository public virtual DbSet MigrationHistory { get; set; } public virtual DbSet SiteGroup { get; set; } public virtual DbSet SiteGroupMember { get; set; } + public virtual DbSet JobTask { get; set; } } } 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/JobTaskRepository.cs b/Oqtane.Server/Repository/JobTaskRepository.cs new file mode 100644 index 00000000..125afa03 --- /dev/null +++ b/Oqtane.Server/Repository/JobTaskRepository.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IJobTaskRepository + { + IEnumerable GetJobTasks(int siteId); + JobTask GetJobTask(int jobTaskId); + JobTask AddJobTask(JobTask jobTask); + JobTask UpdateJobTask(JobTask jobTask); + void DeleteJobTask(int jobTaskId); + } + + public class JobTaskRepository : IJobTaskRepository + { + private TenantDBContext _db; + + public JobTaskRepository(TenantDBContext context) + { + _db = context; + } + + public IEnumerable GetJobTasks(int siteId) + { + return _db.JobTask.Where(item => item.SiteId == siteId && !item.IsCompleted).OrderBy(item => item.CreatedOn); + } + + public JobTask GetJobTask(int jobTaskId) + { + return _db.JobTask.SingleOrDefault(item => item.JobTaskId == jobTaskId); + } + + public JobTask AddJobTask(JobTask jobTask) + { + _db.JobTask.Add(jobTask); + _db.SaveChanges(); + return jobTask; + } + public JobTask UpdateJobTask(JobTask jobTask) + { + _db.Entry(jobTask).State = EntityState.Modified; + _db.SaveChanges(); + return jobTask; + } + + public void DeleteJobTask(int jobTaskId) + { + JobTask jobTask = _db.JobTask.Find(jobTaskId); + _db.JobTask.Remove(jobTask); + _db.SaveChanges(); + } + } +} 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/JobTask.cs b/Oqtane.Shared/Models/JobTask.cs new file mode 100644 index 00000000..abca61b0 --- /dev/null +++ b/Oqtane.Shared/Models/JobTask.cs @@ -0,0 +1,56 @@ +using System; + +namespace Oqtane.Models +{ + /// + /// An instance of a Task which is executed by the TaskJob + /// + public class JobTask : ModelBase + { + /// + /// Internal ID + /// + public int JobTaskId { 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 JobTask() { } + + public JobTask(int siteId, string name, string type, string parameters) + { + SiteId = siteId; + Name = name; + Type = type; + Parameters = parameters; + } + } +}