diff --git a/.azuredevops/azure-pipelines.yml b/.azuredevops/azure-pipelines.yml index 4ec98cd..863dd14 100644 --- a/.azuredevops/azure-pipelines.yml +++ b/.azuredevops/azure-pipelines.yml @@ -37,4 +37,4 @@ steps: - 7.0.x - 8.0.x - 10.0.x - workingDirectory: "$(System.DefaultWorkingDirectory)" \ No newline at end of file + workingDirectory: "$(System.DefaultWorkingDirectory)" diff --git a/Addon.Episerver.EnvironmentSynchronizer/Addon.Episerver.EnvironmentSynchronizer.csproj b/Addon.Episerver.EnvironmentSynchronizer/Addon.Episerver.EnvironmentSynchronizer.csproj index c96ab06..2157f7d 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Addon.Episerver.EnvironmentSynchronizer.csproj +++ b/Addon.Episerver.EnvironmentSynchronizer/Addon.Episerver.EnvironmentSynchronizer.csproj @@ -1,35 +1,35 @@ - - Library - false - net6.0;net7.0;net8.0;net10.0 - 1.3.2.0 - true - embedded - Epinova - Environment Synchronizer - Ove Lartelius, Linus Ekström - Epinova AB - https://github.com/Epinova/Addon.Episerver.EnvironmentSynchronizer - false - Environment synchronizer helps you to set your environment into a known state after synchronizing databases between environments. - Copyright © 2021 Epinova AB - - - true - - + + Library + false + net6.0;net7.0;net8.0;net10.0 + 1.3.2.0 + true + embedded + Epinova - Environment Synchronizer + Ove Lartelius, Linus Ekström + Epinova AB + https://github.com/Epinova/Addon.Episerver.EnvironmentSynchronizer + false + Environment synchronizer helps you to set your environment into a known state after synchronizing databases between environments. + Copyright © 2021 Epinova AB + + + true + + - + - - - - - - - + + + + + + + - \ No newline at end of file + diff --git a/Addon.Episerver.EnvironmentSynchronizer/Configuration/ConfigurationReader.cs b/Addon.Episerver.EnvironmentSynchronizer/Configuration/ConfigurationReader.cs index 255ae0e..60058f2 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Configuration/ConfigurationReader.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/Configuration/ConfigurationReader.cs @@ -1,146 +1,198 @@ using Addon.Episerver.EnvironmentSynchronizer.Models; using EPiServer.Logging; -using EPiServer.Web; using System; using System.Collections.Generic; using System.Linq; using System.Globalization; using Microsoft.Extensions.Options; using EPiServer.Security; +#if NET10_0_OR_GREATER +using EPiServer.Applications; +#else +using EPiServer.Web; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Configuration { - public class ConfigurationReader : IConfigurationReader - { - private static readonly ILogger Logger = LogManager.GetLogger(); + public class ConfigurationReader : IConfigurationReader + { + private static readonly ILogger Logger = LogManager.GetLogger(); - private readonly EnvironmentSynchronizerOptions _configuration; + private readonly EnvironmentSynchronizerOptions _configuration; - public ConfigurationReader(IOptions synchronizerConfiguration) + public ConfigurationReader(IOptions synchronizerConfiguration) { - _configuration = synchronizerConfiguration.Value; + _configuration = synchronizerConfiguration.Value; } - public SynchronizationData ReadConfiguration() - { - var syncData = new SynchronizationData(); + public SynchronizationData ReadConfiguration() + { + var syncData = new SynchronizationData(); - if(_configuration == null) + if (_configuration == null) { - return syncData; + return syncData; } - try - { - syncData.RunAsInitializationModule = _configuration.RunAsInitializationModule; - syncData.RunInitializationModuleEveryStartup = _configuration.RunInitializationModuleEveryStartup; - - if (_configuration.SiteDefinitions != null && _configuration.SiteDefinitions.Count > 0) - { - syncData.SiteDefinitions = new List(); - foreach (var options in _configuration.SiteDefinitions) - { - var siteDefinition = CreateEnvironmentSynchronizerSiteDefinition(options); - if (!string.IsNullOrEmpty(siteDefinition.Name) && siteDefinition.SiteUrl != null) - { - syncData.SiteDefinitions.Add(siteDefinition); - } - } - } - else - { - Logger.Information($"Found no site definitions to handle. Missing Name/SiteUrl?"); - } - - if (_configuration.ScheduledJobs != null && _configuration.ScheduledJobs.Count > 0) - { - syncData.ScheduledJobs = new List(); - foreach (var options in _configuration.ScheduledJobs) - { - var job = new ScheduledJobDefinition - { - Id = options.Id, - Name = options.Name, - IsEnabled = options.IsEnabled, - AutoRun = options.AutoRun - }; - syncData.ScheduledJobs.Add(job); - } - } - else - { - Logger.Information($"Found no schedule jobs to handle."); - } - } - catch (ArgumentException argEx) - { - if (argEx.Message.Contains("is not a valid host name")) - { - Logger.Error($"EnvironmentSynchronizer configuration found in the appSettings.json but there are hostnames that is not valid.", argEx); - } else - { - Logger.Error($"EnvironmentSynchronizer configuration found in the appSettings.json but some arguments is not correct.", argEx); - } - - } - catch (Exception ex) - { - Logger.Error($"No configuration found in the appSettings.json. Missing EnvironmentSynchronizer section.", ex); - } - - return syncData; - } - - private EnvironmentSynchronizerSiteDefinition CreateEnvironmentSynchronizerSiteDefinition(SiteDefinitionOptions options) - { - var siteDefinition = new EnvironmentSynchronizerSiteDefinition() - { - Id = string.IsNullOrEmpty(options.Id) ? Guid.Empty : new Guid(options.Id), - Name = string.IsNullOrEmpty(options.Name) ? string.Empty : options.Name, - SiteUrl = string.IsNullOrEmpty(options.SiteUrl) ? null : new Uri(options.SiteUrl), - Hosts = ToHostDefinitions(options.Hosts), - ForceLogin = options.ForceLogin, - SetRoles = options.SetRoles != null ? ToSetRoleDefinitions(options.SetRoles) : new List(), - RemoveRoles = options.RemoveRoles != null ? ToRemoveRoleDefinitions(options.RemoveRoles) : new List() - }; - - return siteDefinition; - } - - private List ToHostDefinitions(IList hosts) - { - return hosts.Select(hostOptions => { - return new HostDefinition - { - Name = hostOptions.Name, - Type = hostOptions.Type != HostDefinitionType.Undefined ? hostOptions.Type : HostDefinitionType.Undefined, - UseSecureConnection = hostOptions.UseSecureConnection, - Language = string.IsNullOrEmpty(hostOptions.Language) ? null : new CultureInfo(hostOptions.Language) - }; - }).ToList(); - } - - private List ToSetRoleDefinitions(IList setRoles) - { - return setRoles.Select(setRoleOptions => - { - return new SetRoleDefinition - { - Name = setRoleOptions.Name, - Access = setRoleOptions.Access != null ? AccessLevelConverter.ConvertToAccessLevel(setRoleOptions.Access) : AccessLevel.NoAccess, - }; - }).ToList(); - } - - private List ToRemoveRoleDefinitions(IList removeRoles) - { - return removeRoles.Select(removeRoleOptions => - { - return new RemoveRoleDefinition - { - Name = removeRoleOptions - }; - }).ToList(); - } - } + try + { + syncData.RunAsInitializationModule = _configuration.RunAsInitializationModule; + syncData.RunInitializationModuleEveryStartup = _configuration.RunInitializationModuleEveryStartup; + + if (_configuration.SiteDefinitions != null && _configuration.SiteDefinitions.Count > 0) + { + syncData.SiteDefinitions = new List(); + foreach (var options in _configuration.SiteDefinitions) + { + var siteDefinition = CreateEnvironmentSynchronizerSiteDefinition(options); +#if NET10_0_OR_GREATER + if (!string.IsNullOrEmpty(siteDefinition.Name)) +#else + if (!string.IsNullOrEmpty(siteDefinition.Name) && siteDefinition.SiteUrl != null) +#endif + { + syncData.SiteDefinitions.Add(siteDefinition); + } + } + } + else + { +#if NET10_0_OR_GREATER + Logger.Information($"Found no site definitions to handle. Missing Name?"); +#else + Logger.Information($"Found no site definitions to handle. Missing Name/SiteUrl?"); +#endif + } + + if (_configuration.ScheduledJobs != null && _configuration.ScheduledJobs.Count > 0) + { + syncData.ScheduledJobs = new List(); + foreach (var options in _configuration.ScheduledJobs) + { + var job = new ScheduledJobDefinition + { + Id = options.Id, + Name = options.Name, + IsEnabled = options.IsEnabled, + AutoRun = options.AutoRun + }; + syncData.ScheduledJobs.Add(job); + } + } + else + { + Logger.Information($"Found no schedule jobs to handle."); + } + } + catch (ArgumentException argEx) + { + if (argEx.Message.Contains("is not a valid host name")) + { + Logger.Error($"EnvironmentSynchronizer configuration found in the appSettings.json but there are hostnames that is not valid.", argEx); + } + else + { + Logger.Error($"EnvironmentSynchronizer configuration found in the appSettings.json but some arguments is not correct.", argEx); + } + + } + catch (Exception ex) + { + Logger.Error($"No configuration found in the appSettings.json. Missing EnvironmentSynchronizer section.", ex); + } + + return syncData; + } + + private EnvironmentSynchronizerSiteDefinition CreateEnvironmentSynchronizerSiteDefinition(SiteDefinitionOptions options) + { + var siteDefinition = new EnvironmentSynchronizerSiteDefinition() + { +#if NET10_0_OR_GREATER + Id = string.IsNullOrEmpty(options.Id) ? string.Empty : options.Id, +#else + Id = string.IsNullOrEmpty(options.Id) ? Guid.Empty : new Guid(options.Id), + SiteUrl = string.IsNullOrEmpty(options.SiteUrl) ? null : new Uri(options.SiteUrl), +#endif + Name = string.IsNullOrEmpty(options.Name) ? string.Empty : options.Name, + Hosts = ToHostDefinitions(options.Hosts), +#if NET10_0_OR_GREATER + IsDefault = options.IsDefault, +#endif + ForceLogin = options.ForceLogin, + SetRoles = options.SetRoles != null ? ToSetRoleDefinitions(options.SetRoles) : new List(), + RemoveRoles = options.RemoveRoles != null ? ToRemoveRoleDefinitions(options.RemoveRoles) : new List() + }; + + return siteDefinition; + } + +#if NET10_0_OR_GREATER + private List ToHostDefinitions(IList hosts) + { + return hosts + .Select(hostOptions => new ApplicationHost(hostOptions.Name) + { + Type = hostOptions.Type, + PreferredUrlScheme = hostOptions.UseSecureConnection ? UrlScheme.Https : UrlScheme.Http, + Locale = GetApplicationHostLocale(hostOptions) + }) + .ToList(); + } + + private static CultureInfo GetApplicationHostLocale(HostOptions hostOptions) + { + if (string.IsNullOrEmpty(hostOptions.Language) || !SupportsLocale(hostOptions.Type)) + { + return null; + } + + return new CultureInfo(hostOptions.Language); + } + + private static bool SupportsLocale(ApplicationHostType hostType) + { + return hostType == ApplicationHostType.Primary || + hostType == ApplicationHostType.Default || + hostType == ApplicationHostType.RedirectPermanent || + hostType == ApplicationHostType.RedirectTemporary; + } +#else + private List ToHostDefinitions(IList hosts) + { + return hosts.Select(hostOptions => { + return new HostDefinition + { + Name = hostOptions.Name, + Type = hostOptions.Type != HostDefinitionType.Undefined ? hostOptions.Type : HostDefinitionType.Undefined, + UseSecureConnection = hostOptions.UseSecureConnection, + Language = string.IsNullOrEmpty(hostOptions.Language) ? null : new CultureInfo(hostOptions.Language) + }; + }).ToList(); + } +#endif + + private List ToSetRoleDefinitions(IList setRoles) + { + return setRoles.Select(setRoleOptions => + { + return new SetRoleDefinition + { + Name = setRoleOptions.Name, + Access = setRoleOptions.Access != null ? AccessLevelConverter.ConvertToAccessLevel(setRoleOptions.Access) : AccessLevel.NoAccess, + }; + }).ToList(); + } + + private List ToRemoveRoleDefinitions(IList removeRoles) + { + return removeRoles.Select(removeRoleOptions => + { + return new RemoveRoleDefinition + { + Name = removeRoleOptions + }; + }).ToList(); + } + } } diff --git a/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerOptions.cs b/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerOptions.cs index bd3a027..930eb3f 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerOptions.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerOptions.cs @@ -1,6 +1,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +#if NET10_0_OR_GREATER +using EPiServer.Applications; +#else using EPiServer.Web; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Configuration { @@ -10,9 +14,9 @@ public class EnvironmentSynchronizerOptions public bool RunAsInitializationModule { get; set; } - public bool RunInitializationModuleEveryStartup { get; set;} + public bool RunInitializationModuleEveryStartup { get; set; } - public IList SiteDefinitions { get; set; } + public IList SiteDefinitions { get; set; } public IList ScheduledJobs { get; set; } @@ -40,18 +44,21 @@ public class SiteDefinitionOptions [Required] public string Name { get; set; } +#if NET10_0_OR_GREATER + public bool? IsDefault { get; set; } +#else [Required] public string SiteUrl { get; set; } +#endif + public bool ForceLogin { get; set; } - public bool ForceLogin { get; set; } - - [Required] + [Required] public IList Hosts { get; set; } - public IList SetRoles { get; set; } + public IList SetRoles { get; set; } - public IList RemoveRoles { get; set; } - } + public IList RemoveRoles { get; set; } + } public class HostOptions { @@ -59,7 +66,11 @@ public class HostOptions public bool UseSecureConnection { get; set; } +#if NET10_0_OR_GREATER + public ApplicationHostType Type { get; set; } +#else public HostDefinitionType Type { get; set; } +#endif public string Language { get; set; } } diff --git a/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerSiteDefinition.cs b/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerSiteDefinition.cs index e7adc94..805b013 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerSiteDefinition.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/Configuration/EnvironmentSynchronizerSiteDefinition.cs @@ -1,26 +1,42 @@ +using System; +using System.Collections.Generic; using EPiServer.Security; +#if NET10_0_OR_GREATER +using EPiServer.Applications; +#else using EPiServer.Web; -using System.Collections.Generic; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Configuration { - public class EnvironmentSynchronizerSiteDefinition : SiteDefinition - { - public bool ForceLogin { get; set; } - public IEnumerable SetRoles { get; set; } - public IEnumerable RemoveRoles { get; set; } - } +#if NET10_0_OR_GREATER + public class EnvironmentSynchronizerSiteDefinition + { + public string Id { get; set; } - public class SetRoleDefinition - { - public string Name { get; set; } + public string Name { get; set; } - public AccessLevel Access { get; set;} - } + public IList Hosts { get; set; } - public class RemoveRoleDefinition + public bool? IsDefault { get; set; } +#else + public class EnvironmentSynchronizerSiteDefinition : SiteDefinition { - public string Name { get; set; } - } +#endif + public bool ForceLogin { get; set; } + public IEnumerable SetRoles { get; set; } + public IEnumerable RemoveRoles { get; set; } + } + + public class SetRoleDefinition + { + public string Name { get; set; } + + public AccessLevel Access { get; set; } + } + public class RemoveRoleDefinition + { + public string Name { get; set; } + } } diff --git a/Addon.Episerver.EnvironmentSynchronizer/EnvironmentSynchronizationManager.cs b/Addon.Episerver.EnvironmentSynchronizer/EnvironmentSynchronizationManager.cs index 24bb5d1..ffa8bf8 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/EnvironmentSynchronizationManager.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/EnvironmentSynchronizationManager.cs @@ -1,6 +1,5 @@ using System; using EPiServer.Logging; -using EPiServer.ServiceLocation; using System.Collections.Generic; using System.Linq; using System.Text; @@ -22,12 +21,16 @@ public class EnvironmentSynchronizationManager : IEnvironmentSynchronizationMana private static readonly ILogger Logger = LogManager.GetLogger(); private readonly IEnumerable _environmentSynchronizers; private readonly IEnvironmentSynchronizationStore _environmentSynchronizationStore; + private readonly IEnvironmentNameSource _environmentNameSource; public EnvironmentSynchronizationManager( - IEnumerable environmentSynchronizers, IEnvironmentSynchronizationStore environmentSynchronizationStore) + IEnumerable environmentSynchronizers, + IEnvironmentSynchronizationStore environmentSynchronizationStore, + IEnvironmentNameSource environmentNameSource = null) { _environmentSynchronizers = environmentSynchronizers; _environmentSynchronizationStore = environmentSynchronizationStore; + _environmentNameSource = environmentNameSource; } public string Synchronize() @@ -72,11 +75,9 @@ public string Synchronize(string environmentName) return resultLog.ToString(); } - // TODO: Verify that this still works public string GetEnvironmentName() { - ServiceLocator.Current.TryGetExistingInstance(out var environmentNameSource); - var environmentName = environmentNameSource != null ? environmentNameSource.GetCurrentEnvironmentName() : string.Empty; + var environmentName = _environmentNameSource != null ? _environmentNameSource.GetCurrentEnvironmentName() : string.Empty; if (string.IsNullOrEmpty(environmentName)) { diff --git a/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/InitializationExecuter.cs b/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/InitializationExecuter.cs index 3d0cdca..dfa0685 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/InitializationExecuter.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/InitializationExecuter.cs @@ -4,9 +4,11 @@ using Addon.Episerver.EnvironmentSynchronizer.Jobs; using EPiServer.DataAbstraction; using EPiServer.Logging; -using EPiServer.PlugIn; using EPiServer.Scheduler; using EPiServer.ServiceLocation; +#if !NET10_0_OR_GREATER +using EPiServer.PlugIn; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.InitializationModule { @@ -64,10 +66,17 @@ public void Initialize() try { +#if NET10_0_OR_GREATER + var jobId = + ((ScheduledJobAttribute)typeof(EnvironmentSynchronizationJob).GetCustomAttributes( + typeof(ScheduledJobAttribute), true)[0]).GetGUID(); + var job = jobId.HasValue ? _scheduledJobRepository.Get(jobId.Value) : null; +#else var jobId = ((ScheduledPlugInAttribute)typeof(EnvironmentSynchronizationJob).GetCustomAttributes( typeof(ScheduledPlugInAttribute), true)[0]).GUID; var job = _scheduledJobRepository.Get(Guid.Parse(jobId)); +#endif if (job is not null) { diff --git a/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/SynchronizationInitializationModule.cs b/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/SynchronizationInitializationModule.cs index 5b3ee7c..f8a4558 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/SynchronizationInitializationModule.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/InitializationModule/SynchronizationInitializationModule.cs @@ -1,8 +1,12 @@ using EPiServer.Framework; using EPiServer.Framework.Initialization; using EPiServer.Logging; -using EPiServer.ServiceLocation; using System; +#if NET10_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +#else +using EPiServer.ServiceLocation; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.InitializationModule { @@ -17,11 +21,14 @@ public void Initialize(InitializationEngine context) { try { - var _executer = ServiceLocator.Current.GetInstance(); +#if NET10_0_OR_GREATER + var executer = context.Services.GetRequiredService(); +#else + var executer = ServiceLocator.Current.GetInstance(); +#endif Logger.Information($"InitializableModule:SynchronizationInitializationModule Initialize"); - _executer.Initialize(); - + executer.Initialize(); } catch (InvalidOperationException inOpEx) { @@ -43,7 +50,5 @@ public void Initialize(InitializationEngine context) public void Preload(string[] parameters) { } public void Uninitialize(InitializationEngine context) { } - - } } diff --git a/Addon.Episerver.EnvironmentSynchronizer/Jobs/EnvironmentSynchronizationJob.cs b/Addon.Episerver.EnvironmentSynchronizer/Jobs/EnvironmentSynchronizationJob.cs index b07e405..8607e79 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Jobs/EnvironmentSynchronizationJob.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/Jobs/EnvironmentSynchronizationJob.cs @@ -2,52 +2,58 @@ using System.Diagnostics; using System.Text; using EPiServer.Logging; -using EPiServer.PlugIn; using EPiServer.Scheduler; +#if !NET10_0_OR_GREATER +using EPiServer.PlugIn; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Jobs { +#if NET10_0_OR_GREATER + [ScheduledJob( +#else [ScheduledPlugIn( - DisplayName = "Environment Synchronization", - Description = "Ensures that content and settings that are stored in the databases are corrected given the current environment. This is helpful after a content synchronization between different Optimizely CMS environments. https://github.com/Epinova/Addon.Episerver.EnvironmentSynchronizer", SortIndex = 100, - GUID = "1eda8c91-a367-41df-adee-e6143b1e37c3") - ] - public class EnvironmentSynchronizationJob : ScheduledJobBase - { - private static readonly ILogger Logger = LogManager.GetLogger(); - private readonly IEnvironmentSynchronizationManager _environmentSynchronizationManager; - - public EnvironmentSynchronizationJob(IEnvironmentSynchronizationManager environmentSynchronizationManager) - { - IsStoppable = false; - _environmentSynchronizationManager = environmentSynchronizationManager; - } - - private long Duration { get; set; } - - public override string Execute() - { - var tmr = Stopwatch.StartNew(); - var resultLog = new StringBuilder(); - - try - { - resultLog.AppendLine(_environmentSynchronizationManager.Synchronize()) ; - } - catch (Exception ex) - { - Logger.Error("Error when run Environment Synchronization job.", ex); - resultLog.AppendLine($"Environment synchronization job failed. More information in the log file."); - } - - tmr.Stop(); - Duration = tmr.ElapsedMilliseconds; - - resultLog.AppendLine( - $"Ran environment synchronization job. {Duration}ms on {Environment.MachineName}."); - - return resultLog.ToString(); - } - } +#endif + DisplayName = "Environment Synchronization", + Description = "Ensures that content and settings that are stored in the databases are corrected given the current environment. This is helpful after a content synchronization between different Optimizely CMS environments. https://github.com/Epinova/Addon.Episerver.EnvironmentSynchronizer", + GUID = "1eda8c91-a367-41df-adee-e6143b1e37c3") + ] + public class EnvironmentSynchronizationJob : ScheduledJobBase + { + private static readonly ILogger Logger = LogManager.GetLogger(); + private readonly IEnvironmentSynchronizationManager _environmentSynchronizationManager; + + public EnvironmentSynchronizationJob(IEnvironmentSynchronizationManager environmentSynchronizationManager) + { + IsStoppable = false; + _environmentSynchronizationManager = environmentSynchronizationManager; + } + + private long Duration { get; set; } + + public override string Execute() + { + var tmr = Stopwatch.StartNew(); + var resultLog = new StringBuilder(); + + try + { + resultLog.AppendLine(_environmentSynchronizationManager.Synchronize()); + } + catch (Exception ex) + { + Logger.Error("Error when run Environment Synchronization job.", ex); + resultLog.AppendLine($"Environment synchronization job failed. More information in the log file."); + } + + tmr.Stop(); + Duration = tmr.ElapsedMilliseconds; + + resultLog.AppendLine( + $"Ran environment synchronization job. {Duration}ms on {Environment.MachineName}."); + + return resultLog.ToString(); + } + } } diff --git a/Addon.Episerver.EnvironmentSynchronizer/Synchronizers/SiteDefinitions/SiteDefinitionSynchronizer.cs b/Addon.Episerver.EnvironmentSynchronizer/Synchronizers/SiteDefinitions/SiteDefinitionSynchronizer.cs index 4447fb1..be93d57 100644 --- a/Addon.Episerver.EnvironmentSynchronizer/Synchronizers/SiteDefinitions/SiteDefinitionSynchronizer.cs +++ b/Addon.Episerver.EnvironmentSynchronizer/Synchronizers/SiteDefinitions/SiteDefinitionSynchronizer.cs @@ -3,47 +3,56 @@ using EPiServer.Logging; using EPiServer.Security; using EPiServer.ServiceLocation; -using EPiServer.Web; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; +#if NET10_0_OR_GREATER +using System.Threading; +using EPiServer.Applications; +using WebsiteDefinition = EPiServer.Applications.Application; +using WebsiteRepository = EPiServer.Applications.IApplicationRepository; +#else +using WebsiteDefinition = EPiServer.Web.SiteDefinition; +using WebsiteRepository = EPiServer.Web.ISiteDefinitionRepository; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Synchronizers.SiteDefinitions { - [ServiceConfiguration(typeof(IEnvironmentSynchronizer))] + [ServiceConfiguration(typeof(IEnvironmentSynchronizer))] public class SiteDefinitionSynchronizer : IEnvironmentSynchronizer { +#if NET10_0_OR_GREATER + private const string WebsiteDefinitionType = "application"; +#else + private const string WebsiteDefinitionType = "site"; +#endif private static readonly ILogger Logger = LogManager.GetLogger(); - private readonly ISiteDefinitionRepository _siteDefinitionRepository; - private readonly IContentSecurityRepository _contentSecurityRepository; - private readonly IConfigurationReader _configurationReader; + private readonly WebsiteRepository _websiteRepository; + private readonly IContentSecurityRepository _contentSecurityRepository; + private readonly IConfigurationReader _configurationReader; private StringBuilder resultLog = new StringBuilder(); - private string _environmentName = string.Empty; - - public SiteDefinitionSynchronizer( - ISiteDefinitionRepository siteDefinitionRepository, + public SiteDefinitionSynchronizer( + WebsiteRepository websiteRepository, IContentSecurityRepository contentSecurityRepository, IConfigurationReader configurationReader) { - Logger.Information("SiteDefinitionSynchronizer initialized."); - _siteDefinitionRepository = siteDefinitionRepository; + Logger.Information("SiteDefinitionSynchronizer initialized."); + _websiteRepository = websiteRepository; _contentSecurityRepository = contentSecurityRepository; _configurationReader = configurationReader; } public string Synchronize(string environmentName) { - _environmentName = environmentName; - var syncConfiguration = _configurationReader.ReadConfiguration(); if (syncConfiguration.SiteDefinitions == null || !syncConfiguration.SiteDefinitions.Any()) { - Logger.Information("No site definitions found to synchronize."); - resultLog.AppendLine("No site definitions found to synchronize.
"); + Logger.Information($"No {WebsiteDefinitionType} definitions found to synchronize."); + resultLog.AppendLine($"No {WebsiteDefinitionType} definitions found to synchronize.
"); return resultLog.ToString(); } @@ -53,17 +62,17 @@ public string Synchronize(string environmentName) { var updatedSites = MergeSiteDefinitions(syncConfiguration.SiteDefinitions); - Logger.Information($"Updated total of {updatedSites} sites."); - resultLog.AppendLine($"Updated total of {updatedSites} sites.
"); + Logger.Information($"Updated total of {updatedSites} {WebsiteDefinitionType}s."); + resultLog.AppendLine($"Updated total of {updatedSites} {WebsiteDefinitionType}s.
"); } catch (Exception ex) { - Logger.Error("An exception occured when trying to synchronize site definitions", ex); - resultLog.AppendLine($"An exception occured when trying to synchronize site definitions: {ex.Message}
"); + Logger.Error($"An exception occured when trying to synchronize {WebsiteDefinitionType} definitions", ex); + resultLog.AppendLine($"An exception occured when trying to synchronize {WebsiteDefinitionType} definitions: {ex.Message}
"); } stopwatch.Stop(); - Logger.Information($"Synchronize site definitions took {stopwatch.ElapsedMilliseconds}ms."); + Logger.Information($"Synchronize {WebsiteDefinitionType} definitions took {stopwatch.ElapsedMilliseconds}ms."); return resultLog.ToString(); } @@ -71,159 +80,253 @@ public string Synchronize(string environmentName) private int MergeSiteDefinitions(IEnumerable siteDefinitionsToUpdate) { var updatedSites = 0; - var existingSites = _siteDefinitionRepository.List(); +#if NET10_0_OR_GREATER + var existingSites = _websiteRepository.List().Where(application => application is IRoutableApplication); +#else + var existingSites = _websiteRepository.List(); +#endif foreach (var siteDefinitionToUpdate in siteDefinitionsToUpdate) { - SiteDefinition site = GetExistingSiteDefinition(existingSites, siteDefinitionToUpdate); + WebsiteDefinition site = GetExistingSiteDefinition(existingSites, siteDefinitionToUpdate); if (site != null) { - UpdateSiteDefinitionValues(site, siteDefinitionToUpdate); - updatedSites++; + UpdateSiteDefinitionValues(site, siteDefinitionToUpdate); + updatedSites++; - UpdateSitePermissions(site, siteDefinitionToUpdate); - } + UpdateSitePermissions(site, siteDefinitionToUpdate); + } else { - Logger.Warning($"Could not find site {siteDefinitionToUpdate.Name} or site already has site URL {siteDefinitionToUpdate.SiteUrl}."); - resultLog.AppendLine($"Could not find site {siteDefinitionToUpdate.Name} or site already has site URL {siteDefinitionToUpdate.SiteUrl}.
"); + Logger.Warning($"Could not find {WebsiteDefinitionType} {siteDefinitionToUpdate.Name}."); + resultLog.AppendLine($"Could not find {WebsiteDefinitionType} {siteDefinitionToUpdate.Name}.
"); } } return updatedSites; } - private void UpdateSiteDefinitionValues(SiteDefinition site, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) + private void UpdateSiteDefinitionValues(WebsiteDefinition site, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) { +#if NET10_0_OR_GREATER + var writableApplication = site.CreateWritableClone(); + var writableSite = (IRoutableApplication)writableApplication; + + if (!string.IsNullOrEmpty(siteDefinitionToUpdate.Name) && + !string.Equals(writableApplication.DisplayName, siteDefinitionToUpdate.Name, StringComparison.Ordinal)) + { + writableApplication.DisplayName = siteDefinitionToUpdate.Name; + } + + var supportedHosts = GetSupportedHosts(site, siteDefinitionToUpdate.Hosts); + writableSite.Hosts.Clear(); + + foreach (var host in supportedHosts) + { + writableSite.Hosts.Add(host); + } + _websiteRepository.SaveAsync(writableApplication, CancellationToken.None).GetAwaiter().GetResult(); + + if (siteDefinitionToUpdate.IsDefault is not null && + ((IRoutableApplication)site).IsDefault != siteDefinitionToUpdate.IsDefault.Value) + { + _websiteRepository.MakeDefaultAsync(writableSite, siteDefinitionToUpdate.IsDefault.Value, CancellationToken.None).GetAwaiter().GetResult(); + } +#else site = site.CreateWritableClone(); if (!string.IsNullOrEmpty(siteDefinitionToUpdate.Name) && site.Name != siteDefinitionToUpdate.Name) { - // Will set the name of the site to the provided Name if Id exist and Name is specified and different from the found existing site. + // Will set the name to the provided Name if Id exists and Name is specified and different from the found definition. site.Name = siteDefinitionToUpdate.Name; } site.SiteUrl = siteDefinitionToUpdate.SiteUrl; site.Hosts = siteDefinitionToUpdate.Hosts; - _siteDefinitionRepository.Save(site); - Logger.Information($"Updated {siteDefinitionToUpdate.Name} to site URL {siteDefinitionToUpdate.SiteUrl} and {siteDefinitionToUpdate.Hosts.Count} hostnames."); - resultLog.AppendLine($"Updated {siteDefinitionToUpdate.Name} to site URL {siteDefinitionToUpdate.SiteUrl} and {siteDefinitionToUpdate.Hosts.Count} hostnames.
"); + _websiteRepository.Save(site); +#endif +#if NET10_0_OR_GREATER + Logger.Information($"Updated {siteDefinitionToUpdate.Name} {WebsiteDefinitionType} with {supportedHosts.Count} hostnames."); + resultLog.AppendLine($"Updated {siteDefinitionToUpdate.Name} {WebsiteDefinitionType} with {supportedHosts.Count} hostnames.
"); +#else + Logger.Information($"Updated {siteDefinitionToUpdate.Name} to {WebsiteDefinitionType} URL {siteDefinitionToUpdate.SiteUrl} and {siteDefinitionToUpdate.Hosts.Count} hostnames."); + resultLog.AppendLine($"Updated {siteDefinitionToUpdate.Name} to {WebsiteDefinitionType} URL {siteDefinitionToUpdate.SiteUrl} and {siteDefinitionToUpdate.Hosts.Count} hostnames.
"); +#endif - } + } - public void UpdateSitePermissions(SiteDefinition site, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) - { - Logger.Debug($"UpdateSitePermissions"); - var siteStartPageContentLink = site.StartPage; +#if NET10_0_OR_GREATER + private List GetSupportedHosts(WebsiteDefinition site, IEnumerable hosts) + { + var supportedHosts = new List(); - if (siteDefinitionToUpdate.ForceLogin || (siteDefinitionToUpdate.SetRoles != null && siteDefinitionToUpdate.SetRoles.Any()) || (siteDefinitionToUpdate.RemoveRoles != null && siteDefinitionToUpdate.RemoveRoles.Any()) && siteStartPageContentLink != null) - { - IContentSecurityDescriptor securityDescriptor = (IContentSecurityDescriptor)_contentSecurityRepository.Get(siteStartPageContentLink).CreateWritableClone(); + foreach (var host in hosts ?? []) + { + if (IsSupportedHostType(site, host.Type)) + { + supportedHosts.Add(host); + } + else + { + Logger.Warning($"Skipping host {host.Authority} with type {host.Type} because it is not supported by {site.GetType().Name} {site.Name}."); + } + } - if (securityDescriptor != null) - { - if (securityDescriptor.IsInherited) - { - securityDescriptor.IsInherited = false; - } + return supportedHosts; + } - var existingEntries = GetExistingAce(securityDescriptor); + private static bool IsSupportedHostType(WebsiteDefinition site, ApplicationHostType hostType) + { + if (site is Website) + { + return hostType == ApplicationHostType.Primary || + hostType == ApplicationHostType.Preview || + hostType == ApplicationHostType.Media; + } - if (siteDefinitionToUpdate.SetRoles != null && siteDefinitionToUpdate.SetRoles.Any()) - { - SetRoles(existingEntries, siteDefinitionToUpdate); - } + if (site is InProcessWebsite) + { + return hostType == ApplicationHostType.Primary || + hostType == ApplicationHostType.Default || + hostType == ApplicationHostType.Edit || + hostType == ApplicationHostType.RedirectPermanent || + hostType == ApplicationHostType.RedirectTemporary || + hostType == ApplicationHostType.Media; + } - if (siteDefinitionToUpdate.RemoveRoles != null && siteDefinitionToUpdate.RemoveRoles.Any()) - { - RemoveRoles(existingEntries, siteDefinitionToUpdate); - } + return false; + } +#endif - if (siteDefinitionToUpdate.ForceLogin) - { - Logger.Debug($"Start ForceLogin."); - RemoveRole(existingEntries, new RemoveRoleDefinition { Name = "Everyone" } , siteDefinitionToUpdate.Name); - } + public void UpdateSitePermissions(WebsiteDefinition website, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) + { + Logger.Debug($"UpdateSitePermissions"); +#if NET10_0_OR_GREATER + var siteStartPageContentLink = ((IRoutableApplication)website).EntryPoint; +#else + var siteStartPageContentLink = website.StartPage; +#endif + + if (siteStartPageContentLink != null && + (siteDefinitionToUpdate.ForceLogin || + (siteDefinitionToUpdate.SetRoles != null && siteDefinitionToUpdate.SetRoles.Any()) || + (siteDefinitionToUpdate.RemoveRoles != null && siteDefinitionToUpdate.RemoveRoles.Any()))) + { + IContentSecurityDescriptor securityDescriptor = (IContentSecurityDescriptor)_contentSecurityRepository.Get(siteStartPageContentLink).CreateWritableClone(); - SetAce(securityDescriptor, existingEntries); + if (securityDescriptor != null) + { + if (securityDescriptor.IsInherited) + { + securityDescriptor.IsInherited = false; + } - _contentSecurityRepository.Save(siteStartPageContentLink, securityDescriptor, SecuritySaveType.Replace); - _contentSecurityRepository.Save(siteStartPageContentLink, securityDescriptor, SecuritySaveType.ReplaceChildPermissions); + var existingEntries = GetExistingAce(securityDescriptor); - } else { - Logger.Error($"Could not get a security descriptor from site {site.Name} startpage."); - } - } - } + if (siteDefinitionToUpdate.SetRoles != null && siteDefinitionToUpdate.SetRoles.Any()) + { + SetRoles(existingEntries, siteDefinitionToUpdate); + } - private SiteDefinition GetExistingSiteDefinition(IEnumerable existingSites, SiteDefinition siteDefinitionToUpdate) - { - SiteDefinition siteDefinition = null; + if (siteDefinitionToUpdate.RemoveRoles != null && siteDefinitionToUpdate.RemoveRoles.Any()) + { + RemoveRoles(existingEntries, siteDefinitionToUpdate); + } + if (siteDefinitionToUpdate.ForceLogin) + { + Logger.Debug($"Start ForceLogin."); + RemoveRole(existingEntries, new RemoveRoleDefinition { Name = "Everyone" }, siteDefinitionToUpdate.Name); + } + + SetAce(securityDescriptor, existingEntries); + + _contentSecurityRepository.Save(siteStartPageContentLink, securityDescriptor, SecuritySaveType.Replace); + _contentSecurityRepository.Save(siteStartPageContentLink, securityDescriptor, SecuritySaveType.ReplaceChildPermissions); + + } + else + { + Logger.Error($"Could not get a security descriptor from {WebsiteDefinitionType} {website.Name} startpage."); + } + } + } + + private WebsiteDefinition GetExistingSiteDefinition(IEnumerable existingSites, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) + { +#if NET10_0_OR_GREATER + if (string.IsNullOrEmpty(siteDefinitionToUpdate.Id)) + { + return existingSites.FirstOrDefault(application => application.DisplayName == siteDefinitionToUpdate.Name); + } + + return existingSites.FirstOrDefault(application => application.Name == siteDefinitionToUpdate.Id); +#else + WebsiteDefinition siteDefinition = null; if (siteDefinitionToUpdate.Id != Guid.Empty) { - //Update the site definition if it doesn't have the same value for SiteUrl + // Update the definition if it doesn't have the same value for SiteUrl. siteDefinition = existingSites.FirstOrDefault(s => s.Id == siteDefinitionToUpdate.Id); } else { - //Update the site definition if it doesn't have the same value for SiteUrl + // Update the definition if it doesn't have the same value for SiteUrl. siteDefinition = existingSites.FirstOrDefault(s => s.Name == siteDefinitionToUpdate.Name); } return siteDefinition; +#endif } - private List GetExistingAce(IContentSecurityDescriptor securityDescriptor) - { - return securityDescriptor.Entries.Select(x => x).ToList(); - } - - private void SetAce(IContentSecurityDescriptor securityDescriptor, IEnumerable existingEntries) - { - securityDescriptor.Clear(); - foreach (var entry in existingEntries) - { - securityDescriptor.AddEntry(entry); - } - } + private List GetExistingAce(IContentSecurityDescriptor securityDescriptor) + { + return securityDescriptor.Entries.Select(x => x).ToList(); + } - private void SetRoles(List existingEntries, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) - { - Logger.Debug($"Start SetRoles."); - foreach (var role in siteDefinitionToUpdate.SetRoles) - { - var existingRole = existingEntries.Where(x => x.Name == role.Name).FirstOrDefault(); - if (existingRole != null) - { - existingEntries.Remove(existingRole); - Logger.Debug($"RemoveRole {existingRole.Name}."); - } - existingEntries.Add(new AccessControlEntry(role.Name, role.Access, SecurityEntityType.Role)); - Logger.Debug($"SetRole {role.Name} {role.Access}."); - Logger.Information($"Set AccessControlEntry {role.Name} AccessLevel.{role.Access} for site {siteDefinitionToUpdate.Name}."); - resultLog.AppendLine($"Set AccessControlEntry {role.Name} AccessLevel.{role.Access} for site {siteDefinitionToUpdate.Name}.
"); - } - } + private void SetAce(IContentSecurityDescriptor securityDescriptor, IEnumerable existingEntries) + { + securityDescriptor.Clear(); + foreach (var entry in existingEntries) + { + securityDescriptor.AddEntry(entry); + } + } - private void RemoveRoles(List existingEntries, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) - { - Logger.Debug($"Start RemoveRoles."); - foreach (var role in siteDefinitionToUpdate.RemoveRoles) - { - RemoveRole(existingEntries, role, siteDefinitionToUpdate.Name); - } - } - private void RemoveRole(List existingEntries, RemoveRoleDefinition removeRoleDefinition, string siteName) - { - var existingRole = existingEntries.Where(x => x.Name == removeRoleDefinition.Name).FirstOrDefault(); - if (existingRole != null) - { - existingEntries.Remove(existingRole); - Logger.Debug($"RemoveRole {existingRole.Name}."); - Logger.Information($"Remove AccessControlEntry {removeRoleDefinition.Name} AccessLevel.{existingRole.Access} for site {siteName}."); - resultLog.AppendLine($"Remove AccessControlEntry {removeRoleDefinition.Name} AccessLevel.{existingRole.Access} for site {siteName}.
"); - } - } + private void SetRoles(List existingEntries, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) + { + Logger.Debug($"Start SetRoles."); + foreach (var role in siteDefinitionToUpdate.SetRoles) + { + var existingRole = existingEntries.Where(x => x.Name == role.Name).FirstOrDefault(); + if (existingRole != null) + { + existingEntries.Remove(existingRole); + Logger.Debug($"RemoveRole {existingRole.Name}."); + } + existingEntries.Add(new AccessControlEntry(role.Name, role.Access, SecurityEntityType.Role)); + Logger.Debug($"SetRole {role.Name} {role.Access}."); + Logger.Information($"Set AccessControlEntry {role.Name} AccessLevel.{role.Access} for {WebsiteDefinitionType} {siteDefinitionToUpdate.Name}."); + resultLog.AppendLine($"Set AccessControlEntry {role.Name} AccessLevel.{role.Access} for {WebsiteDefinitionType} {siteDefinitionToUpdate.Name}.
"); + } + } + private void RemoveRoles(List existingEntries, EnvironmentSynchronizerSiteDefinition siteDefinitionToUpdate) + { + Logger.Debug($"Start RemoveRoles."); + foreach (var role in siteDefinitionToUpdate.RemoveRoles) + { + RemoveRole(existingEntries, role, siteDefinitionToUpdate.Name); + } + } + private void RemoveRole(List existingEntries, RemoveRoleDefinition removeRoleDefinition, string siteName) + { + var existingRole = existingEntries.Where(x => x.Name == removeRoleDefinition.Name).FirstOrDefault(); + if (existingRole != null) + { + existingEntries.Remove(existingRole); + Logger.Debug($"RemoveRole {existingRole.Name}."); + Logger.Information($"Remove AccessControlEntry {removeRoleDefinition.Name} AccessLevel.{existingRole.Access} for {WebsiteDefinitionType} {siteName}."); + resultLog.AppendLine($"Remove AccessControlEntry {removeRoleDefinition.Name} AccessLevel.{existingRole.Access} for {WebsiteDefinitionType} {siteName}.
"); + } + } } } diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Addon.Episerver.EnvironmentSynchronizer.Test.csproj b/Addon.Episever.EnvironmentSynchronizer.Test/Addon.Episerver.EnvironmentSynchronizer.Test.csproj index 38e3f9f..ef9034f 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Addon.Episerver.EnvironmentSynchronizer.Test.csproj +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Addon.Episerver.EnvironmentSynchronizer.Test.csproj @@ -1,58 +1,58 @@  - - net6.0;net7.0;net8.0;net10.0 - - - true - - - - - - - Always - - - Always - - - Always - - - - - - + + net6.0;net7.0;net8.0;net10.0 + + + true + + + + + + + Always + + + Always + + + Always + + + + + + - + + + + - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - \ No newline at end of file + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Configuration/ConfigurationReaderTests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Configuration/ConfigurationReaderTests.cs index f389ea1..136ef01 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Configuration/ConfigurationReaderTests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Configuration/ConfigurationReaderTests.cs @@ -39,7 +39,9 @@ public void ReadConfiguration_SiteDefinition_All_Settings_Test() options.SiteDefinitions.Should().HaveCount(1); options.SiteDefinitions[0].Id.Should().Be("6AAEAF2F-20F9-41EB-8260-D0BBA76DB141"); options.SiteDefinitions[0].Name.Should().Be("CustomerX"); +#if !NET10_0_OR_GREATER options.SiteDefinitions[0].SiteUrl.Should().Be("https://custxmstr972znb5prep.azurewebsites.net/"); +#endif options.SiteDefinitions[0].Hosts.Should().HaveCount(2); options.SiteDefinitions[0].ForceLogin.Should().BeTrue(); @@ -148,4 +150,4 @@ public static EnvironmentSynchronizerOptions GetConfiguration(string name) } -} \ No newline at end of file +} diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/EnvironmentSynchronizationJobTests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/EnvironmentSynchronizationJobTests.cs index a1c2984..87e675d 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/EnvironmentSynchronizationJobTests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/EnvironmentSynchronizationJobTests.cs @@ -1,6 +1,11 @@ using Xunit; +using System; using Addon.Episerver.EnvironmentSynchronizer.Jobs; +#if NET10_0_OR_GREATER +using EPiServer.Scheduler; +#else using EPiServer.PlugIn; +#endif namespace Addon.Episerver.EnvironmentSynchronizer.Test { @@ -9,11 +14,19 @@ public class EnvironmentSynchronizationJobTests [Fact] public void GetEnvironmentSynchronizationJobGuidAttribute() { +#if NET10_0_OR_GREATER + var jobId = + ((ScheduledJobAttribute) typeof(EnvironmentSynchronizationJob).GetCustomAttributes( + typeof(ScheduledJobAttribute), true)[0]).GetGUID(); + + Assert.Equal(Guid.Parse("1eda8c91-a367-41df-adee-e6143b1e37c3"), jobId); +#else var jobId = (string) ((ScheduledPlugInAttribute) typeof(EnvironmentSynchronizationJob).GetCustomAttributes( typeof(ScheduledPlugInAttribute), true)[0]).GUID; Assert.Equal("1eda8c91-a367-41df-adee-e6143b1e37c3", jobId.ToString()); +#endif } } } diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/ApplicationSynchronizationTests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/ApplicationSynchronizationTests.cs new file mode 100644 index 0000000..c6dfe78 --- /dev/null +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/ApplicationSynchronizationTests.cs @@ -0,0 +1,258 @@ +#if NET10_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Addon.Episerver.EnvironmentSynchronizer.Configuration; +using Addon.Episerver.EnvironmentSynchronizer.Models; +using Addon.Episerver.EnvironmentSynchronizer.Synchronizers.SiteDefinitions; +using EPiServer.Applications; +using EPiServer.Core; +using EPiServer.DataAbstraction; +using EPiServer.Security; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Addon.Episever.EnvironmentSynchronizer.Test.Synchronizers.SiteDefinitions +{ + public class ApplicationSynchronizationTests + { + [Fact] + public void ReadConfiguration_MapsApplicationHostsWithoutSiteUrlFallback() + { + var options = new EnvironmentSynchronizerOptions + { + SiteDefinitions = new List + { + new SiteDefinitionOptions + { + Id = "customerx", + Name = "Customer X", + Hosts = new List + { + new HostOptions + { + Name = "customer.example", + Type = ApplicationHostType.Primary, + UseSecureConnection = true, + Language = "en" + } + } + } + } + }; + + var configuration = new ConfigurationReader(Options.Create(options)).ReadConfiguration(); + var application = configuration.SiteDefinitions.Single(); + + application.Id.Should().Be("customerx"); + application.Name.Should().Be("Customer X"); + application.IsDefault.Should().BeNull(); + application.Hosts.Should().ContainSingle(); + application.Hosts.Single().Authority.Should().Be("customer.example"); + application.Hosts.Single().Type.Should().Be(ApplicationHostType.Primary); + application.Hosts.Single().PreferredUrlScheme.Should().Be(UrlScheme.Https); + application.Hosts.Single().Locale.Name.Should().Be("en"); + } + + [Fact] + public void ReadConfiguration_DoesNotMapLocaleForApplicationHostTypesThatRejectLocale() + { + var options = new EnvironmentSynchronizerOptions + { + SiteDefinitions = new List + { + new SiteDefinitionOptions + { + Id = "customerx", + Name = "Customer X", + Hosts = new List + { + new HostOptions + { + Name = "media.customer.example", + Type = ApplicationHostType.Media, + Language = "en" + }, + new HostOptions + { + Name = "edit.customer.example", + Type = ApplicationHostType.Edit, + Language = "en" + }, + new HostOptions + { + Name = "preview.customer.example", + Type = ApplicationHostType.Preview, + Language = "en" + } + } + } + } + }; + + var configuration = new ConfigurationReader(Options.Create(options)).ReadConfiguration(); + var application = configuration.SiteDefinitions.Single(); + + application.Hosts.Should().HaveCount(3); + application.Hosts.Should().OnlyContain(host => host.Locale == null); + } + + [Fact] + public void Synchronize_SavesMatchingApplicationAndUpdatesDefaultState() + { + var repository = new Mock(); + var contentSecurityRepository = new Mock(); + var configurationReader = new Mock(); + var existingApplication = new InProcessWebsite("customerx", new ContentReference(9)); + var applicationDefinition = new EnvironmentSynchronizerSiteDefinition + { + Id = "customerx", + Name = "Customer X", + IsDefault = true, + Hosts = new List + { + new ApplicationHost("customer.example") + { + Type = ApplicationHostType.Primary, + PreferredUrlScheme = UrlScheme.Https + }, + new ApplicationHost("edit.customer.example") + { + Type = ApplicationHostType.Edit, + PreferredUrlScheme = UrlScheme.Https + }, + new ApplicationHost("preview.customer.example") + { + Type = ApplicationHostType.Preview, + PreferredUrlScheme = UrlScheme.Https + } + } + }; + + repository.Setup(x => x.List()).Returns(new Application[] { existingApplication }); + repository.Setup(x => x.SaveAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + repository.Setup(x => x.MakeDefaultAsync(It.IsAny(), true, It.IsAny())).Returns(Task.CompletedTask); + configurationReader.Setup(x => x.ReadConfiguration()).Returns(new SynchronizationData + { + SiteDefinitions = new List { applicationDefinition } + }); + + var subject = new SiteDefinitionSynchronizer(repository.Object, contentSecurityRepository.Object, configurationReader.Object); + + subject.Synchronize("Test"); + + repository.Verify(x => x.SaveAsync( + It.Is(application => + application is InProcessWebsite && + application.DisplayName == "Customer X" && + ((IRoutableApplication)application).Hosts.Any(host => host.Authority == "customer.example") && + ((IRoutableApplication)application).Hosts.Any(host => host.Authority == "edit.customer.example") && + !((IRoutableApplication)application).Hosts.Any(host => host.Authority == "preview.customer.example")), + It.IsAny()), Times.Once); + repository.Verify(x => x.MakeDefaultAsync(It.IsAny(), true, It.IsAny()), Times.Once); + } + + [Fact] + public void Synchronize_SavesMatchingHeadlessWebsiteWithoutChangingItsType() + { + var repository = new Mock(); + var contentSecurityRepository = new Mock(); + var configurationReader = new Mock(); + var existingApplication = new Website("headless", new ContentReference(10)); + var applicationDefinition = new EnvironmentSynchronizerSiteDefinition + { + Id = "headless", + Name = "Headless Website", + Hosts = new List + { + new ApplicationHost("headless.example") + { + Type = ApplicationHostType.Primary, + PreferredUrlScheme = UrlScheme.Https + }, + new ApplicationHost("preview.headless.example") + { + Type = ApplicationHostType.Preview, + PreferredUrlScheme = UrlScheme.Https + }, + new ApplicationHost("redirect.headless.example") + { + Type = ApplicationHostType.RedirectPermanent, + PreferredUrlScheme = UrlScheme.Https + } + } + }; + + repository.Setup(x => x.List()).Returns(new Application[] { existingApplication }); + repository.Setup(x => x.SaveAsync(It.IsAny(), It.IsAny())).Returns(Task.CompletedTask); + configurationReader.Setup(x => x.ReadConfiguration()).Returns(new SynchronizationData + { + SiteDefinitions = new List { applicationDefinition } + }); + + var subject = new SiteDefinitionSynchronizer(repository.Object, contentSecurityRepository.Object, configurationReader.Object); + + subject.Synchronize("Test"); + + repository.Verify(x => x.SaveAsync( + It.Is(application => + application is Website && + application.DisplayName == "Headless Website" && + ((Website)application).Hosts.Any(host => host.Authority == "headless.example") && + ((Website)application).Hosts.Any(host => host.Authority == "preview.headless.example") && + !((Website)application).Hosts.Any(host => host.Authority == "redirect.headless.example")), + It.IsAny()), Times.Once); + } + + [Fact] + public void UpdateSitePermissions_UsesApplicationEntryPoint() + { + var startPage = new ContentReference(9); + var securityDescriptor = new FakeContentSecurityDescriptor + { + Entries = new List { new AccessControlEntry("Everyone", AccessLevel.Read) } + }; + var repository = new Mock(); + var contentSecurityRepository = new Mock(); + var configurationReader = new Mock(); + contentSecurityRepository.Setup(x => x.Get(startPage)).Returns(securityDescriptor); + + var subject = new SiteDefinitionSynchronizer(repository.Object, contentSecurityRepository.Object, configurationReader.Object); + var application = new InProcessWebsite("customerx", startPage); + var definition = new EnvironmentSynchronizerSiteDefinition { Name = "Customer X", ForceLogin = true }; + + subject.UpdateSitePermissions(application, definition); + + securityDescriptor.Entries.Should().BeEmpty(); + contentSecurityRepository.Verify(x => x.Save(startPage, securityDescriptor, SecuritySaveType.Replace), Times.Once); + } + + [Fact] + public void UpdateSitePermissions_UsesHeadlessWebsiteEntryPoint() + { + var startPage = new ContentReference(10); + var securityDescriptor = new FakeContentSecurityDescriptor + { + Entries = new List { new AccessControlEntry("Everyone", AccessLevel.Read) } + }; + var repository = new Mock(); + var contentSecurityRepository = new Mock(); + var configurationReader = new Mock(); + contentSecurityRepository.Setup(x => x.Get(startPage)).Returns(securityDescriptor); + + var subject = new SiteDefinitionSynchronizer(repository.Object, contentSecurityRepository.Object, configurationReader.Object); + var application = new Website("headless", startPage); + var definition = new EnvironmentSynchronizerSiteDefinition { Name = "Headless Website", ForceLogin = true }; + + subject.UpdateSitePermissions(application, definition); + + securityDescriptor.Entries.Should().BeEmpty(); + contentSecurityRepository.Verify(x => x.Save(startPage, securityDescriptor, SecuritySaveType.Replace), Times.Once); + } + } +} +#endif diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissionsTests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissionsTests.cs index 22e1c96..506c13d 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissionsTests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissionsTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using Xunit; +#if !NET10_0_OR_GREATER namespace Addon.Episever.EnvironmentSynchronizer.Test.Synchronizers.SiteDefinitions { public class UpdateSitePermissionsTests @@ -97,3 +98,4 @@ public void When_noforcelogin_nosetroles_noremoveroles_nothing_should_be_done() } } } +#endif diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_ForceLogin_Tests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_ForceLogin_Tests.cs index 6a2de72..4b2b8d5 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_ForceLogin_Tests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_ForceLogin_Tests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using Xunit; +#if !NET10_0_OR_GREATER namespace Addon.Episever.EnvironmentSynchronizer.Test.Synchronizers.SiteDefinitions { public class UpdateSitePermissions_ForceLogin_Tests @@ -226,3 +227,4 @@ public void When_forcelogin_accesscontrolentrylist_contains_4_other_entries_it_s } } } +#endif diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_RemoveRoles_Tests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_RemoveRoles_Tests.cs index f640815..1a96508 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_RemoveRoles_Tests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_RemoveRoles_Tests.cs @@ -11,6 +11,7 @@ using System.Linq; using Xunit; +#if !NET10_0_OR_GREATER namespace Addon.Episever.EnvironmentSynchronizer.Test.Synchronizers.SiteDefinitions { public class UpdateSitePermissions_RemoveRoles_Tests @@ -283,3 +284,4 @@ public void When_forcelogin_two_setroles_two_removeroles_on_many_existing_access } } +#endif diff --git a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_SetRoles_Tests.cs b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_SetRoles_Tests.cs index 5e25d8e..6efa8e1 100644 --- a/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_SetRoles_Tests.cs +++ b/Addon.Episever.EnvironmentSynchronizer.Test/Synchronizers/SiteDefinitions/UpdateSitePermissions_SetRoles_Tests.cs @@ -11,6 +11,7 @@ using System.Linq; using Xunit; +#if !NET10_0_OR_GREATER namespace Addon.Episever.EnvironmentSynchronizer.Test.Synchronizers.SiteDefinitions { public class UpdateSitePermissions_SetRoles_Tests @@ -227,3 +228,4 @@ public void When_two_setroles_on_many_existing_accesscontrolentrylist_save_as_up } } +#endif diff --git a/README.md b/README.md index f87f1ef..32e7709 100644 --- a/README.md +++ b/README.md @@ -193,12 +193,14 @@ Tells the synchronizer that you want to run it as an InitializationModule. Tells the synchronizer that you will run synchronization with InitializationModule every time the application is starting up. If this is set to `false`, that is default if this attribute is not set. It will check for a flag that tells the synchronizer if it has already synchronized the current environment or not. So it will only run if the flag specifies a value of a environment that is not equal to the current environment. This logic will only be used for the InitializationModule logic. The schedule job will always synchronize. This function is implemented for these projects that don´t want the payload of synchronization every time the application starts up. -### sitedefinition +### sitedefinition / application **Id** is the GUID that identify the site. If this is provided it will ignore the "Name" attribute. **Name** is the name of the sitedefinition that will be updated. If **Id** is not specified it will match the existing SiteDefinition in the Episerver CMS against this name. **SiteUrl*** is the SiteUrl that this site should have/use. [**ForceLogin***](/documentation/ForceLogin.md) will remove 'Everyone' AccessLevel.Read if it is found for the site. +For Optimizely CMS 13, `SiteDefinitions` is retained as the configuration key for compatibility, but it synchronizes both `InProcessWebsite` applications and headless `Website` applications. The existing application's concrete website type is retained. `Name` must match the application's display name `Application.DisplayName`. + ### hosts You need to specify all the hosts that the site needs. When the synchronizer is updating a SiteDefinition it will expect that you have specified all hostnames. So of you in Episerver CMS has a extra host that is not specified in the web.config it will be removed. @@ -214,6 +216,8 @@ Options (`EPiServer.Web.HostDefinitionType [Enum]`): - **RedirectTemporary** **Language** is the CultureInfo that is related to the hostname +For CMS 13, hosts are `EPiServer.Applications.ApplicationHost` instances and **Type** uses `ApplicationHostType`: `Default`, `Primary`, `Preview`, `Edit`, `Media`, `RedirectPermanent`, or `RedirectTemporary`. + ### SetRoles Can contain a list of roles that will be added/updated. If not exist in existing list it will be added. If it exist, the AccessLevel will be updated with the access level you specified in the configuration. Note: The permission will be set on startpage of the site and all childpages. @@ -342,4 +346,4 @@ If you run the scheduled job "Environment Synchronization" and get the message " Then you have installed the NuGet package but you have not add "services.AddEnvironmentSynchronization();" in the startup. Go through the installation instructions earlier in this readme. ## Unable to autorun search and navigation indexing job Environment synchronizer is unable to trigger this job using `"AutoRun: true"`. This is because the job requires access to `HttpContext` and the `IScheduledJobExecutor` runs jobs as background tasks, where `HttpContext` does not exist. -This can be solved by modifying the job via a custom synchronizer. [**See solution.**](/documentation/SearchReindexSynchronizer.md) \ No newline at end of file +This can be solved by modifying the job via a custom synchronizer. [**See solution.**](/documentation/SearchReindexSynchronizer.md) diff --git a/nugets/dotnet6/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg b/nugets/dotnet6/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg deleted file mode 100644 index 5abb425..0000000 Binary files a/nugets/dotnet6/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg and /dev/null differ diff --git a/nugets/dotnet7/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg b/nugets/dotnet7/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg deleted file mode 100644 index 5abb425..0000000 Binary files a/nugets/dotnet7/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg and /dev/null differ diff --git a/nugets/dotnet8/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg b/nugets/dotnet8/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg deleted file mode 100644 index 5abb425..0000000 Binary files a/nugets/dotnet8/Addon.Episerver.EnvironmentSynchronizer.1.3.1.nupkg and /dev/null differ