diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 12a0afea47..358eae2e38 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -39,7 +39,7 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism /// public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + string.Equals(_connectionStrings.CurrentValue.ProviderName,Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); /// public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs index 63f0d8f9fe..5c10f46828 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Constants.cs @@ -8,5 +8,8 @@ public static class Constants /// /// SQLite provider name. /// - public const string ProviderName = "Microsoft.Data.SQLite"; + public const string ProviderName = "Microsoft.Data.Sqlite"; + + [Obsolete("This will be removed in Umbraco 12. Use Constants.ProviderName instead")] + public const string ProviderNameLegacy = "Microsoft.Data.SQLite"; } diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index c704bb8272..a4a31416fa 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -35,7 +35,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism /// public bool Enabled => _connectionStrings.CurrentValue.IsConnectionStringConfigured() && - _connectionStrings.CurrentValue.ProviderName == Constants.ProviderName; + string.Equals(_connectionStrings.CurrentValue.ProviderName, Constants.ProviderName, StringComparison.InvariantCultureIgnoreCase); // With journal_mode=wal we can always read a snapshot. public IDistributedLock ReadLock(int lockId, TimeSpan? obtainLockTimeout = null) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs index b617831b86..b3002fa8fe 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -46,11 +46,15 @@ public static class UmbracoBuilderExtensions DbProviderFactories.UnregisterFactory(Constants.ProviderName); DbProviderFactories.RegisterFactory(Constants.ProviderName, SqliteFactory.Instance); + // Remove this registration in Umbraco 12 + DbProviderFactories.UnregisterFactory(Constants.ProviderNameLegacy); + DbProviderFactories.RegisterFactory(Constants.ProviderNameLegacy, SqliteFactory.Instance); + // Prevent accidental creation of SQLite database files builder.Services.PostConfigureAll(options => { // Skip empty connection string and other providers - if (!options.IsConnectionStringConfigured() || options.ProviderName != Constants.ProviderName) + if (!options.IsConnectionStringConfigured() || (options.ProviderName != Constants.ProviderName && options.ProviderName != Constants.ProviderNameLegacy)) { return; } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 5351da317c..2665c0738f 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -33,6 +33,7 @@ public class GlobalSettings internal const string StaticDistributedLockingWriteLockDefaultTimeout = "00:00:05"; internal const bool StaticSanitizeTinyMce = false; internal const int StaticMainDomReleaseSignalPollingInterval = 2000; + private const bool StaticForceCombineUrlPathLeftToRight = true; /// /// Gets or sets a value for the reserved URLs (must end with a comma). @@ -226,7 +227,25 @@ public class GlobalSettings TimeSpan.Parse(StaticDistributedLockingWriteLockDefaultTimeout); /// - /// Gets or sets a value representing the DistributedLockingMechanism to use. + /// Gets or sets a value representing the DistributedLockingMechanism to use. /// public string DistributedLockingMechanism { get; set; } = string.Empty; + + /// + /// Force url paths to be left to right, even when the culture has right to left text + /// + /// + /// For the following hierarchy + /// - Root (/ar) + /// - 1 (/ar/1) + /// - 2 (/ar/1/2) + /// - 3 (/ar/1/2/3) + /// - 3 (/ar/1/2/3/4) + /// When forced + /// - https://www.umbraco.com/ar/1/2/3/4 + /// when not + /// - https://www.umbraco.com/ar/4/3/2/1 + /// + [DefaultValue(StaticForceCombineUrlPathLeftToRight)] + public bool ForceCombineUrlPathLeftToRight { get; set; } = StaticForceCombineUrlPathLeftToRight; } diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index c791ec6168..0e7e1812c6 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -50,11 +50,11 @@ public class ModelsBuilderSettings } return _flagOutOfDateModels; - } - - set =>_flagOutOfDateModels = value; } + set => _flagOutOfDateModels = value; + } + /// /// Gets or sets a value for the models directory. diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs new file mode 100644 index 0000000000..3f38167af8 --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/RuntimeMode.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Configuration.Models; + +/// +/// Represents the configured Umbraco runtime mode. +/// +public enum RuntimeMode +{ + /// + /// The backoffice development mode ensures the runtime is configured for rapidly applying changes within the backoffice. + /// + BackofficeDevelopment = 0, + + /// + /// The development mode ensures the runtime is configured for rapidly applying changes. + /// + Development = 1, + + /// + /// The production mode ensures optimal performance settings are configured and denies any changes that would require recompilations. + /// + Production = 2 +} diff --git a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs index ac4e51a1c2..7f31c9319b 100644 --- a/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RuntimeSettings.cs @@ -1,16 +1,24 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.ComponentModel; + namespace Umbraco.Cms.Core.Configuration.Models; /// -/// Typed configuration options for runtime settings. +/// Typed configuration options for runtime settings. /// [UmbracoOptions(Constants.Configuration.ConfigRuntime)] public class RuntimeSettings { /// - /// Gets or sets a value for the maximum query string length. + /// Gets or sets the runtime mode. + /// + [DefaultValue(RuntimeMode.BackofficeDevelopment)] + public RuntimeMode Mode { get; set; } = RuntimeMode.BackofficeDevelopment; + + /// + /// Gets or sets a value for the maximum query string length. /// public int? MaxQueryStringLength { get; set; } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 3f7f3188a9..11694fa5c0 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -5,23 +5,19 @@ public static partial class Constants public static class Configuration { /// - /// Case insensitive prefix for all configurations + /// Case insensitive prefix for all configurations. /// /// - /// ":" is used as marker for nested objects in json. E.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{....}} + /// ":" is used as marker for nested objects in JSON, e.g. "Umbraco:CMS:" = {"Umbraco":{"CMS":{...}}. /// public const string ConfigPrefix = "Umbraco:CMS:"; - public const string ConfigContentPrefix = ConfigPrefix + "Content:"; public const string ConfigContentNotificationsPrefix = ConfigContentPrefix + "Notifications:"; public const string ConfigCorePrefix = ConfigPrefix + "Core:"; public const string ConfigCustomErrorsPrefix = ConfigPrefix + "CustomErrors:"; public const string ConfigGlobalPrefix = ConfigPrefix + "Global:"; public const string ConfigGlobalId = ConfigGlobalPrefix + "Id"; - - public const string ConfigGlobalDistributedLockingMechanism = - ConfigGlobalPrefix + "DistributedLockingMechanism"; - + public const string ConfigGlobalDistributedLockingMechanism = ConfigGlobalPrefix + "DistributedLockingMechanism"; public const string ConfigHostingPrefix = ConfigPrefix + "Hosting:"; public const string ConfigModelsBuilderPrefix = ConfigPrefix + "ModelsBuilder:"; public const string ConfigSecurityPrefix = ConfigPrefix + "Security:"; @@ -49,6 +45,7 @@ public static partial class Constants public const string ConfigPlugins = ConfigPrefix + "Plugins"; public const string ConfigRequestHandler = ConfigPrefix + "RequestHandler"; public const string ConfigRuntime = ConfigPrefix + "Runtime"; + public const string ConfigRuntimeMode = ConfigRuntime + ":Mode"; public const string ConfigRuntimeMinification = ConfigPrefix + "RuntimeMinification"; public const string ConfigRuntimeMinificationVersion = ConfigRuntimeMinification + ":Version"; public const string ConfigSecurity = ConfigPrefix + "Security"; @@ -62,7 +59,7 @@ public static partial class Constants public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; public const string ConfigInstallDefaultData = ConfigPrefix + "InstallDefaultData"; - public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; + public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; public static class NamedOptions { diff --git a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs index 466c9d7a3b..87c755fdc9 100644 --- a/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Core/ContentApps/ListViewContentAppFactory.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.PropertyEditors; @@ -80,8 +80,7 @@ public class ListViewContentAppFactory : IContentAppFactory throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); } - IDictionary listViewConfig = - editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); + IDictionary listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditorNullable(dt.Configuration); // add the entity type to the config listViewConfig["entityType"] = entityType; @@ -90,7 +89,7 @@ public class ListViewContentAppFactory : IContentAppFactory if (listViewConfig.ContainsKey("tabName")) { var configTabName = listViewConfig["tabName"]; - if (string.IsNullOrWhiteSpace(configTabName.ToString()) == false) + if (string.IsNullOrWhiteSpace(configTabName?.ToString()) == false) { contentApp.Name = configTabName.ToString(); } @@ -100,7 +99,7 @@ public class ListViewContentAppFactory : IContentAppFactory if (listViewConfig.ContainsKey("icon")) { var configIcon = listViewConfig["icon"]; - if (string.IsNullOrWhiteSpace(configIcon.ToString()) == false) + if (string.IsNullOrWhiteSpace(configIcon?.ToString()) == false) { contentApp.Icon = configIcon.ToString(); } @@ -123,7 +122,7 @@ public class ListViewContentAppFactory : IContentAppFactory Value = null, View = editor.GetValueEditor().View, HideLabel = true, - Config = listViewConfig, + ConfigNullable = listViewConfig, }, }; diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index f9a63fb6d4..e54691d7ba 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -187,9 +187,6 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); - // by default, register a noop factory - Services.AddUnique(); - Services.AddUnique(); Services.AddSingleton(f => f.GetRequiredService().CreateDictionary()); diff --git a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs index e5428fb4c4..2003079736 100644 --- a/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs +++ b/src/Umbraco.Core/Extensions/ConfigurationExtensions.cs @@ -1,10 +1,11 @@ using Microsoft.Extensions.Configuration; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Extensions; /// -/// Extension methods for configuration. +/// Extensions for . /// public static class ConfigurationExtensions { @@ -90,4 +91,14 @@ public static class ConfigurationExtensions return connectionString; } + + /// + /// Gets the Umbraco runtime mode. + /// + /// The configuration. + /// + /// The Umbraco runtime mode. + /// + public static RuntimeMode GetRuntimeMode(this IConfiguration configuration) + => configuration.GetValue(Constants.Configuration.ConfigRuntimeMode); } diff --git a/src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs b/src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs similarity index 100% rename from src/Umbraco.Core/Configuration/Extensions/HealthCheckSettingsExtensions.cs rename to src/Umbraco.Core/Extensions/HealthCheckSettingsExtensions.cs diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs index e2fcf71053..eb800791a2 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentItemDisplay.cs @@ -17,9 +17,9 @@ public class ContentItemDisplayWithSchedule : ContentItemDisplay [DataContract(Name = "content", Namespace = "")] -public class - ContentItemDisplay : INotificationModel, - IErrorModel // ListViewAwareContentItemDisplayBase +public class ContentItemDisplay : + INotificationModel, + IErrorModel // ListViewAwareContentItemDisplayBase where TVariant : ContentVariantDisplay { public ContentItemDisplay() diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs index ca8c2f1fc2..d0f2b9aed6 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentPropertyDisplay.cs @@ -26,9 +26,14 @@ public class ContentPropertyDisplay : ContentPropertyBasic [Required(AllowEmptyStrings = false)] public string? View { get; set; } + [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ConfigNullable instead.")] [DataMember(Name = "config")] public IDictionary? Config { get; set; } + // TODO: Obsolete in V11. + [IgnoreDataMember] + public IDictionary? ConfigNullable { get => Config!; set => Config = value!; } + [DataMember(Name = "hideLabel")] public bool HideLabel { get; set; } diff --git a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs index d61dcd0e98..cbcb945c77 100644 --- a/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IConfigurationEditor.cs @@ -73,8 +73,13 @@ public interface IConfigurationEditor /// Converts the configuration object to values for the configuration editor. /// /// The configuration. + [Obsolete("The value type parameter of the dictionary will be made nullable in V11, use ToConfigurationEditorNullable.")] IDictionary ToConfigurationEditor(object? configuration); + // TODO: Obsolete in V11. + IDictionary ToConfigurationEditorNullable(object? configuration) => + ToConfigurationEditor(configuration)!; + /// /// Converts the configuration object to values for the value editor. /// diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 3c05e56ac6..1c874b2efa 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -51,6 +51,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0.DataTypes; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Mappers; using Umbraco.Cms.Infrastructure.Runtime; +using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; @@ -63,7 +64,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection; public static partial class UmbracoBuilderExtensions { /// - /// Adds all core Umbraco services required to run which may be replaced later in the pipeline + /// Adds all core Umbraco services required to run which may be replaced later in the pipeline. /// public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builder) { @@ -83,6 +84,14 @@ public static partial class UmbracoBuilderExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + // Add runtime mode validation + builder.Services.AddSingleton(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + // composers builder .AddRepositories() @@ -102,9 +111,9 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 658b8dd47d..bd69c7857c 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -3,8 +3,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Cms.Web.Common.DependencyInjection; @@ -16,17 +18,28 @@ public class ReportSiteTask : RecurringHostedServiceBase private static HttpClient _httpClient = new(); private readonly ILogger _logger; private readonly ITelemetryService _telemetryService; + private readonly IRuntimeState _runtimeState; public ReportSiteTask( ILogger logger, - ITelemetryService telemetryService) - : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(1)) + ITelemetryService telemetryService, + IRuntimeState runtimeState) + : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(5)) { _logger = logger; _telemetryService = telemetryService; + _runtimeState = runtimeState; _httpClient = new HttpClient(); } + [Obsolete("Use the constructor that takes IRuntimeState, scheduled for removal in V12")] + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService) + : this(logger, telemetryService, StaticServiceProvider.Instance.GetRequiredService()) + { + } + [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] public ReportSiteTask( ILogger logger, @@ -42,6 +55,12 @@ public class ReportSiteTask : RecurringHostedServiceBase /// public override async Task PerformExecuteAsync(object? state) { + if (_runtimeState.Level is not RuntimeLevel.Run) + { + // We probably haven't installed yet, so we can't get telemetry. + return; + } + if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) { _logger.LogWarning("No telemetry marker found"); @@ -66,7 +85,8 @@ public class ReportSiteTask : RecurringHostedServiceBase using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, + "application/json"); // Make a HTTP Post to telemetry service // https://telemetry.umbraco.com/installs/ diff --git a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs index d0ad59fbb8..1ea941932e 100644 --- a/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/DatabaseProviderMetadataExtensions.cs @@ -27,7 +27,7 @@ public static class DatabaseProviderMetadataExtensions /// true if a database can be created for the specified provider name; otherwise, false. /// public static bool CanForceCreateDatabase(this IEnumerable databaseProviderMetadata, string? providerName) - => databaseProviderMetadata.FirstOrDefault(x => x.ProviderName == providerName)?.ForceCreateDatabase == true; + => databaseProviderMetadata.FirstOrDefault(x => string.Equals(x.ProviderName, providerName, StringComparison.InvariantCultureIgnoreCase))?.ForceCreateDatabase == true; /// /// Generates the connection string. diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 05b8a41f1e..acb90da5b2 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -40,10 +40,10 @@ public class DbProviderFactoryCreator : IDbProviderFactoryCreator { _getFactory = getFactory; _providerSpecificInterceptors = providerSpecificInterceptors; - _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName); - _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName); - _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName); - _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName); + _databaseCreators = databaseCreators.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _syntaxProviders = syntaxProviders.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); + _providerSpecificMapperFactories = providerSpecificMapperFactories.ToDictionary(x => x.ProviderName, StringComparer.InvariantCultureIgnoreCase); } public DbProviderFactory? CreateFactory(string? providerName) @@ -98,5 +98,5 @@ public class DbProviderFactoryCreator : IDbProviderFactoryCreator } public IEnumerable GetProviderSpecificInterceptors(string providerName) - => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); + => _providerSpecificInterceptors.Where(x => x.ProviderName.Equals(providerName, StringComparison.InvariantCultureIgnoreCase)); } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs index 7948164280..9ab958c306 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/LanguageFactory.cs @@ -1,3 +1,4 @@ +using System.Globalization; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -8,11 +9,13 @@ internal static class LanguageFactory public static ILanguage BuildEntity(LanguageDto dto) { ArgumentNullException.ThrowIfNull(dto); - if (dto.IsoCode == null || dto.CultureName == null) + if (dto.IsoCode is null) { - throw new InvalidOperationException("Language ISO code and/or culture name can't be null."); + throw new InvalidOperationException("Language ISO code can't be null."); } + dto.CultureName ??= CultureInfo.GetCultureInfo(dto.IsoCode).EnglishName; + var lang = new Language(dto.IsoCode, dto.CultureName) { Id = dto.Id, diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs index e96856d0b8..26b8d55f96 100644 --- a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs @@ -11,7 +11,7 @@ internal class FileSystemMainDomLock : IMainDomLock { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IHostingEnvironment _hostingEnvironment; - private readonly IOptionsMonitor _globalSettings; + private readonly IOptionsMonitor _globalSettings; private readonly string _lockFilePath; private readonly ILogger _logger; private readonly string _releaseSignalFilePath; @@ -27,7 +27,7 @@ internal class FileSystemMainDomLock : IMainDomLock { _logger = logger; _hostingEnvironment = hostingEnvironment; - _globalSettings = globalSettings; + _globalSettings = globalSettings; var lockFileName = $"MainDom_{mainDomKeyGenerator.GenerateKey()}.lock"; _lockFilePath = Path.Combine(hostingEnvironment.LocalTempPath, lockFileName); @@ -43,7 +43,8 @@ internal class FileSystemMainDomLock : IMainDomLock { try { - Directory.CreateDirectory(_hostingEnvironment.LocalTempPath);_logger.LogDebug("Attempting to obtain MainDom lock file handle {lockFilePath}", _lockFilePath); + Directory.CreateDirectory(_hostingEnvironment.LocalTempPath); + _logger.LogDebug("Attempting to obtain MainDom lock file handle {lockFilePath}", _lockFilePath); _lockFileStream = File.Open(_lockFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); DeleteLockReleaseSignalFile(); return Task.FromResult(true); @@ -64,8 +65,7 @@ internal class FileSystemMainDomLock : IMainDomLock _lockFileStream?.Close(); return Task.FromResult(false); } - } - while (stopwatch.ElapsedMilliseconds < millisecondsTimeout); + } while (stopwatch.ElapsedMilliseconds < millisecondsTimeout); return Task.FromResult(false); } @@ -94,7 +94,8 @@ internal class FileSystemMainDomLock : IMainDomLock } public void CreateLockReleaseSignalFile() => - File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete) + File.Open(_releaseSignalFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, + FileShare.ReadWrite | FileShare.Delete) .Close(); public void DeleteLockReleaseSignalFile() => diff --git a/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs new file mode 100644 index 0000000000..3741c4065d --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs @@ -0,0 +1,18 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Provides a service to validate configuration based on the runtime mode. +/// +public interface IRuntimeModeValidationService +{ + /// + /// Validates configuration based on the runtime mode. + /// + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + bool Validate([NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs new file mode 100644 index 0000000000..dcfc39ed83 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Validates configuration based on the runtime mode. +/// +public interface IRuntimeModeValidator +{ + /// + /// Validates configuration based on the specified . + /// + /// The runtime mode. + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs new file mode 100644 index 0000000000..85eec91786 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +internal class RuntimeModeValidationService : IRuntimeModeValidationService +{ + private readonly IOptionsMonitor _runtimeSettings; + private readonly IServiceProvider _serviceProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime settings. + /// The service provider. + public RuntimeModeValidationService(IOptionsMonitor runtimeSettings, IServiceProvider serviceProvider) + { + _runtimeSettings = runtimeSettings; + _serviceProvider = serviceProvider; + } + + /// + public bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + var runtimeMode = _runtimeSettings.CurrentValue.Mode; + var validationMessages = new List(); + + // Runtime mode validators are registered transient, but this service is registered as singleton + foreach (var runtimeModeValidator in _serviceProvider.GetServices()) + { + if (runtimeModeValidator.Validate(runtimeMode, out var validationMessage) == false) + { + validationMessages.Add(validationMessage); + } + } + + if (validationMessages.Count > 0) + { + validationErrorMessage = $"Runtime mode validation failed for {runtimeMode}:\n" + string.Join("\n", validationMessages); + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs new file mode 100644 index 0000000000..d075001ecd --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the JIT/runtime optimizer of the entry assembly is enabled in production runtime mode. +/// +/// +/// This can be ensured by building the application using the Release configuration. +/// +/// +public class JITOptimizerValidator : RuntimeModeProductionValidatorBase +{ + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + DebuggableAttribute? debuggableAttribute = Assembly.GetEntryAssembly()?.GetCustomAttribute(); + if (debuggableAttribute != null && debuggableAttribute.IsJITOptimizerDisabled) + { + validationErrorMessage = "The JIT/runtime optimizer of the entry assembly needs to be enabled in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs new file mode 100644 index 0000000000..06f7735d60 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the ModelsBuilder mode is not set to when in development runtime mode and set to when in production runtime mode. +/// +/// +public class ModelsBuilderModeValidator : IRuntimeModeValidator +{ + private readonly IOptionsMonitor _modelsBuilderSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The models builder settings. + public ModelsBuilderModeValidator(IOptionsMonitor modelsBuilderSettings) + => _modelsBuilderSettings = modelsBuilderSettings; + + /// + public bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage) + { + ModelsMode modelsMode = _modelsBuilderSettings.CurrentValue.ModelsMode; + + if (runtimeMode == RuntimeMode.Development && modelsMode == ModelsMode.InMemoryAuto) + { + validationErrorMessage = "ModelsBuilder mode cannot be set to InMemoryAuto in development mode."; + return false; + } + + if (runtimeMode == RuntimeMode.Production && modelsMode != ModelsMode.Nothing) + { + validationErrorMessage = "ModelsBuilder mode needs to be set to Nothing in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs new file mode 100644 index 0000000000..01bc0dd3dc --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether the runtime minification cache buster is not set to when in production runtime mode. +/// +/// +public class RuntimeMinificationValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _runtimeMinificationSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The runtime minification settings. + public RuntimeMinificationValidator(IOptionsMonitor runtimeMinificationSettings) + => _runtimeMinificationSettings = runtimeMinificationSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (_runtimeMinificationSettings.CurrentValue.CacheBuster == RuntimeMinificationCacheBuster.Timestamp) + { + validationErrorMessage = "Runtime minification setting needs to be set to a fixed cache buster (like Version or AppDomain) in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs new file mode 100644 index 0000000000..7d23c0138b --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates configuration based on the production runtime mode. +/// +/// +public abstract class RuntimeModeProductionValidatorBase : IRuntimeModeValidator +{ + /// + public bool Validate(RuntimeMode runtimeMode, [NotNullWhen(false)] out string? validationErrorMessage) + { + if (runtimeMode == RuntimeMode.Production) + { + return Validate(out validationErrorMessage); + } + + validationErrorMessage = null; + return true; + } + + /// + /// Validates configuration based on the production runtime mode. + /// + /// The validation error message. + /// + /// true when the validation passes; otherwise, false. + /// + protected abstract bool Validate([NotNullWhen(false)] out string? validationErrorMessage); +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs new file mode 100644 index 0000000000..7d990dda5d --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether a fixed Umbraco application URL is set when in production runtime mode. +/// +/// +public class UmbracoApplicationUrlValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _webRoutingSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The web routing settings. + public UmbracoApplicationUrlValidator(IOptionsMonitor webRoutingSettings) + => _webRoutingSettings = webRoutingSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (string.IsNullOrWhiteSpace(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl)) + { + validationErrorMessage = "Umbraco application URL needs to be set in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs new file mode 100644 index 0000000000..1a02581ae6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators; + +/// +/// Validates whether HTTPS is enforced when in production runtime mode. +/// +/// +public class UseHttpsValidator : RuntimeModeProductionValidatorBase +{ + private readonly IOptionsMonitor _globalSettings; + + /// + /// Initializes a new instance of the class. + /// + /// The global settings. + public UseHttpsValidator(IOptionsMonitor globalSettings) + => _globalSettings = globalSettings; + + /// + protected override bool Validate([NotNullWhen(false)] out string? validationErrorMessage) + { + if (!_globalSettings.CurrentValue.UseHttps) + { + validationErrorMessage = "Using HTTPS should be enforced in production mode."; + return false; + } + + validationErrorMessage = null; + return true; + } +} diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 88c5a9389b..74b00d3644 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -12,151 +12,200 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; -namespace Umbraco.Cms.Infrastructure.Runtime +namespace Umbraco.Cms.Infrastructure.Runtime; + +/// +/// Represents the state of the Umbraco runtime. +/// +public class RuntimeState : IRuntimeState { + internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations"; + + private readonly IOptions _globalSettings = null!; + private readonly IOptions _unattendedSettings = null!; + private readonly IUmbracoVersion _umbracoVersion = null!; + private readonly IUmbracoDatabaseFactory _databaseFactory = null!; + private readonly ILogger _logger = null!; + private readonly PendingPackageMigrations _packageMigrationState = null!; + private readonly Dictionary _startupState = new Dictionary(); + private readonly IConflictingRouteService _conflictingRouteService = null!; + private readonly IEnumerable _databaseProviderMetadata = null!; + private readonly IRuntimeModeValidationService _runtimeModeValidationService = null!; /// - /// Represents the state of the Umbraco runtime. + /// The initial + /// The initial /// - public class RuntimeState : IRuntimeState + public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; + + /// + /// Initializes a new instance of the class. + /// + private RuntimeState() + { } + + /// + /// Initializes a new instance of the class. + /// + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService, + IEnumerable databaseProviderMetadata, + IRuntimeModeValidationService runtimeModeValidationService) { - internal const string PendingPackageMigrationsStateKey = "PendingPackageMigrations"; + _globalSettings = globalSettings; + _unattendedSettings = unattendedSettings; + _umbracoVersion = umbracoVersion; + _databaseFactory = databaseFactory; + _logger = logger; + _packageMigrationState = packageMigrationState; + _conflictingRouteService = conflictingRouteService; + _databaseProviderMetadata = databaseProviderMetadata; + _runtimeModeValidationService = runtimeModeValidationService; + } - private readonly IOptions _globalSettings = null!; - private readonly IOptions _unattendedSettings = null!; - private readonly IUmbracoVersion _umbracoVersion = null!; - private readonly IUmbracoDatabaseFactory _databaseFactory = null!; - private readonly ILogger _logger = null!; - private readonly PendingPackageMigrations _packageMigrationState = null!; - private readonly Dictionary _startupState = new Dictionary(); - private readonly IConflictingRouteService _conflictingRouteService = null!; - private readonly IEnumerable _databaseProviderMetadata = null!; + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")] + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService, + IEnumerable databaseProviderMetadata) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + conflictingRouteService, + databaseProviderMetadata, + StaticServiceProvider.Instance.GetRequiredService()) + { } - /// - /// The initial - /// The initial - /// - public static RuntimeState Booting() => new RuntimeState() { Level = RuntimeLevel.Boot }; + [Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")] + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetServices()) + { } - private RuntimeState() - { } + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params. This will be removed in Umbraco 12.")] + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService()) + { } - public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState, - IConflictingRouteService conflictingRouteService, - IEnumerable databaseProviderMetadata) + /// + public Version Version => _umbracoVersion.Version; + + /// + public string VersionComment => _umbracoVersion.Comment; + + /// + public SemVersion SemanticVersion => _umbracoVersion.SemanticVersion; + + /// + public string? CurrentMigrationState { get; private set; } + + /// + public string? FinalMigrationState { get; private set; } + + /// + public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown; + + /// + public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown; + + /// + public BootFailedException? BootFailedException { get; internal set; } + + /// + public IReadOnlyDictionary StartupState => _startupState; + + /// + public void DetermineRuntimeLevel() + { + if (_databaseFactory.Configured == false) { - _globalSettings = globalSettings; - _unattendedSettings = unattendedSettings; - _umbracoVersion = umbracoVersion; - _databaseFactory = databaseFactory; - _logger = logger; - _packageMigrationState = packageMigrationState; - _conflictingRouteService = conflictingRouteService; - _databaseProviderMetadata = databaseProviderMetadata; + // local version *does* match code version, but the database is not configured + // install - may happen with Deploy/Cloud/etc + _logger.LogDebug("Database is not configured, need to install Umbraco."); + + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallNoDatabase; + + return; } - [Obsolete("Use ctor with all params. This will be removed in Umbraco 12")] - public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState, - IConflictingRouteService conflictingRouteService) - : this( - globalSettings, - unattendedSettings, - umbracoVersion, - databaseFactory, - logger, - packageMigrationState, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetServices()) - { } - - /// - /// Initializes a new instance of the class. - /// - [Obsolete("use ctor with all params")] - public RuntimeState( - IOptions globalSettings, - IOptions unattendedSettings, - IUmbracoVersion umbracoVersion, - IUmbracoDatabaseFactory databaseFactory, - ILogger logger, - PendingPackageMigrations packageMigrationState) - : this( - globalSettings, - unattendedSettings, - umbracoVersion, - databaseFactory, - logger, - packageMigrationState, - StaticServiceProvider.Instance.GetRequiredService()) - { } - - /// - public Version Version => _umbracoVersion.Version; - - /// - public string VersionComment => _umbracoVersion.Comment; - - /// - public SemVersion SemanticVersion => _umbracoVersion.SemanticVersion; - - /// - public string? CurrentMigrationState { get; private set; } - - /// - public string? FinalMigrationState { get; private set; } - - /// - public RuntimeLevel Level { get; internal set; } = RuntimeLevel.Unknown; - - /// - public RuntimeLevelReason Reason { get; internal set; } = RuntimeLevelReason.Unknown; - - /// - public BootFailedException? BootFailedException { get; internal set; } - - /// - public IReadOnlyDictionary StartupState => _startupState; - - /// - public void DetermineRuntimeLevel() + // Validate runtime mode + if (_runtimeModeValidationService.Validate(out var validationErrorMessage) == false) { - if (_databaseFactory.Configured == false) - { - // local version *does* match code version, but the database is not configured - // install - may happen with Deploy/Cloud/etc - _logger.LogDebug("Database is not configured, need to install Umbraco."); - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallNoDatabase; - return; - } + _logger.LogError(validationErrorMessage); - // Check if we have multiple controllers with the same name. - if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) - { - Level = RuntimeLevel.BootFailed; - Reason = RuntimeLevelReason.BootFailedOnException; - BootFailedException = new BootFailedException($"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"); + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException(validationErrorMessage); - return; - } + return; + } - // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... + // Check if we have multiple controllers with the same name. + if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) + { + var message = $"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"; + _logger.LogError(message); - switch (GetUmbracoDatabaseState(_databaseFactory)) - { - case UmbracoDatabaseState.CannotConnect: + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException(message); + + return; + } + + // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... + switch (GetUmbracoDatabaseState(_databaseFactory)) + { + case UmbracoDatabaseState.CannotConnect: { // cannot connect to configured database, this is bad, fail _logger.LogDebug("Could not connect to database."); @@ -174,14 +223,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime BootFailedException = new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); throw BootFailedException; } - case UmbracoDatabaseState.NotInstalled: + case UmbracoDatabaseState.NotInstalled: { // ok to install on an empty database Level = RuntimeLevel.Install; Reason = RuntimeLevelReason.InstallEmptyDatabase; return; } - case UmbracoDatabaseState.NeedsUpgrade: + case UmbracoDatabaseState.NeedsUpgrade: { // the db version does not match... but we do have a migration table // so, at least one valid table, so we quite probably are installed & need to upgrade @@ -193,26 +242,26 @@ namespace Umbraco.Cms.Infrastructure.Runtime Reason = RuntimeLevelReason.UpgradeMigrations; } break; - case UmbracoDatabaseState.NeedsPackageMigration: + case UmbracoDatabaseState.NeedsPackageMigration: - // no matter what the level is run for package migrations. - // they either run unattended, or only manually via the back office. - Level = RuntimeLevel.Run; + // no matter what the level is run for package migrations. + // they either run unattended, or only manually via the back office. + Level = RuntimeLevel.Run; - if (_unattendedSettings.Value.PackageMigrationsUnattended) - { - _logger.LogDebug("Package migrations need to execute."); - Reason = RuntimeLevelReason.UpgradePackageMigrations; - } - else - { - _logger.LogInformation("Package migrations need to execute but unattended package migrations is disabled. They will need to be run from the back office."); - Reason = RuntimeLevelReason.Run; - } + if (_unattendedSettings.Value.PackageMigrationsUnattended) + { + _logger.LogDebug("Package migrations need to execute."); + Reason = RuntimeLevelReason.UpgradePackageMigrations; + } + else + { + _logger.LogInformation("Package migrations need to execute but unattended package migrations is disabled. They will need to be run from the back office."); + Reason = RuntimeLevelReason.Run; + } - break; - case UmbracoDatabaseState.Ok: - default: + break; + case UmbracoDatabaseState.Ok: + default: { @@ -221,116 +270,115 @@ namespace Umbraco.Cms.Infrastructure.Runtime Reason = RuntimeLevelReason.Run; } break; - } - } - - public void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null) - { - Level = level; - Reason = reason; - - if (bootFailedException != null) - { - BootFailedException = new BootFailedException(bootFailedException.Message, bootFailedException); - } - } - - private enum UmbracoDatabaseState - { - Ok, - CannotConnect, - NotInstalled, - NeedsUpgrade, - NeedsPackageMigration - } - - private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) - { - try - { - if (!TryDbConnect(databaseFactory)) - { - return UmbracoDatabaseState.CannotConnect; - } - - // no scope, no service - just directly accessing the database - using (IUmbracoDatabase database = databaseFactory.CreateDatabase()) - { - if (!database.IsUmbracoInstalled()) - { - return UmbracoDatabaseState.NotInstalled; - } - - // Make ONE SQL call to determine Umbraco upgrade vs package migrations state. - // All will be prefixed with the same key. - IReadOnlyDictionary? keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); - - // This could need both an upgrade AND package migrations to execute but - // we will process them one at a time, first the upgrade, then the package migrations. - if (DoesUmbracoRequireUpgrade(keyValues)) - { - return UmbracoDatabaseState.NeedsUpgrade; - } - - IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); - if (packagesRequiringMigration.Count > 0) - { - _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; - - return UmbracoDatabaseState.NeedsPackageMigration; - } - } - - return UmbracoDatabaseState.Ok; - } - catch (Exception e) - { - // can connect to the database so cannot check the upgrade state... oops - _logger.LogWarning(e, "Could not check the upgrade state."); - - // else it is bad enough that we want to throw - Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; - BootFailedException = new BootFailedException("Could not check the upgrade state.", e); - throw BootFailedException; - } - } - - private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary? keyValues) - { - var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); - var stateValueKey = upgrader.StateValueKey; - - if(keyValues?.TryGetValue(stateValueKey, out var value) ?? false) - { - CurrentMigrationState = value; - } - - FinalMigrationState = upgrader.Plan.FinalState; - - _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); - - return CurrentMigrationState != FinalMigrationState; - } - - private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) - { - // anything other than install wants a database - see if we can connect - // (since this is an already existing database, assume localdb is ready) - bool canConnect; - var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; - for (var i = 0; ;) - { - canConnect = databaseFactory.CanConnect; - if (canConnect || ++i == tries) - { - break; - } - - _logger.LogDebug("Could not immediately connect to database, trying again."); - Thread.Sleep(1000); - } - - return canConnect; } } + + public void Configure(RuntimeLevel level, RuntimeLevelReason reason, Exception? bootFailedException = null) + { + Level = level; + Reason = reason; + + if (bootFailedException != null) + { + BootFailedException = new BootFailedException(bootFailedException.Message, bootFailedException); + } + } + + private enum UmbracoDatabaseState + { + Ok, + CannotConnect, + NotInstalled, + NeedsUpgrade, + NeedsPackageMigration + } + + private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) + { + try + { + if (!TryDbConnect(databaseFactory)) + { + return UmbracoDatabaseState.CannotConnect; + } + + // no scope, no service - just directly accessing the database + using (IUmbracoDatabase database = databaseFactory.CreateDatabase()) + { + if (!database.IsUmbracoInstalled()) + { + return UmbracoDatabaseState.NotInstalled; + } + + // Make ONE SQL call to determine Umbraco upgrade vs package migrations state. + // All will be prefixed with the same key. + IReadOnlyDictionary? keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); + + // This could need both an upgrade AND package migrations to execute but + // we will process them one at a time, first the upgrade, then the package migrations. + if (DoesUmbracoRequireUpgrade(keyValues)) + { + return UmbracoDatabaseState.NeedsUpgrade; + } + + IReadOnlyList packagesRequiringMigration = _packageMigrationState.GetPendingPackageMigrations(keyValues); + if (packagesRequiringMigration.Count > 0) + { + _startupState[PendingPackageMigrationsStateKey] = packagesRequiringMigration; + + return UmbracoDatabaseState.NeedsPackageMigration; + } + } + + return UmbracoDatabaseState.Ok; + } + catch (Exception e) + { + // can connect to the database so cannot check the upgrade state... oops + _logger.LogWarning(e, "Could not check the upgrade state."); + + // else it is bad enough that we want to throw + Reason = RuntimeLevelReason.BootFailedCannotCheckUpgradeState; + BootFailedException = new BootFailedException("Could not check the upgrade state.", e); + throw BootFailedException; + } + } + + private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary? keyValues) + { + var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); + var stateValueKey = upgrader.StateValueKey; + + if (keyValues?.TryGetValue(stateValueKey, out var value) ?? false) + { + CurrentMigrationState = value; + } + + FinalMigrationState = upgrader.Plan.FinalState; + + _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); + + return CurrentMigrationState != FinalMigrationState; + } + + private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) + { + // anything other than install wants a database - see if we can connect + // (since this is an already existing database, assume localdb is ready) + bool canConnect; + var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; + for (var i = 0; ;) + { + canConnect = databaseFactory.CanConnect; + if (canConnect || ++i == tries) + { + break; + } + + _logger.LogDebug("Could not immediately connect to database, trying again."); + Thread.Sleep(1000); + } + + return canConnect; + } } diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index c64bd77b92..7a440ef768 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -87,6 +87,12 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab IPublishedContent? content; + if ((!_globalSettings.ForceCombineUrlPathLeftToRight + && CultureInfo.GetCultureInfo(culture ?? _globalSettings.DefaultUILanguage).TextInfo.IsRightToLeft)) + { + parts = parts.Reverse().ToArray(); + } + if (startNodeId > 0) { // if in a domain then start with the root node of the domain @@ -190,8 +196,13 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab ApplyHideTopLevelNodeFromPath(node, pathParts, preview); } - // assemble the route - pathParts.Reverse(); + // assemble the route- We only have to reverse for left to right languages + if ((_globalSettings.ForceCombineUrlPathLeftToRight + || !CultureInfo.GetCultureInfo(culture ?? _globalSettings.DefaultUILanguage).TextInfo.IsRightToLeft)) + { + pathParts.Reverse(); + } + var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc // prefix the root node id containing the domain if it exists (this is a standard way of creating route paths) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs index 2d18905ee9..54e25240e0 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs @@ -37,6 +37,8 @@ namespace Umbraco.Extensions IFileProvider webFileProvider = webHostEnvironment.WebRootFileProvider; IFileProvider contentFileProvider = webHostEnvironment.ContentRootFileProvider; + IEnumerable localPluginFileSources = GetPluginLanguageFileSources(contentFileProvider, Cms.Core.Constants.SystemDirectories.AppPlugins, false); + // gets all langs files in /app_plugins real or virtual locations IEnumerable pluginLangFileSources = GetPluginLanguageFileSources(webFileProvider, Cms.Core.Constants.SystemDirectories.AppPlugins, false); @@ -50,7 +52,9 @@ namespace Umbraco.Extensions .SelectMany(x => x.GetFiles("*.user.xml", SearchOption.TopDirectoryOnly)) .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); - return pluginLangFileSources + return + localPluginFileSources + .Concat(pluginLangFileSources) .Concat(userLangFileSources); } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index b72de65682..0d8cc49bdf 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -23,39 +23,38 @@ using Umbraco.Cms.Web.BackOffice.Trees; namespace Umbraco.Extensions; /// -/// Extension methods for for the Umbraco back office +/// Extension methods for for the Umbraco back office /// public static partial class UmbracoBuilderExtensions { /// - /// Adds all required components to run the Umbraco back office + /// Adds all required components to run the Umbraco back office /// - public static IUmbracoBuilder - AddBackOffice(this IUmbracoBuilder builder, Action? configureMvc = null) => builder - .AddConfiguration() - .AddUmbracoCore() - .AddWebComponents() - .AddRuntimeMinifier() - .AddBackOfficeCore() - .AddBackOfficeAuthentication() - .AddBackOfficeIdentity() - .AddMembersIdentity() - .AddBackOfficeAuthorizationPolicies() - .AddUmbracoProfiler() - .AddMvcAndRazor(configureMvc) - .AddWebServer() - .AddPreviewSupport() - .AddHostedServices() - .AddNuCache() - .AddDistributedCache() - .AddModelsBuilderDashboard() - .AddUnattendedInstallInstallCreateUser() - .AddCoreNotifications() - .AddLogViewer() - .AddExamine() - .AddExamineIndexes() - .AddControllersWithAmbiguousConstructors() - .AddSupplemenataryLocalizedTextFileSources(); + public static IUmbracoBuilder AddBackOffice(this IUmbracoBuilder builder, Action? configureMvc = null) => builder + .AddConfiguration() + .AddUmbracoCore() + .AddWebComponents() + .AddRuntimeMinifier() + .AddBackOfficeCore() + .AddBackOfficeAuthentication() + .AddBackOfficeIdentity() + .AddMembersIdentity() + .AddBackOfficeAuthorizationPolicies() + .AddUmbracoProfiler() + .AddMvcAndRazor(configureMvc) + .AddWebServer() + .AddPreviewSupport() + .AddHostedServices() + .AddNuCache() + .AddDistributedCache() + .TryAddModelsBuilderDashboard() + .AddUnattendedInstallInstallCreateUser() + .AddCoreNotifications() + .AddLogViewer() + .AddExamine() + .AddExamineIndexes() + .AddControllersWithAmbiguousConstructors() + .AddSupplemenataryLocalizedTextFileSources(); public static IUmbracoBuilder AddUnattendedInstallInstallCreateUser(this IUmbracoBuilder builder) { diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 8c4db1a041..3121e654af 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -1,13 +1,16 @@ using System.Collections; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Filters; @@ -25,22 +28,34 @@ internal sealed class OutgoingEditorModelEventAttribute : TypeFilterAttribute private class OutgoingEditorModelEventFilter : IActionFilter { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IEventAggregator _eventAggregator; - + private readonly IUmbracoMapper _mapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; + [ActivatorUtilitiesConstructor] + public OutgoingEditorModelEventFilter( + IUmbracoContextAccessor umbracoContextAccessor, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IEventAggregator eventAggregator, + IUmbracoMapper mapper) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _backOfficeSecurityAccessor = backOfficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backOfficeSecurityAccessor)); + _eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator)); + _mapper = mapper; + } + + [Obsolete("Please use constructor that takes an IUmbracoMapper, scheduled for removal in V12")] public OutgoingEditorModelEventFilter( IUmbracoContextAccessor umbracoContextAccessor, IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IEventAggregator eventAggregator) + : this( + umbracoContextAccessor, + backOfficeSecurityAccessor, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) { - _umbracoContextAccessor = umbracoContextAccessor - ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _backOfficeSecurityAccessor = backOfficeSecurityAccessor - ?? throw new ArgumentNullException(nameof(backOfficeSecurityAccessor)); - _eventAggregator = eventAggregator - ?? throw new ArgumentNullException(nameof(eventAggregator)); } public void OnActionExecuted(ActionExecutedContext context) @@ -77,28 +92,48 @@ internal sealed class OutgoingEditorModelEventAttribute : TypeFilterAttribute case ContentItemDisplay content: _eventAggregator.Publish(new SendingContentNotification(content, umbracoContext)); break; + + case ContentItemDisplayWithSchedule contentWithSchedule: + // This is a bit weird, since ContentItemDisplayWithSchedule was introduced later, + // the SendingContentNotification only accepts ContentItemDisplay, + // which means we have to map it to this before sending the notification. + ContentItemDisplay? display = _mapper.Map(contentWithSchedule); + if (display is null) + { + // This will never happen. + break; + } + + // Now that the display is mapped to the non-schedule one we can publish the notification. + _eventAggregator.Publish(new SendingContentNotification(display, umbracoContext)); + + // We want the changes the handler makes to take effect. + // So we have to map these changes back to the existing ContentItemWithSchedule. + // To avoid losing the schedule information we add the old variants to context. + _mapper.Map(display, contentWithSchedule, mapperContext => mapperContext.Items[nameof(contentWithSchedule.Variants)] = contentWithSchedule.Variants); + break; case MediaItemDisplay media: - _eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext)); - break; - case MemberDisplay member: - _eventAggregator.Publish(new SendingMemberNotification(member, umbracoContext)); - break; - case UserDisplay user: - _eventAggregator.Publish(new SendingUserNotification(user, umbracoContext)); - break; - case IEnumerable> dashboards: - _eventAggregator.Publish(new SendingDashboardsNotification(dashboards, umbracoContext)); - break; - case IEnumerable allowedChildren: - // Changing the Enumerable will generate a new instance, so we need to update the context result with the new content - var notification = new SendingAllowedChildrenNotification(allowedChildren, umbracoContext); - _eventAggregator.Publish(notification); - context.Result = new ObjectResult(notification.Children); - break; + _eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext)); + break; + case MemberDisplay member: + _eventAggregator.Publish(new SendingMemberNotification(member, umbracoContext)); + break; + case UserDisplay user: + _eventAggregator.Publish(new SendingUserNotification(user, umbracoContext)); + break; + case IEnumerable> dashboards: + _eventAggregator.Publish(new SendingDashboardsNotification(dashboards, umbracoContext)); + break; + case IEnumerable allowedChildren: + // Changing the Enumerable will generate a new instance, so we need to update the context result with the new content + var notification = new SendingAllowedChildrenNotification(allowedChildren, umbracoContext); + _eventAggregator.Publish(notification); + context.Result = new ObjectResult(notification.Children); + break; + } } } } - } public void OnActionExecuting(ActionExecutingContext context) { diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index cd58aadb05..5d58642de9 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -96,16 +96,138 @@ internal class ContentMapDefinition : IMapDefinition public void DefineMaps(IUmbracoMapper mapper) { - mapper.Define>( - (source, context) => new ContentItemBasic(), Map); + mapper.Define>((source, context) => new ContentItemBasic(), Map); mapper.Define((source, context) => new ContentPropertyCollectionDto(), Map); mapper.Define((source, context) => new ContentItemDisplay(), Map); - mapper.Define( - (source, context) => new ContentItemDisplayWithSchedule(), Map); + mapper.Define((source, context) => new ContentItemDisplayWithSchedule(), Map); mapper.Define((source, context) => new ContentVariantDisplay(), Map); mapper.Define((source, context) => new ContentVariantScheduleDisplay(), Map); + + mapper.Define((source, context) => new ContentItemDisplay(), Map); + mapper.Define((source, context) => new ContentItemDisplayWithSchedule(), Map); + + mapper.Define((source, context) => new ContentVariantScheduleDisplay(), Map); + mapper.Define((source, context) => new ContentVariantDisplay(), Map); + } + + // Umbraco.Code.MapAll + private void Map(ContentVariantScheduleDisplay source, ContentVariantDisplay target, MapperContext context) + { + target.CreateDate = source.CreateDate; + target.DisplayName = source.DisplayName; + target.Language = source.Language; + target.Name = source.Name; + target.PublishDate = source.PublishDate; + target.Segment = source.Segment; + target.State = source.State; + target.Tabs = source.Tabs; + target.UpdateDate = source.UpdateDate; + } + + // Umbraco.Code.MapAll + private void Map(ContentItemDisplay source, ContentItemDisplayWithSchedule target, MapperContext context) + { + target.AllowedActions = source.AllowedActions; + target.AllowedTemplates = source.AllowedTemplates; + target.AllowPreview = source.AllowPreview; + target.ContentApps = source.ContentApps; + target.ContentDto = source.ContentDto; + target.ContentTypeAlias = source.ContentTypeAlias; + target.ContentTypeId = source.ContentTypeId; + target.ContentTypeKey = source.ContentTypeKey; + target.ContentTypeName = source.ContentTypeName; + target.DocumentType = source.DocumentType; + target.Errors = source.Errors; + target.Icon = source.Icon; + target.Id = source.Id; + target.IsBlueprint = source.IsBlueprint; + target.IsChildOfListView = source.IsChildOfListView; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Owner = source.Owner; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PersistedContent = source.PersistedContent; + target.SortOrder = source.SortOrder; + target.TemplateAlias = source.TemplateAlias; + target.TemplateId = source.TemplateId; + target.Trashed = source.Trashed; + target.TreeNodeUrl = source.TreeNodeUrl; + target.Udi = source.Udi; + target.UpdateDate = source.UpdateDate; + target.Updater = source.Updater; + target.Urls = source.Urls; + target.Variants = context.MapEnumerable(source.Variants); + } + + // Umbraco.Code.MapAll + private void Map(ContentVariantDisplay source, ContentVariantScheduleDisplay target, MapperContext context) + { + target.CreateDate = source.CreateDate; + target.DisplayName = source.DisplayName; + target.Language = source.Language; + target.Name = source.Name; + target.PublishDate = source.PublishDate; + target.Segment = source.Segment; + target.State = source.State; + target.Tabs = source.Tabs; + target.UpdateDate = source.UpdateDate; + + // We'll only try and map the ReleaseDate/ExpireDate if the "old" ContentVariantScheduleDisplay is in the context, otherwise we'll just skip it quietly. + _ = context.Items.TryGetValue(nameof(ContentItemDisplayWithSchedule.Variants), out var variants); + if (variants is IEnumerable scheduleDisplays) + { + ContentVariantScheduleDisplay? item = scheduleDisplays.FirstOrDefault(x => x.Language?.Id == source.Language?.Id && x.Segment == source.Segment); + + if (item is null) + { + // If we can't find the old variants display, we'll just not try and map it. + return; + } + + target.ReleaseDate = item.ReleaseDate; + target.ExpireDate = item.ExpireDate; + } + } + + // Umbraco.Code.MapAll + private static void Map(ContentItemDisplayWithSchedule source, ContentItemDisplay target, MapperContext context) + { + target.AllowedActions = source.AllowedActions; + target.AllowedTemplates = source.AllowedTemplates; + target.AllowPreview = source.AllowPreview; + target.ContentApps = source.ContentApps; + target.ContentDto = source.ContentDto; + target.ContentTypeAlias = source.ContentTypeAlias; + target.ContentTypeId = source.ContentTypeId; + target.ContentTypeKey = source.ContentTypeKey; + target.ContentTypeName = source.ContentTypeName; + target.DocumentType = source.DocumentType; + target.Errors = source.Errors; + target.Icon = source.Icon; + target.Id = source.Id; + target.IsBlueprint = source.IsBlueprint; + target.IsChildOfListView = source.IsChildOfListView; + target.IsContainer = source.IsContainer; + target.IsElement = source.IsElement; + target.Key = source.Key; + target.Owner = source.Owner; + target.ParentId = source.ParentId; + target.Path = source.Path; + target.PersistedContent = source.PersistedContent; + target.SortOrder = source.SortOrder; + target.TemplateAlias = source.TemplateAlias; + target.TemplateId = source.TemplateId; + target.Trashed = source.Trashed; + target.TreeNodeUrl = source.TreeNodeUrl; + target.Udi = source.Udi; + target.UpdateDate = source.UpdateDate; + target.Updater = source.Updater; + target.Urls = source.Urls; + target.Variants = source.Variants; } // Umbraco.Code.MapAll diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs index 970fe7e778..2e11af0d1f 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/DisableModelsBuilderNotificationHandler.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder; /// -/// Used in conjunction with +/// Used in conjunction with /// internal class DisableModelsBuilderNotificationHandler : INotificationHandler { diff --git a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs index f9bdcd1b77..a0a0aeec8c 100644 --- a/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/ModelsBuilder/UmbracoBuilderExtensions.cs @@ -1,30 +1,59 @@ -using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.ModelsBuilder; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.ModelsBuilder; /// -/// Extension methods for for the common Umbraco functionality +/// Extension methods for for the common Umbraco functionality /// public static class UmbracoBuilderExtensions { /// - /// Adds the ModelsBuilder dashboard. + /// Adds the ModelsBuilder dashboard, but only when not in production mode. /// - public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) + internal static IUmbracoBuilder TryAddModelsBuilderDashboard(this IUmbracoBuilder builder) { - builder.Services.AddUnique(); + if (builder.Config.GetRuntimeMode() == RuntimeMode.Production) + { + builder.RemoveModelsBuilderDashboard(); + } + else + { + builder.AddModelsBuilderDashboard(); + } + return builder; } /// - /// Can be called if using an external models builder to remove the embedded models builder controller features + /// Adds the ModelsBuilder dashboard (dashboard and API controller are automatically added). /// - public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + public static IUmbracoBuilder AddModelsBuilderDashboard(this IUmbracoBuilder builder) { - builder.Services.AddSingleton(); + builder.Services.AddUnique(); + return builder; } + + /// + /// Removes the ModelsBuilder dashboard (and API controller). + /// + public static IUmbracoBuilder RemoveModelsBuilderDashboard(this IUmbracoBuilder builder) + { + builder.Dashboards().Remove(); + builder.WithCollectionBuilder().Remove(); + + return builder; + } + + /// + /// Can be called if using an external models builder to remove the embedded models builder controller features. + /// + public static IUmbracoBuilder DisableModelsBuilderControllers(this IUmbracoBuilder builder) + => builder.AddNotificationHandler(); } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 666342dc10..40b84a0987 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -224,13 +224,12 @@ public static partial class UmbracoBuilderExtensions { // TODO: We need to figure out if we can work around this because calling AddControllersWithViews modifies the global app and order is very important // this will directly affect developers who need to call that themselves. - // We need to have runtime compilation of views when using umbraco. We could consider having only this when a specific config is set. - // But as far as I can see, there are still precompiled views, even when this is activated, so maybe it is okay. - IMvcBuilder mvcBuilder = builder.Services - .AddControllersWithViews(); + IMvcBuilder mvcBuilder = builder.Services.AddControllersWithViews(); - FixForDotnet6Preview1(builder.Services); - mvcBuilder.AddRazorRuntimeCompilation(); + if (builder.Config.GetRuntimeMode() != RuntimeMode.Production) + { + mvcBuilder.AddRazorRuntimeCompilation(); + } mvcBuilding?.Invoke(mvcBuilder); @@ -421,30 +420,4 @@ public static partial class UmbracoBuilderExtensions wrappedWebRoutingSettings, webHostEnvironment); } - - /// - /// This fixes an issue for .NET6 Preview1, that in AddRazorRuntimeCompilation cannot remove the existing - /// IViewCompilerProvider. - /// - /// - /// When running .NET6 Preview1 there is an issue with looks to be fixed when running ASP.NET Core 6. - /// This issue is because the default implementation of IViewCompilerProvider has changed, so the - /// AddRazorRuntimeCompilation extension can't remove the default and replace with the runtimeviewcompiler. - /// This method basically does the same as the ASP.NET Core 6 version of AddRazorRuntimeCompilation - /// https://github.com/dotnet/aspnetcore/blob/f7dc5e24af7f9692a1db66741954b90b42d84c3a/src/Mvc/Mvc.Razor.RuntimeCompilation/src/DependencyInjection/RazorRuntimeCompilationMvcCoreBuilderExtensions.cs#L71-L80 - /// While running .NET5 this does nothing as the ImplementationType has another FullName, and this is handled by the - /// .NET5 version of AddRazorRuntimeCompilation - /// - private static void FixForDotnet6Preview1(IServiceCollection services) - { - ServiceDescriptor? compilerProvider = services.FirstOrDefault(f => - f.ServiceType == typeof(IViewCompilerProvider) && - f.ImplementationType?.Assembly == typeof(IViewCompilerProvider).Assembly && - f.ImplementationType.FullName == "Microsoft.AspNetCore.Mvc.Razor.Compilation.DefaultViewCompiler"); - - if (compilerProvider != null) - { - services.Remove(compilerProvider); - } - } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs index 7749c4cbc9..95ae91d7b7 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/DependencyInjection/UmbracoBuilderDependencyInjectionExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -81,8 +82,7 @@ public static class UmbracoBuilderDependencyInjectionExtensions /// public static IUmbracoBuilder AddModelsBuilder(this IUmbracoBuilder builder) { - var umbServices = - new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); + var umbServices = new UniqueServiceDescriptor(typeof(UmbracoServices), typeof(UmbracoServices), ServiceLifetime.Singleton); if (builder.Services.Contains(umbServices)) { // if this ext method is called more than once just exit @@ -91,44 +91,35 @@ public static class UmbracoBuilderDependencyInjectionExtensions builder.Services.Add(umbServices); - builder.AddInMemoryModelsRazorEngine(); + if (builder.Config.GetRuntimeMode() == RuntimeMode.BackofficeDevelopment) + { + // Configure services to allow InMemoryAuto mode + builder.AddInMemoryModelsRazorEngine(); - // TODO: I feel like we could just do builder.AddNotificationHandler() and it - // would automatically just register for all implemented INotificationHandler{T}? - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } + if (builder.Config.GetRuntimeMode() != RuntimeMode.Production) + { + // Configure service to allow models generation + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + builder.AddNotificationHandler(); + } + + builder.Services.TryAddSingleton(); + + // Register required services for ModelsBuilderDashboardController builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.AddNotificationHandler(); - builder.AddNotificationHandler(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // This is what the community MB would replace, all of the above services are fine to be registered - // even if the community MB is in place. - builder.Services.AddSingleton(factory => - { - ModelsBuilderSettings config = factory.GetRequiredService>().Value; - if (config.ModelsMode == ModelsMode.InMemoryAuto) - { - return factory.GetRequiredService(); - } - - return factory.CreateDefaultPublishedModelFactory(); - }); - - if (!builder.Services.Any(x => x.ServiceType == typeof(IModelsBuilderDashboardProvider))) - { - builder.Services.AddUnique(); - } - return builder; } @@ -152,6 +143,23 @@ public static class UmbracoBuilderDependencyInjectionExtensions }, s.GetRequiredService())); + builder.Services.AddSingleton(); + + // This is what the community MB would replace, all of the above services are fine to be registered + // even if the community MB is in place. + builder.Services.AddSingleton(factory => + { + ModelsBuilderSettings modelsBuilderSettings = factory.GetRequiredService>().Value; + if (modelsBuilderSettings.ModelsMode == ModelsMode.InMemoryAuto) + { + return factory.GetRequiredService(); + } + else + { + return factory.CreateDefaultPublishedModelFactory(); + } + }); + return builder; } } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs index 1be67c575c..169f6af0f5 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs @@ -2,5 +2,5 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder; public class NoopModelsBuilderDashboardProvider : IModelsBuilderDashboardProvider { - public string GetUrl() => string.Empty; + public string GetUrl() => null!; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 425ebf3a10..26019b53f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -46,7 +46,7 @@ vm.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.canSendRequiredEmail && Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; vm.errorMsg = ""; const tempUrl = new URL(Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl, window.location.origin); - tempUrl.searchParams.append("redirectUrl", $location.search().returnPath ?? "") + tempUrl.searchParams.append("redirectUrl", decodeURIComponent($location.search().returnPath ?? "")) vm.externalLoginFormAction = tempUrl.pathname + tempUrl.search; vm.externalLoginProviders = externalLoginInfoService.getLoginProviders(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js index b33d707c94..5827f7e530 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/login.controller.js @@ -6,9 +6,9 @@ angular.module('umbraco').controller("Umbraco.LoginController", function (events var evtOn = eventsService.on("app.ready", function(evt, data){ $scope.avatar = "assets/img/application/logo.png"; - var path = "/"; + var path = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath; - //check if there's a returnPath query string, if so redirect to it + //check if there's a returnPath query string, if so redirect to it var locationObj = $location.search(); if (locationObj.returnPath) { path = decodeURIComponent(locationObj.returnPath); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 42e894a7f3..5f067d8a9d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,75 +1,57 @@ - - + net6.0 Umbraco.Cms.Web.UI - - bin/Release/Umbraco.Web.UI.xml - - - - true - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - + + + + + + + + + false false - false + true $(ProjectDir)appsettings-schema.json $(ProjectDir)../JsonSchema/JsonSchema.csproj - + - - - - - - + - + - - + + - + - diff --git a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs index 230dcaefe5..a33ac7bca2 100644 --- a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs +++ b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs @@ -84,15 +84,25 @@ public class PublicAccessRequestHandler : IPublicAccessRequestHandler switch (publicAccessStatus) { case PublicAccessStatus.NotLoggedIn: - _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); - routeValues = await SetPublishedContentAsOtherPageAsync( - httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.LoginNodeId); + // redirect if this is not the login page + if (publicAccessAttempt.Result!.LoginNodeId != publishedContent.Id) + { + _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.LoginNodeId); + } + break; case PublicAccessStatus.AccessDenied: - _logger.LogDebug( - "EnsurePublishedContentAccess: Current member has not access, redirect to error page"); - routeValues = await SetPublishedContentAsOtherPageAsync( - httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); + // Redirect if this is not the access denied page + if (publicAccessAttempt.Result!.NoAccessNodeId != publishedContent.Id) + { + _logger.LogDebug( + "EnsurePublishedContentAccess: Current member has not access, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); + } + break; case PublicAccessStatus.LockedOut: _logger.LogDebug("Current member is locked out, redirect to error page"); diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index 932c31ada6..6476fb1568 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -162,7 +162,7 @@ "cases": [ { "condition": "(DevelopmentDatabaseType == 'SQLite')", - "value": "Microsoft.Data.SQLite" + "value": "Microsoft.Data.Sqlite" }, { "condition": "(true)", diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index efaa0edbb0..574ff04452 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -21,17 +21,14 @@ - - true - - - + false false + true diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/recycleBin.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/recycleBin.ts new file mode 100644 index 0000000000..292d54acc0 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/recycleBin.ts @@ -0,0 +1,78 @@ +/// +import { + ContentBuilder, + DocumentTypeBuilder, +} from 'umbraco-cypress-testhelpers'; + +context('Recycle bin', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + function refreshContentTree() { + // Refresh to update the tree + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.umbracoContextMenuAction("action-refreshNode").click(); + // We have to wait in case the execution is slow, otherwise we'll try and click the item before it appears in the UI + cy.get('.umb-tree-item__inner').should('exist', { timeout: 10000 }); + } + + it('Can delete content from recycle bin', () => { + const contentToDeleteName = "DeleteMe"; + const contentToNotDeleteName = "DontDelete"; + const testType = "TestType"; + + cy.umbracoEnsureDocumentTypeNameNotExists(testType); + cy.deleteAllContent(); + + const docType = new DocumentTypeBuilder() + .withName(testType) + .build(); + + cy.saveDocumentType(docType).then((savedDocType) => { + const contentToDelete = new ContentBuilder() + .withContentTypeAlias(savedDocType.alias) + .withAction("saveNew") + .addVariant() + .withName(contentToDeleteName) + .withSave(true) + .done() + .build(); + + const contentToNotDelete = new ContentBuilder() + .withContentTypeAlias(savedDocType.alias) + .withAction("saveNew") + .addVariant() + .withName(contentToNotDeleteName) + .withSave(true) + .done() + .build(); + + // Put it in the recycle bin + cy.saveContent(contentToDelete).then(savedToDelete => { + cy.deleteContentById(savedToDelete.id); + }); + cy.saveContent(contentToNotDelete).then(savedNotToDelete => { + cy.deleteContentById(savedNotToDelete.id) + }); + }); + + refreshContentTree(); + cy.umbracoTreeItem('content', ["Recycle Bin"]).click(); + cy.get('.umb-content-grid__content').contains(contentToDeleteName).closest('div').click(); + cy.umbracoButtonByLabelKey('actions_delete').click(); + cy.umbracoButtonByLabelKey('contentTypeEditor_yesDelete').click(); + + cy.umbracoSuccessNotification().should('be.visible'); + + cy.get('.umb-content-grid__content').contains(contentToDeleteName).should('not.exist'); + cy.umbracoTreeItem('content', ["Recycle Bin", contentToDeleteName]).should('not.exist'); + + cy.get('.umb-content-grid__content').contains(contentToNotDeleteName).should('be.visible'); + cy.umbracoTreeItem('content', ["Recycle Bin", contentToNotDeleteName]).should('be.visible'); + + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(testType); + }); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker b/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker index b44e817fec..5ae033d6d3 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker +++ b/tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker @@ -24,7 +24,7 @@ COPY --from=build dist . ENV ASPNETCORE_URLS="http://0.0.0.0:5000" ENV Umbraco__CMS__Global__InstallMissingDatabase="true" -ENV ConnectionStrings__umbracoDbDSN_ProviderName="Microsoft.Data.SQLite" +ENV ConnectionStrings__umbracoDbDSN_ProviderName="Microsoft.Data.Sqlite" ENV ConnectionStrings__umbracoDbDSN="Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True" ENV Umbraco__CMS__Unattended__InstallUnattended="true" ENV Umbraco__CMS__Unattended__UnattendedUserName="Cypress Test" diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 8cb9262ba2..823754bdfc 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; @@ -29,231 +30,247 @@ using Umbraco.Cms.Web.Common.Hosting; using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; -namespace Umbraco.Cms.Tests.Integration.TestServerTest; - -[TestFixture] -[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] -public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase +namespace Umbraco.Cms.Tests.Integration.TestServerTest { - [SetUp] - public void Setup() + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console, Boot = true)] + public abstract class UmbracoTestServerTestBase : UmbracoIntegrationTestBase { - /* - * It's worth noting that our usage of WebApplicationFactory is non-standard, - * the intent is that your Startup.ConfigureServices is called just like - * when the app starts up, then replacements are registered in this class with - * builder.ConfigureServices (builder.ConfigureTestServices has hung around from before the - * generic host switchover). - * - * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but - * we should get there one day. - * - * However we need to separate the testing framework we provide for downstream projects from our own tests. - * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. - * - * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests - */ - var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + protected HttpClient Client { get; private set; } - // additional host configuration for web server integration tests - Factory = factory.WithWebHostBuilder(builder => + protected LinkGenerator LinkGenerator { get; private set; } + + protected WebApplicationFactory Factory { get; private set; } + + /// + /// Hook for altering UmbracoBuilder setup + /// + /// + /// Can also be used for registering test doubles. + /// + protected virtual void CustomTestSetup(IUmbracoBuilder builder) { - // Otherwise inferred as $(SolutionDir)/Umbraco.Tests.Integration (note lack of src/tests) - builder.UseContentRoot(Assembly.GetExecutingAssembly().GetRootDirectorySafe()); + } - // Executes after the standard ConfigureServices method - builder.ConfigureTestServices(services => - - // Add a test auth scheme with a test auth handler to authn and assign the user - services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) - .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); - }); - - Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); - - LinkGenerator = Factory.Services.GetRequiredService(); - } - - protected HttpClient Client { get; private set; } - - protected LinkGenerator LinkGenerator { get; private set; } - - protected WebApplicationFactory Factory { get; private set; } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareApiControllerUrl(Expression> methodSelector) - where T : UmbracoApiController - { - var url = LinkGenerator.GetUmbracoApiService(methodSelector); - return PrepareUrl(url); - } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareSurfaceControllerUrl(Expression> methodSelector) - where T : SurfaceController - { - var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); - return PrepareUrl(url); - } - - /// - /// Prepare a url before using . - /// This returns the url but also sets the HttpContext.request into to use this url. - /// - /// The string URL of the controller action. - protected string PrepareUrl(string url) - { - var umbracoContextFactory = GetRequiredService(); - var httpContextAccessor = GetRequiredService(); - - httpContextAccessor.HttpContext = new DefaultHttpContext + [SetUp] + public void Setup() { - Request = + /* + * It's worth noting that our usage of WebApplicationFactory is non-standard, + * the intent is that your Startup.ConfigureServices is called just like + * when the app starts up, then replacements are registered in this class with + * builder.ConfigureServices (builder.ConfigureTestServices has hung around from before the + * generic host switchover). + * + * This is currently a pain to refactor towards due to UmbracoBuilder+TypeFinder+TypeLoader setup but + * we should get there one day. + * + * However we need to separate the testing framework we provide for downstream projects from our own tests. + * We cannot use the Umbraco.Web.UI startup yet as that is not available downstream. + * + * See https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests + */ + var factory = new UmbracoWebApplicationFactory(CreateHostBuilder); + + // additional host configuration for web server integration tests + Factory = factory.WithWebHostBuilder(builder => { - Scheme = "https", - Host = new HostString("localhost", 80), - Path = url, - QueryString = new QueryString(string.Empty) - } - }; + // Otherwise inferred as $(SolutionDir)/Umbraco.Tests.Integration (note lack of src/tests) + builder.UseContentRoot(Assembly.GetExecutingAssembly().GetRootDirectorySafe()); - umbracoContextFactory.EnsureUmbracoContext(); + // Executes after the standard ConfigureServices method + builder.ConfigureTestServices(services => - return url; - } + // Add a test auth scheme with a test auth handler to authn and assign the user + services.AddAuthentication(TestAuthHandler.TestAuthenticationScheme) + .AddScheme(TestAuthHandler.TestAuthenticationScheme, options => { })); + }); - private IHostBuilder CreateHostBuilder() - { - var hostBuilder = Host.CreateDefaultBuilder() - .ConfigureUmbracoDefaults() - .ConfigureAppConfiguration((context, configBuilder) => + Client = Factory.CreateClient(new WebApplicationFactoryClientOptions { - context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); - configBuilder.Sources.Clear(); - configBuilder.AddInMemoryCollection(InMemoryConfiguration); - configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); + AllowAutoRedirect = false + }); - Configuration = configBuilder.Build(); - }) - .ConfigureWebHost(builder => + LinkGenerator = Factory.Services.GetRequiredService(); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareApiControllerUrl(Expression> methodSelector) + where T : UmbracoApiController + { + var url = LinkGenerator.GetUmbracoApiService(methodSelector); + return PrepareUrl(url); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareSurfaceControllerUrl(Expression> methodSelector) + where T : SurfaceController + { + var url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); + return PrepareUrl(url); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareUrl(string url) + { + IUmbracoContextFactory umbracoContextFactory = GetRequiredService(); + IHttpContextAccessor httpContextAccessor = GetRequiredService(); + + httpContextAccessor.HttpContext = new DefaultHttpContext { - builder.ConfigureServices((context, services) => + Request = + { + Scheme = "https", + Host = new HostString("localhost", 80), + Path = url, + QueryString = new QueryString(string.Empty) + } + }; + + umbracoContextFactory.EnsureUmbracoContext(); + + return url; + } + + private IHostBuilder CreateHostBuilder() + { + IHostBuilder hostBuilder = Host.CreateDefaultBuilder() + .ConfigureUmbracoDefaults() + .ConfigureAppConfiguration((context, configBuilder) => { context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + configBuilder.Sources.Clear(); + configBuilder.AddInMemoryCollection(InMemoryConfiguration); + configBuilder.AddConfiguration(GlobalSetupTeardown.TestConfiguration); - ConfigureServices(services); - ConfigureTestServices(services); - services.AddUnique(CreateLoggerFactory()); - - if (!TestOptions.Boot) + Configuration = configBuilder.Build(); + }) + .ConfigureWebHost(builder => + { + builder.ConfigureServices((context, services) => { - // If boot is false, we don't want the CoreRuntime hosted service to start - // So we replace it with a Mock - services.AddUnique(Mock.Of()); - } + context.HostingEnvironment = TestHelper.GetWebHostEnvironment(); + + ConfigureServices(services); + ConfigureTestServices(services); + services.AddUnique(CreateLoggerFactory()); + + if (!TestOptions.Boot) + { + // If boot is false, we don't want the CoreRuntime hosted service to start + // So we replace it with a Mock + services.AddUnique(Mock.Of()); + } + }); + + // call startup + builder.Configure(Configure); + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; }); - // call startup - builder.Configure(Configure); - }) - .UseDefaultServiceProvider(cfg => - { - // These default to true *if* WebHostEnvironment.EnvironmentName == Development - // When running tests, EnvironmentName used to be null on the mock that we register into services. - // Enable opt in for tests so that validation occurs regardless of environment name. - // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. - cfg.ValidateOnBuild = true; - cfg.ValidateScopes = true; - }); + return hostBuilder; + } - return hostBuilder; - } + protected virtual IServiceProvider Services => Factory.Services; - protected virtual IServiceProvider Services => Factory.Services; + protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); - protected virtual T GetRequiredService() => Factory.Services.GetRequiredService(); + protected void ConfigureServices(IServiceCollection services) + { + services.AddTransient(); - protected void ConfigureServices(IServiceCollection services) - { - services.AddTransient(); + Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); - var hostingEnvironment = TestHelper.GetHostingEnvironment(); + TypeLoader typeLoader = services.AddTypeLoader( + GetType().Assembly, + hostingEnvironment, + TestHelper.ConsoleLoggerFactory, + AppCaches.NoCache, + Configuration, + TestHelper.Profiler); - var typeLoader = services.AddTypeLoader( - GetType().Assembly, - hostingEnvironment, - TestHelper.ConsoleLoggerFactory, - AppCaches.NoCache, - Configuration, - TestHelper.Profiler); + services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); - services.AddLogger(TestHelper.GetWebHostEnvironment(), Configuration); + var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); - var builder = new UmbracoBuilder(services, Configuration, typeLoader, TestHelper.ConsoleLoggerFactory, TestHelper.Profiler, AppCaches.NoCache, hostingEnvironment); + builder + .AddConfiguration() + .AddUmbracoCore() + .AddWebComponents() + .AddNuCache() + .AddRuntimeMinifier() + .AddBackOfficeCore() + .AddBackOfficeAuthentication() + .AddBackOfficeIdentity() + .AddMembersIdentity() + .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) + .AddPreviewSupport() + .AddMvcAndRazor(mvcBuilding: mvcBuilder => + { + // Adds Umbraco.Web.BackOffice + mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); - builder - .AddConfiguration() - .AddUmbracoCore() - .AddWebComponents() - .AddNuCache() - .AddRuntimeMinifier() - .AddBackOfficeCore() - .AddBackOfficeAuthentication() - .AddBackOfficeIdentity() - .AddMembersIdentity() - .AddBackOfficeAuthorizationPolicies(TestAuthHandler.TestAuthenticationScheme) - .AddPreviewSupport() - .AddMvcAndRazor(mvcBuilder => - { - // Adds Umbraco.Web.BackOffice - mvcBuilder.AddApplicationPart(typeof(ContentController).Assembly); + // Adds Umbraco.Web.Common + mvcBuilder.AddApplicationPart(typeof(RenderController).Assembly); - // Adds Umbraco.Web.Common - mvcBuilder.AddApplicationPart(typeof(RenderController).Assembly); + // Adds Umbraco.Web.Website + mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); - // Adds Umbraco.Web.Website - mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); + // Adds Umbraco.Tests.Integration + mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); + }) + .AddWebServer() + .AddWebsite() + .AddUmbracoSqlServerSupport() + .AddUmbracoSqliteSupport() + .AddTestServices(TestHelper); // This is the important one! - // Adds Umbraco.Tests.Integration - mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); - }) - .AddWebServer() - .AddWebsite() - .AddUmbracoSqlServerSupport() - .AddUmbracoSqliteSupport() - .AddTestServices(TestHelper) // This is the important one! - .Build(); - } + CustomTestSetup(builder); + builder.Build(); + } - /// - /// Hook for registering test doubles. - /// - protected virtual void ConfigureTestServices(IServiceCollection services) - { - } + /// + /// Hook for registering test doubles. + /// + protected virtual void ConfigureTestServices(IServiceCollection services) + { + } - protected void Configure(IApplicationBuilder app) - { - UseTestDatabase(app); + protected void Configure(IApplicationBuilder app) + { + UseTestDatabase(app); - app.UseUmbraco() - .WithMiddleware(u => - { - u.UseBackOffice(); - u.UseWebsite(); - }) - .WithEndpoints(u => - { - u.UseBackOfficeEndpoints(); - u.UseWebsiteEndpoints(); - }); + app.UseUmbraco() + .WithMiddleware(u => + { + u.UseBackOffice(); + u.UseWebsite(); + }) + .WithEndpoints(u => + { + u.UseBackOfficeEndpoints(); + u.UseWebsiteEndpoints(); + }); + } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs new file mode 100644 index 0000000000..e627a3300f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs @@ -0,0 +1,163 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Integration.TestServerTest; +using Umbraco.Cms.Web.BackOffice.Controllers; +using Umbraco.Cms.Web.Common.Formatters; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.Filters; + +[TestFixture] +public class OutgoingEditorModelEventFilterTests : UmbracoTestServerTestBase +{ + private static int _messageCount; + private static Action _handler; + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.AddNotificationHandler(); + } + + [TearDown] + public void Reset() => ResetNotifications(); + + [Test] + public async Task Content_Item_With_Schedule_Raises_SendingContentNotification() + { + IContentTypeService contentTypeService = GetRequiredService(); + IContentService contentService = GetRequiredService(); + IJsonSerializer serializer = GetRequiredService(); + + var contentType = new ContentTypeBuilder().Build(); + contentTypeService.Save(contentType); + + var contentToRequest = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .Build(); + + contentService.Save(contentToRequest); + + _handler = notification => notification.Content.AllowPreview = false; + + var url = PrepareApiControllerUrl(x => x.GetById(contentToRequest.Id)); + + HttpResponseMessage response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + var text = await response.Content.ReadAsStringAsync(); + text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var display = serializer.Deserialize(text); + + Assert.AreEqual(1, _messageCount); + Assert.IsNotNull(display); + Assert.IsFalse(display.AllowPreview); + } + + [Test] + public async Task Publish_Schedule_Is_Mapped_Correctly() + { + const string UsIso = "en-US"; + const string DkIso = "da-DK"; + const string SweIso = "sv-SE"; + var contentTypeService = GetRequiredService(); + var contentService = GetRequiredService(); + var localizationService = GetRequiredService(); + IJsonSerializer serializer = GetRequiredService(); + + var contentType = new ContentTypeBuilder() + .WithContentVariation(ContentVariation.Culture) + .Build(); + contentTypeService.Save(contentType); + + var dkLang = new LanguageBuilder() + .WithCultureInfo(DkIso) + .WithIsDefault(false) + .Build(); + + var sweLang = new LanguageBuilder() + .WithCultureInfo(SweIso) + .WithIsDefault(false) + .Build(); + + localizationService.Save(dkLang); + localizationService.Save(sweLang); + + var content = new ContentBuilder() + .WithoutIdentity() + .WithContentType(contentType) + .WithCultureName(UsIso, "Same Name") + .WithCultureName(SweIso, "Same Name") + .WithCultureName(DkIso, "Same Name") + .Build(); + + contentService.Save(content); + var schedule = new ContentScheduleCollection(); + + var dkReleaseDate = new DateTime(2022, 06, 22, 21, 30, 42); + var dkExpireDate = new DateTime(2022, 07, 15, 18, 00, 00); + + var sweReleaseDate = new DateTime(2022, 06, 23, 22, 30, 42); + var sweExpireDate = new DateTime(2022, 07, 10, 14, 20, 00); + schedule.Add(DkIso, dkReleaseDate, dkExpireDate); + schedule.Add(SweIso, sweReleaseDate, sweExpireDate); + contentService.PersistContentSchedule(content, schedule); + + var url = PrepareApiControllerUrl(x => x.GetById(content.Id)); + + HttpResponseMessage response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + + var text = await response.Content.ReadAsStringAsync(); + text = text.TrimStart(AngularJsonMediaTypeFormatter.XsrfPrefix); + var display = serializer.Deserialize(text); + + Assert.IsNotNull(display); + Assert.AreEqual(1, _messageCount); + + var dkVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == DkIso); + Assert.IsNotNull(dkVariant); + Assert.AreEqual(dkReleaseDate, dkVariant.ReleaseDate); + Assert.AreEqual(dkExpireDate, dkVariant.ExpireDate); + + var sweVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == SweIso); + Assert.IsNotNull(sweVariant); + Assert.AreEqual(sweReleaseDate, sweVariant.ReleaseDate); + Assert.AreEqual(sweExpireDate, sweVariant.ExpireDate); + + var usVariant = display.Variants.FirstOrDefault(x => x.Language?.IsoCode == UsIso); + Assert.IsNotNull(usVariant); + Assert.IsNull(usVariant.ReleaseDate); + Assert.IsNull(usVariant.ExpireDate); + } + + private void ResetNotifications() + { + _messageCount = 0; + _handler = null; + } + + private class FilterEventHandler : INotificationHandler + { + public void Handle(SendingContentNotification notification) + { + _messageCount += 1; + _handler?.Invoke(notification); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs index 11eb292ac2..cd1ec39dc9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs @@ -71,7 +71,7 @@ public class ConfigurationExtensionsTests { const string ConfiguredConnectionString = "Data Source=|DataDirectory|/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True"; - const string ConfiguredProviderName = "Microsoft.Data.SQLite"; + const string ConfiguredProviderName = "Microsoft.Data.Sqlite"; var mockedConfig = CreateConfig(ConfiguredConnectionString, ConfiguredProviderName); SetDataDirectory(); @@ -80,7 +80,7 @@ public class ConfigurationExtensionsTests AssertResults( @"Data Source=C:\Data/Umbraco.sqlite.db;Cache=Shared;Foreign Keys=True;Pooling=True", - "Microsoft.Data.SQLite", + "Microsoft.Data.Sqlite", connectionString, providerName); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index 648b45bb28..77eba6533f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -592,10 +592,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping using (var scope = scopeProvider.CreateScope()) { - Assert.AreEqual(0, scope.Depth); + Assert.AreEqual(0,scope.Depth); } } + [Test] public void Depth_WhenChildScope_ReturnsDepth() {