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()
{