diff --git a/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj b/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj index 5aa7f8d6..65aa6dd3 100644 --- a/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj +++ b/Lombiq.Hosting.Tenants.Admin.Login/Lombiq.Hosting.Tenants.Admin.Login.csproj @@ -34,10 +34,10 @@ - - - - + + + + @@ -45,7 +45,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj b/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj index c509a130..6295dd52 100644 --- a/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj +++ b/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI/Lombiq.Hosting.Tenants.EnvironmentRobots.Tests.UI.csproj @@ -21,7 +21,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj b/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj index b4bfd8ab..8a9ab48c 100644 --- a/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj +++ b/Lombiq.Hosting.Tenants.EnvironmentRobots/Lombiq.Hosting.Tenants.EnvironmentRobots.csproj @@ -34,8 +34,8 @@ - - - + + + diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs index 071b1792..ae4f26ff 100644 --- a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs +++ b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs @@ -66,7 +66,7 @@ private static async Task SetUpNewTenantAndGoToFeaturesListAsync( { await context.SignInDirectlyAsync(); - await context.CreateAndSwitchToTenantManuallyAsync(tenantName, tenantUrlPrefix, string.Empty, "features guard"); + await context.CreateAndSwitchToTenantManuallyAsync(tenantName, tenantUrlPrefix, string.Empty, "Features Guard"); await context.GoToSetupPageAndSetupOrchardCoreAsync( new OrchardCoreSetupParameters(context) diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj index da830ffc..078102de 100644 --- a/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj +++ b/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI/Lombiq.Hosting.Tenants.FeaturesGuard.Tests.UI.csproj @@ -27,13 +27,13 @@ - + - + diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs b/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs index 3da2235a..e9b6682e 100644 --- a/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs +++ b/Lombiq.Hosting.Tenants.FeaturesGuard/Handlers/FeaturesEventHandler.cs @@ -1,10 +1,9 @@ using Lombiq.Hosting.Tenants.FeaturesGuard.Models; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OrchardCore.Environment.Extensions.Features; using OrchardCore.Environment.Shell; -using OrchardCore.Environment.Shell.Descriptor; -using OrchardCore.Environment.Shell.Descriptor.Models; +using OrchardCore.Environment.Shell.Scope; using System; using System.Collections.Generic; using System.Linq; @@ -14,168 +13,145 @@ namespace Lombiq.Hosting.Tenants.FeaturesGuard.Handlers; public sealed class FeaturesEventHandler : IFeatureEventHandler { - private readonly IShellFeaturesManager _shellFeaturesManager; - private readonly IOptions _conditionallyEnabledFeaturesOptions; - private readonly ShellSettings _shellSettings; - private readonly IShellDescriptorManager _shellDescriptorManager; - - public FeaturesEventHandler( - IShellFeaturesManager shellFeaturesManager, - IOptions conditionallyEnabledFeaturesOptions, - ShellSettings shellSettings, - IConfiguration configuration, - IShellDescriptorManager shellDescriptorManager) - { - _shellFeaturesManager = shellFeaturesManager; - _conditionallyEnabledFeaturesOptions = conditionallyEnabledFeaturesOptions; - _shellSettings = shellSettings; - _shellDescriptorManager = shellDescriptorManager; - } - - Task IFeatureEventHandler.InstallingAsync(IFeatureInfo feature) => Task.CompletedTask; - - Task IFeatureEventHandler.InstalledAsync(IFeatureInfo feature) => Task.CompletedTask; - - Task IFeatureEventHandler.EnablingAsync(IFeatureInfo feature) => Task.CompletedTask; - - Task IFeatureEventHandler.EnabledAsync(IFeatureInfo feature) => EnableConditionallyEnabledFeaturesAsync(feature); - - Task IFeatureEventHandler.DisablingAsync(IFeatureInfo feature) => Task.CompletedTask; - - async Task IFeatureEventHandler.DisabledAsync(IFeatureInfo feature) - { - await KeepConditionallyEnabledFeaturesEnabledAsync(feature); - await DisableConditionallyEnabledFeaturesAsync(feature); - } - - Task IFeatureEventHandler.UninstallingAsync(IFeatureInfo feature) => Task.CompletedTask; // #spell-check-ignore-line + private bool _deferredTaskTriggered; - Task IFeatureEventHandler.UninstalledAsync(IFeatureInfo feature) => Task.CompletedTask; + public Task InstallingAsync(IFeatureInfo feature) => Task.CompletedTask; - /// - /// Enables conditional features (key) if one of their corresponding condition features (value) was enabled. - /// - /// The feature that was just enabled. - public async Task EnableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo) - { - if (_shellSettings.IsDefaultShell() || - _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures) - { - return; - } - - var allConditionFeatureIds = new List(); - foreach (var conditionFeatureIds in conditionallyEnabledFeatures.Values) - { - allConditionFeatureIds.AddRange(conditionFeatureIds); - } + public Task InstalledAsync(IFeatureInfo feature) => Task.CompletedTask; - if (!allConditionFeatureIds.Contains(featureInfo.Id)) - { - return; - } + public Task EnablingAsync(IFeatureInfo feature) => Task.CompletedTask; - // Enable conditional features if they are not already enabled. - var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync(); + public Task EnabledAsync(IFeatureInfo feature) => HandleConditionallyEnabledFeaturesAsync(); - var conditionalFeatureIds = conditionallyEnabledFeatures - .Where(keyValuePair => keyValuePair.Value.Contains(featureInfo.Id)) - .Select(keyValuePair => keyValuePair.Key) - .ToList(); + public Task DisablingAsync(IFeatureInfo feature) => Task.CompletedTask; - // During setup, Shell Descriptor can become out of sync with the DB when it comes to enabled features, - // but it's more accurate than IShellDescriptorManager's methods. - var shellDescriptor = await _shellDescriptorManager.GetShellDescriptorAsync(); + public Task DisabledAsync(IFeatureInfo feature) => HandleConditionallyEnabledFeaturesAsync(); - // If Shell Descriptor's Features already contains a feature that is found in conditionalFeatures, remove it - // from the list. Handle multiple conditional features as well. - var featuresToEnable = allFeatures.Where(feature => - conditionalFeatureIds.Contains(feature.Id) && !shellDescriptor.Features.Contains(new ShellFeature(feature.Id))); + public Task UninstallingAsync(IFeatureInfo feature) => Task.CompletedTask; // #spell-check-ignore-line - await _shellFeaturesManager.EnableFeaturesAsync(featuresToEnable, force: true); - } + public Task UninstalledAsync(IFeatureInfo feature) => Task.CompletedTask; /// - /// When a conditional feature (key) is disabled, keeps the conditional feature enabled if any of the corresponding - /// condition features (value) are enabled. + /// Enables or disables conditional features depending on ConditionallyEnabledFeaturesOptions. + /// Prevents disabling features that should be enabled according to their conditions. /// - /// The feature that was just disabled. - public async Task KeepConditionallyEnabledFeaturesEnabledAsync(IFeatureInfo featureInfo) + private Task HandleConditionallyEnabledFeaturesAsync() { - if (_shellSettings.IsDefaultShell() || - _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures) + if (_deferredTaskTriggered) { - return; + return Task.CompletedTask; } - if (!conditionallyEnabledFeatures.ContainsKey(featureInfo.Id)) - { - return; - } - - // Re-enable conditional feature if any its condition features are enabled. - var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync(); - var conditionFeatureIds = conditionallyEnabledFeatures[featureInfo.Id]; + _deferredTaskTriggered = true; - var currentlyEnabledFeatures = await _shellFeaturesManager.GetEnabledFeaturesAsync(); - var conditionFeatures = allFeatures.Where(feature => conditionFeatureIds.Contains(feature.Id)); - - var currentlyEnabledConditionFeatures = currentlyEnabledFeatures.Intersect(conditionFeatures); - if (currentlyEnabledConditionFeatures.Any()) + ShellScope.AddDeferredTask(async scope => { - var conditionalFeature = allFeatures.Where(feature => feature.Id == featureInfo.Id); - await _shellFeaturesManager.EnableFeaturesAsync(conditionalFeature); - } + if (scope.ShellContext.Settings.IsDefaultShell()) + { + return; + } + + var shellFeaturesManager = scope + .ServiceProvider + .GetRequiredService(); + + var conditionallyEnabledFeaturesOptions = scope + .ServiceProvider + .GetRequiredService>() + .Value + .EnableFeatureIfOtherFeatureIsEnabled; + + var enabledFeatures = (await shellFeaturesManager.GetEnabledFeaturesAsync()) + .ToHashSet(); + + var enabledFeaturesIds = enabledFeatures + .Select(feature => feature.Id) + .ToHashSet(); + + if (!TryGetFeaturesToBeEnabledAndDisabled( + conditionallyEnabledFeaturesOptions, + enabledFeaturesIds, + out var featuresToEnableIds, + out var featuresToDisableIds)) + { + return; + } + + var availableFeatures = await shellFeaturesManager.GetAvailableFeaturesAsync(); + + var featuresToEnable = availableFeatures + .Where(feature => featuresToEnableIds.Contains(feature.Id)) + .ToList(); + + if (featuresToEnable.Exists(feature => feature.DefaultTenantOnly || feature.EnabledByDependencyOnly)) + { + throw new InvalidOperationException("'DefaultTenantOnly' feature can't be enabled by FeaturesGuard."); + } + + var featuresToDisable = enabledFeatures + .Where(feature => featuresToDisableIds.Contains(feature.Id)) + .ToList(); + + if (featuresToDisable.Exists(feature => feature.IsAlwaysEnabled || feature.EnabledByDependencyOnly)) + { + throw new InvalidOperationException("'IsAlwaysEnabled' feature can't be disabled by FeaturesGuard."); + } + + if (!featuresToEnable.Any() && !featuresToDisable.Any()) + { + return; + } + + await shellFeaturesManager.UpdateFeaturesAsync(featuresToDisable, featuresToEnable, force: true); + }); + + return Task.CompletedTask; } /// - /// When a condition feature (value) is disabled, disables the corresponding conditional features (key) if all of - /// their condition features are disabled. + /// Extracts the feature ids from ConditionallyEnabledFeaturesOptions and separates them into + /// and hash sets + /// and compares them to collection to determine which features need to be + /// enabled or disabled. /// - /// The feature that was just disabled. - public async Task DisableConditionallyEnabledFeaturesAsync(IFeatureInfo featureInfo) + /// A boolean value whether ConditionallyEnabledFeaturesOptions is populated or not. + /// Also produces and . + /// + private static bool TryGetFeaturesToBeEnabledAndDisabled( + IDictionary> conditionallyEnabledFeatures, + IReadOnlySet enabledFeatureIds, + out HashSet featuresToEnable, + out HashSet featuresToDisable) { - if (_shellSettings.IsDefaultShell() || - _conditionallyEnabledFeaturesOptions.Value.EnableFeatureIfOtherFeatureIsEnabled is not { } conditionallyEnabledFeatures) + if (!conditionallyEnabledFeatures.Any()) { - return; + featuresToEnable = null; + featuresToDisable = null; + return false; } - var allConditionFeatureIds = new List(); - foreach (var conditionFeatureIdList in conditionallyEnabledFeatures.Values) - { - allConditionFeatureIds.AddRange(conditionFeatureIdList); - } + var featuresToEnableIds = new HashSet(); + var featuresToDisableIds = new HashSet(); - if (!allConditionFeatureIds.Contains(featureInfo.Id)) + foreach (var condition in conditionallyEnabledFeatures) { - return; + var hasConditional = enabledFeatureIds.Contains(condition.Key); + var hasCondition = enabledFeatureIds.Intersect(condition.Value).Any(); + + if (hasCondition && !hasConditional) + { + featuresToEnableIds.Add(condition.Key); + } + + if (!hasCondition && hasConditional) + { + featuresToDisableIds.Add(condition.Key); + } } - // If current feature is one of the condition features, disable its corresponding conditional features if they - // are not already disabled. - var allFeatures = await _shellFeaturesManager.GetAvailableFeaturesAsync(); - - var conditionalFeatureIds = new List(); - var conditionFeatureIds = new List(); - foreach (var keyValuePair in conditionallyEnabledFeatures.Where(keyValuePair => keyValuePair.Value.Contains(featureInfo.Id))) - { - conditionalFeatureIds.Add(keyValuePair.Key); - conditionFeatureIds.AddRange(keyValuePair.Value); - } + featuresToEnable = featuresToEnableIds; + featuresToDisable = featuresToDisableIds; - var currentlyEnabledFeatures = await _shellFeaturesManager.GetEnabledFeaturesAsync(); - var conditionFeatures = allFeatures.Where(feature => conditionFeatureIds.Contains(feature.Id)); - - // Only disable conditional feature if none of its condition features are enabled. - var currentlyEnabledConditionFeatures = currentlyEnabledFeatures.Intersect(conditionFeatures); - if (!currentlyEnabledConditionFeatures.Any()) - { - // Handle multiple conditional features as well. - var conditionalFeatures = allFeatures.Where(feature => conditionalFeatureIds.Contains(feature.Id)); - var currentlyEnabledConditionalFeatures = currentlyEnabledFeatures.Intersect(conditionalFeatures); - - await _shellFeaturesManager.DisableFeaturesAsync(currentlyEnabledConditionalFeatures); - } + return featuresToEnableIds.Any() || featuresToDisableIds.Any(); } } diff --git a/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj b/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj index b261ca10..9f722cdc 100644 --- a/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj +++ b/Lombiq.Hosting.Tenants.FeaturesGuard/Lombiq.Hosting.Tenants.FeaturesGuard.csproj @@ -34,11 +34,11 @@ - - - - - + + + + + @@ -47,8 +47,8 @@ - - + + diff --git a/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj b/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj index 3c473d98..70bf4784 100644 --- a/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj +++ b/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI/Lombiq.Hosting.Tenants.IdleTenantManagement.Tests.UI.csproj @@ -21,7 +21,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj b/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj index b5ec9b2c..23d1dfa2 100644 --- a/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj +++ b/Lombiq.Hosting.Tenants.IdleTenantManagement/Lombiq.Hosting.Tenants.IdleTenantManagement.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj b/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj index b6787a0c..f404b07a 100644 --- a/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj +++ b/Lombiq.Hosting.Tenants.Maintenance.Tests.UI/Lombiq.Hosting.Tenants.Maintenance.Tests.UI.csproj @@ -21,7 +21,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj b/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj index 303ab779..30e11eb1 100644 --- a/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj +++ b/Lombiq.Hosting.Tenants.Maintenance/Lombiq.Hosting.Tenants.Maintenance.csproj @@ -23,13 +23,13 @@ - - - - - - - + + + + + + + @@ -48,7 +48,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj b/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj index f97aa793..da73240a 100644 --- a/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj +++ b/Lombiq.Hosting.Tenants.Management/Lombiq.Hosting.Tenants.Management.csproj @@ -34,10 +34,10 @@ - - - - + + + + @@ -45,7 +45,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj b/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj index 40863d93..6829289a 100644 --- a/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj +++ b/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI/Lombiq.Hosting.Tenants.MediaStorageManagement.Tests.UI.csproj @@ -33,7 +33,7 @@ - + diff --git a/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj b/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj index 335edbd9..fe67cfc0 100644 --- a/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj +++ b/Lombiq.Hosting.Tenants.MediaStorageManagement/Lombiq.Hosting.Tenants.MediaStorageManagement.csproj @@ -34,10 +34,10 @@ - - - - + + + + @@ -46,7 +46,7 @@ - +