From 588525e99fc4ee786ec5f7f6d283a4ff6e0d5077 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 1 Jun 2022 14:46:01 +0200 Subject: [PATCH 01/39] Add Umbraco specific global usings --- src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props | 4 ++++ src/Umbraco.Web.UI/Startup.cs | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props index ea0b013665..7266a05f6e 100644 --- a/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props +++ b/src/Umbraco.Cms/buildTransitive/Umbraco.Cms.props @@ -5,4 +5,8 @@ $(DefaultItemExcludes);umbraco/Logs/** $(DefaultItemExcludes);wwwroot/media/** + + + + diff --git a/src/Umbraco.Web.UI/Startup.cs b/src/Umbraco.Web.UI/Startup.cs index 0176974b6b..eba72b6924 100644 --- a/src/Umbraco.Web.UI/Startup.cs +++ b/src/Umbraco.Web.UI/Startup.cs @@ -1,12 +1,3 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Umbraco.Cms.Core.DependencyInjection; -using Umbraco.Extensions; - namespace Umbraco.Cms.Web.UI { public class Startup From 87dbc8b2874efbdd201d34e117ebb3be9ff31719 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 1 Jun 2022 15:26:55 +0200 Subject: [PATCH 02/39] Enable implicit usings --- src/Umbraco.Web.UI/Program.cs | 2 - .../UmbracoProject/UmbracoProject.csproj | 59 +++++++++---------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI/Program.cs b/src/Umbraco.Web.UI/Program.cs index 44c045800d..fb6c724b3f 100644 --- a/src/Umbraco.Web.UI/Program.cs +++ b/src/Umbraco.Web.UI/Program.cs @@ -1,5 +1,3 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; using Umbraco.Cms.Web.Common.Hosting; namespace Umbraco.Cms.Web.UI diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj index e34e743acb..efaa0edbb0 100644 --- a/templates/UmbracoProject/UmbracoProject.csproj +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -1,38 +1,37 @@ + + net6.0 + enable + enable + Umbraco.Cms.Web.UI + - - net6.0 - enable - Umbraco.Cms.Web.UI - + + + - - - + + + + + - - - - - + + + + - - - - + + true + - - true - - - - - - - - - false - false - + + + + + + false + false + From 4dcb017afe68477d77b77fafb6f1b7df91210a14 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jun 2022 08:44:03 +0200 Subject: [PATCH 03/39] v10: Wait for updated ConnectionStrings during install (#12536) * Do not change/reload configuration * Wait for updated connection string options --- .../Configuration/JsonConfigManipulator.cs | 30 ++++--------------- .../Migrations/Install/DatabaseBuilder.cs | 20 +++++++++++++ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs index e9e1236a9e..6d583151ba 100644 --- a/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs +++ b/src/Umbraco.Infrastructure/Configuration/JsonConfigManipulator.cs @@ -35,11 +35,6 @@ namespace Umbraco.Cms.Core.Configuration public void RemoveConnectionString() { - // Update and reload configuration - _configuration[UmbracoConnectionStringPath] = null; - _configuration[UmbracoConnectionStringProviderNamePath] = null; - (_configuration as IConfigurationRoot)?.Reload(); - // Remove keys from JSON var provider = GetJsonConfigurationProvider(UmbracoConnectionStringPath); @@ -58,11 +53,6 @@ namespace Umbraco.Cms.Core.Configuration public void SaveConnectionString(string connectionString, string? providerName) { - // Update and reload configuration - _configuration[UmbracoConnectionStringPath] = connectionString; - _configuration[UmbracoConnectionStringProviderNamePath] = providerName; - (_configuration as IConfigurationRoot)?.Reload(); - // Save keys to JSON var provider = GetJsonConfigurationProvider(); @@ -84,10 +74,6 @@ namespace Umbraco.Cms.Core.Configuration public void SaveConfigValue(string key, object value) { - // Update and reload configuration - _configuration[key] = value?.ToString(); - (_configuration as IConfigurationRoot)?.Reload(); - // Save key to JSON var provider = GetJsonConfigurationProvider(); @@ -122,10 +108,6 @@ namespace Umbraco.Cms.Core.Configuration public void SaveDisableRedirectUrlTracking(bool disable) { - // Update and reload configuration - _configuration["Umbraco:CMS:WebRouting:DisableRedirectUrlTracking"] = disable.ToString(); - (_configuration as IConfigurationRoot)?.Reload(); - // Save key to JSON var provider = GetJsonConfigurationProvider(); @@ -147,10 +129,6 @@ namespace Umbraco.Cms.Core.Configuration public void SetGlobalId(string id) { - // Update and reload configuration - _configuration["Umbraco:CMS:Global:Id"] = id; - (_configuration as IConfigurationRoot)?.Reload(); - // Save key to JSON var provider = GetJsonConfigurationProvider(); @@ -336,17 +314,21 @@ namespace Umbraco.Cms.Core.Configuration { if (token is JObject obj) { - foreach (var property in obj.Properties()) { if (name is null) + { return property.Value; + } + if (string.Equals(property.Name, name, StringComparison.OrdinalIgnoreCase)) + { return property.Value; + } } } + return null; } - } } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 2f8bd3f4ec..b3188720b0 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -163,7 +163,27 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (!isTrialRun) { + // File configuration providers use a delay before reloading and triggering changes, so wait + using var isChanged = new ManualResetEvent(false); + using IDisposable? onChange = _connectionStrings.OnChange((options, name) => + { + // Only watch default named option (CurrentValue) + if (name != Options.DefaultName) + { + return; + } + + // Signal change + isChanged.Set(); + }); + + // Update configuration and wait for change _configManipulator.SaveConnectionString(connectionString, providerName); + if (!isChanged.WaitOne(10_000)) + { + throw new InstallException("Didn't retrieve updated connection string within 10 seconds, try manual configuration instead."); + } + Configure(_globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); } From 559a85812f9b0f893e76cbd893bda8ab4386b903 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 7 Jun 2022 08:59:45 +0200 Subject: [PATCH 04/39] recase assigndomain (#12448) --- src/Umbraco.Core/Actions/ActionAssignDomain.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 0638f605af..452ca51549 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -17,7 +17,8 @@ public class ActionAssignDomain : IAction public char Letter => ActionLetter; /// - public string Alias => "assignDomain"; + // This is all lower-case because of case sensitive filesystems, see issue: https://github.com/umbraco/Umbraco-CMS/issues/11670 + public string Alias => "assigndomain"; /// public string Category => Constants.Conventions.PermissionCategories.AdministrationCategory; From ea9d27c03881c04a952aaa0aa07e05185f4cbe7e Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 7 Jun 2022 10:25:05 +0100 Subject: [PATCH 05/39] Add depth property to ICoreScope (#12540) --- src/Umbraco.Core/Scoping/ICoreScope.cs | 9 +++++- src/Umbraco.Infrastructure/Scoping/Scope.cs | 13 +++++++++ .../Scoping/ScopeUnitTests.cs | 29 +++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Scoping/ICoreScope.cs b/src/Umbraco.Core/Scoping/ICoreScope.cs index 8bb85ca29d..e1382f0ec8 100644 --- a/src/Umbraco.Core/Scoping/ICoreScope.cs +++ b/src/Umbraco.Core/Scoping/ICoreScope.cs @@ -1,4 +1,3 @@ -using System; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; @@ -9,6 +8,14 @@ namespace Umbraco.Cms.Core.Scoping; /// public interface ICoreScope : IDisposable, IInstanceIdentifiable { + /// + /// Gets the distance from the root scope. + /// + /// + /// A zero represents a root scope, any value greater than zero represents a child scope. + /// + public int Depth => -1; + /// /// Gets the scope notification publisher /// diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 190261a808..e1ad1d2389 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -478,6 +478,19 @@ namespace Umbraco.Cms.Infrastructure.Scoping } } + public int Depth + { + get + { + if (ParentScope == null) + { + return 0; + } + + return ParentScope.Depth + 1; + } + } + public IScopedNotificationPublisher Notifications { get diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index f429f584e5..5734622e3e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -579,5 +579,34 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping Assert.IsNull(realScope.GetReadLocks()); } } + + [Test] + public void Depth_WhenRootScope_ReturnsZero() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (var scope = scopeProvider.CreateScope()) + { + Assert.AreEqual(0,scope.Depth); + } + } + + + [Test] + public void Depth_WhenChildScope_ReturnsDepth() + { + var scopeProvider = GetScopeProvider(out var syntaxProviderMock); + + using (scopeProvider.CreateScope()) + { + using (scopeProvider.CreateScope()) + { + using (var c2 = scopeProvider.CreateScope()) + { + Assert.AreEqual(2, c2.Depth); + } + } + } + } } } From fde1b6685a98e175c03035883e5e5839963beb71 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 7 Jun 2022 10:35:36 +0100 Subject: [PATCH 06/39] Remove ambient scope stack from httpcontext.items. (#12539) This change makes it easier to use service calls in parallel whilst a httpcontext is available. --- .../UmbracoBuilder.CoreServices.cs | 7 +- .../Scoping/AmbientScopeContextStack.cs | 39 ++ .../Scoping/AmbientScopeStack.cs | 39 ++ .../Scoping/IAmbientScopeContextStack.cs | 10 + .../Scoping/IAmbientScopeStack.cs | 7 + src/Umbraco.Infrastructure/Scoping/Scope.cs | 23 +- .../Scoping/ScopeProvider.cs | 358 +++--------------- .../Scoping/ScopeTests.cs | 50 --- .../Services/ThreadSafetyServiceTest.cs | 10 - .../Umbraco.Core/Components/ComponentTests.cs | 2 +- .../ScopedNotificationPublisherTests.cs | 3 +- .../Scoping/ScopeUnitTests.cs | 3 +- 12 files changed, 164 insertions(+), 387 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Scoping/AmbientScopeContextStack.cs create mode 100644 src/Umbraco.Infrastructure/Scoping/AmbientScopeStack.cs create mode 100644 src/Umbraco.Infrastructure/Scoping/IAmbientScopeContextStack.cs create mode 100644 src/Umbraco.Infrastructure/Scoping/IAmbientScopeStack.cs diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 4b30a10159..48bdb6d399 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -95,11 +95,14 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Mappers()?.AddCoreMappers(); // register the scope provider - builder.Services.AddSingleton(); // implements IScopeProvider, IScopeAccessor + builder.Services.AddSingleton(sp => ActivatorUtilities.CreateInstance(sp, sp.GetRequiredService())); // implements IScopeProvider, IScopeAccessor builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(f => f.GetRequiredService()); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(f => f.GetRequiredService()); + builder.Services.AddSingleton(); builder.Services.AddScoped(); diff --git a/src/Umbraco.Infrastructure/Scoping/AmbientScopeContextStack.cs b/src/Umbraco.Infrastructure/Scoping/AmbientScopeContextStack.cs new file mode 100644 index 0000000000..18e68120a6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Scoping/AmbientScopeContextStack.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Infrastructure.Scoping; + +internal class AmbientScopeContextStack : IAmbientScopeContextStack +{ + private static AsyncLocal> _stack = new(); + + public IScopeContext? AmbientContext + { + get + { + if (_stack.Value?.TryPeek(out IScopeContext? ambientContext) ?? false) + { + return ambientContext; + } + + return null; + } + } + + public IScopeContext Pop() + { + if (_stack.Value?.TryPop(out IScopeContext? ambientContext) ?? false) + { + return ambientContext; + } + + throw new InvalidOperationException("No AmbientContext was found."); + } + + public void Push(IScopeContext scope) + { + _stack.Value ??= new ConcurrentStack(); + + _stack.Value.Push(scope); + } +} diff --git a/src/Umbraco.Infrastructure/Scoping/AmbientScopeStack.cs b/src/Umbraco.Infrastructure/Scoping/AmbientScopeStack.cs new file mode 100644 index 0000000000..3ad5e89e51 --- /dev/null +++ b/src/Umbraco.Infrastructure/Scoping/AmbientScopeStack.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; + +namespace Umbraco.Cms.Infrastructure.Scoping +{ + internal class AmbientScopeStack : IAmbientScopeStack + { + private static AsyncLocal> _stack = new (); + + public IScope? AmbientScope + { + get + { + if (_stack.Value?.TryPeek(out IScope? ambientScope) ?? false) + { + return ambientScope; + } + + return null; + } + } + + public IScope Pop() + { + if (_stack.Value?.TryPop(out IScope? ambientScope) ?? false) + { + return ambientScope; + } + + throw new InvalidOperationException("No AmbientScope was found."); + } + + public void Push(IScope scope) + { + _stack.Value ??= new ConcurrentStack(); + + _stack.Value.Push(scope); + } + } +} diff --git a/src/Umbraco.Infrastructure/Scoping/IAmbientScopeContextStack.cs b/src/Umbraco.Infrastructure/Scoping/IAmbientScopeContextStack.cs new file mode 100644 index 0000000000..28da9a6427 --- /dev/null +++ b/src/Umbraco.Infrastructure/Scoping/IAmbientScopeContextStack.cs @@ -0,0 +1,10 @@ +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Infrastructure.Scoping; + +internal interface IAmbientScopeContextStack +{ + IScopeContext? AmbientContext { get; } + IScopeContext Pop(); + void Push(IScopeContext scope); +} diff --git a/src/Umbraco.Infrastructure/Scoping/IAmbientScopeStack.cs b/src/Umbraco.Infrastructure/Scoping/IAmbientScopeStack.cs new file mode 100644 index 0000000000..71cfbf3a03 --- /dev/null +++ b/src/Umbraco.Infrastructure/Scoping/IAmbientScopeStack.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Infrastructure.Scoping; + +internal interface IAmbientScopeStack : IScopeAccessor +{ + IScope Pop(); + void Push(IScope scope); +} diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index e1ad1d2389..2fdc1a04dd 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -40,7 +40,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping private readonly bool? _scopeFileSystem; private readonly ScopeProvider _scopeProvider; - private bool _callContext; private bool? _completed; private IUmbracoDatabase? _database; @@ -93,7 +92,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping _eventDispatcher = eventDispatcher; _notificationPublisher = notificationPublisher; _scopeFileSystem = scopeFileSystems; - _callContext = callContext; _autoComplete = autoComplete; Detachable = detachable; _dictionaryLocker = new object(); @@ -259,24 +257,15 @@ namespace Umbraco.Cms.Infrastructure.Scoping { } + [Obsolete("Scopes are never stored on HttpContext.Items anymore, so CallContext is always true.")] // a value indicating whether to force call-context public bool CallContext { - get + get => true; + set { - if (_callContext) - { - return true; - } - - if (ParentScope != null) - { - return ParentScope.CallContext; - } - - return false; + // NOOP - always true. } - set => _callContext = value; } public bool ScopedFileSystems @@ -564,7 +553,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping } } - _scopeProvider.PopAmbientScope(this); // might be null = this is how scopes are removed from context objects + _scopeProvider.PopAmbientScope(); // might be null = this is how scopes are removed from context objects #if DEBUG_SCOPES _scopeProvider.Disposed(this); @@ -916,7 +905,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping // by Deploy which I don't fully understand since there is limited tests on this in the CMS if (OrigScope != _scopeProvider.AmbientScope) { - _scopeProvider.PopAmbientScope(_scopeProvider.AmbientScope); + _scopeProvider.PopAmbientScope(); } if (OrigContext != _scopeProvider.AmbientContext) diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index 4022f366e2..9be7eee387 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -1,5 +1,5 @@ -using System; using System.Data; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; @@ -8,10 +8,9 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Infrastructure.Persistence; using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; using Umbraco.Extensions; -using System.Collections.Concurrent; -using System.Threading; using Umbraco.Cms.Core.DistributedLocking; using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Web.Common.DependencyInjection; #if DEBUG_SCOPES using System.Linq; @@ -27,20 +26,46 @@ namespace Umbraco.Cms.Infrastructure.Scoping ICoreScopeProvider, IScopeProvider, Core.Scoping.IScopeProvider, - IScopeAccessor + IScopeAccessor // TODO: No need to implement this here but literally hundreds of our tests cast ScopeProvider to ScopeAccessor { private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IRequestCache _requestCache; + private readonly IEventAggregator _eventAggregator; + private readonly IAmbientScopeStack _ambientScopeStack; + private readonly IAmbientScopeContextStack _ambientContextStack; + private readonly FileSystems _fileSystems; private CoreDebugSettings _coreDebugSettings; private readonly MediaFileManager _mediaFileManager; - private static readonly AsyncLocal> s_scopeStack = new AsyncLocal>(); - private static readonly AsyncLocal> s_scopeContextStack = new AsyncLocal>(); - private static readonly string s_scopeItemKey = typeof(Scope).FullName!; - private static readonly string s_contextItemKey = typeof(ScopeProvider).FullName!; - private readonly IEventAggregator _eventAggregator; + public ScopeProvider( + IAmbientScopeStack ambientScopeStack, + IAmbientScopeContextStack ambientContextStack, + IDistributedLockingMechanismFactory distributedLockingMechanismFactory, + IUmbracoDatabaseFactory databaseFactory, + FileSystems fileSystems, + IOptionsMonitor coreDebugSettings, + MediaFileManager mediaFileManager, + ILoggerFactory loggerFactory, + IEventAggregator eventAggregator) + { + DistributedLockingMechanismFactory = distributedLockingMechanismFactory; + DatabaseFactory = databaseFactory; + _ambientScopeStack = ambientScopeStack; + _ambientContextStack = ambientContextStack; + _fileSystems = fileSystems; + _coreDebugSettings = coreDebugSettings.CurrentValue; + _mediaFileManager = mediaFileManager; + _logger = loggerFactory.CreateLogger(); + _loggerFactory = loggerFactory; + _eventAggregator = eventAggregator; + // take control of the FileSystems + _fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems; + + coreDebugSettings.OnChange(x => _coreDebugSettings = x); + } + + [Obsolete("Please use an alternative constructor. This constructor is due for removal in v12.")] public ScopeProvider( IDistributedLockingMechanismFactory distributedLockingMechanismFactory, IUmbracoDatabaseFactory databaseFactory, @@ -50,20 +75,17 @@ namespace Umbraco.Cms.Infrastructure.Scoping ILoggerFactory loggerFactory, IRequestCache requestCache, IEventAggregator eventAggregator) + : this( + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + distributedLockingMechanismFactory, + databaseFactory, + fileSystems, + coreDebugSettings, + mediaFileManager, + loggerFactory, + eventAggregator) { - DistributedLockingMechanismFactory = distributedLockingMechanismFactory; - DatabaseFactory = databaseFactory; - _fileSystems = fileSystems; - _coreDebugSettings = coreDebugSettings.CurrentValue; - _mediaFileManager = mediaFileManager; - _logger = loggerFactory.CreateLogger(); - _loggerFactory = loggerFactory; - _requestCache = requestCache; - _eventAggregator = eventAggregator; - // take control of the FileSystems - _fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems; - - coreDebugSettings.OnChange(x => _coreDebugSettings = x); } public IDistributedLockingMechanismFactory DistributedLockingMechanismFactory { get; } @@ -72,195 +94,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping public ISqlContext SqlContext => DatabaseFactory.SqlContext; - #region Context - - private void MoveHttpContextScopeToCallContext() - { - var source = (ConcurrentStack?)_requestCache.Get(s_scopeItemKey); - ConcurrentStack? stack = s_scopeStack.Value; - MoveContexts(s_scopeItemKey, source, stack, (_, v) => s_scopeStack.Value = v); - } - - private void MoveHttpContextScopeContextToCallContext() - { - var source = (ConcurrentStack?)_requestCache.Get(s_contextItemKey); - ConcurrentStack? stack = s_scopeContextStack.Value; - MoveContexts(s_contextItemKey, source, stack, (_, v) => s_scopeContextStack.Value = v); - } - - private void MoveCallContextScopeToHttpContext() - { - ConcurrentStack? source = s_scopeStack.Value; - var stack = (ConcurrentStack?)_requestCache.Get(s_scopeItemKey); - MoveContexts(s_scopeItemKey, source, stack, (k, v) => _requestCache.Set(k, v)); - } - - private void MoveCallContextScopeContextToHttpContext() - { - ConcurrentStack? source = s_scopeContextStack.Value; - var stack = (ConcurrentStack?)_requestCache.Get(s_contextItemKey); - MoveContexts(s_contextItemKey, source, stack, (k, v) => _requestCache.Set(k, v)); - } - - private void MoveContexts(string key, ConcurrentStack? source, ConcurrentStack? stack, Action> setter) - where T : class, IInstanceIdentifiable - { - if (source == null) - { - return; - } - - if (stack != null) - { - stack.Clear(); - } - else - { - // TODO: This isn't going to copy it back up the execution context chain - stack = new ConcurrentStack(); - setter(key, stack); - } - - var arr = new T[source.Count]; - source.CopyTo(arr, 0); - Array.Reverse(arr); - foreach (T a in arr) - { - stack.Push(a); - } - - source.Clear(); - } - - private void SetCallContextScope(IScope? value) - { - ConcurrentStack? stack = s_scopeStack.Value; - -#if DEBUG_SCOPES - // first, null-register the existing value - if (stack != null && stack.TryPeek(out IScope ambientScope)) - { - RegisterContext(ambientScope, null); - } - - // then register the new value - if (value != null) - { - RegisterContext(value, "call"); - } -#endif - - if (value == null) - { - if (stack != null) - { - stack.TryPop(out _); - } - } - else - { - -#if DEBUG_SCOPES - _logger.LogDebug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8)); -#endif - if (stack == null) - { - stack = new ConcurrentStack(); - } - stack.Push(value); - s_scopeStack.Value = stack; - } - } - - private void SetCallContextScopeContext(IScopeContext? value) - { - ConcurrentStack? stack = s_scopeContextStack.Value; - - if (value == null) - { - if (stack != null) - { - stack.TryPop(out _); - } - } - else - { - if (stack == null) - { - stack = new ConcurrentStack(); - } - stack.Push(value); - s_scopeContextStack.Value = stack; - } - } - - - private T? GetHttpContextObject(string key, bool required = true) - where T : class - { - if (!_requestCache.IsAvailable && required) - { - throw new Exception("Request cache is unavailable."); - } - - var stack = (ConcurrentStack?)_requestCache.Get(key); - return stack != null && stack.TryPeek(out T? peek) ? peek : null; - } - - private bool SetHttpContextObject(string key, T? value, bool required = true) - { - if (!_requestCache.IsAvailable) - { - if (required) - { - throw new Exception("Request cache is unavailable."); - } - - return false; - } - -#if DEBUG_SCOPES - // manage the 'context' that contains the scope (null, "http" or "call") - // only for scopes of course! - if (key == s_scopeItemKey) - { - // first, null-register the existing value - var ambientScope = (IScope)_requestCache.Get(s_scopeItemKey); - if (ambientScope != null) - { - RegisterContext(ambientScope, null); - } - - // then register the new value - if (value is IScope scope) - { - RegisterContext(scope, "http"); - } - } -#endif - var stack = (ConcurrentStack?)_requestCache.Get(key); - - if (value == null) - { - if (stack != null) - { - stack.TryPop(out _); - } - } - else - { - if (stack == null) - { - stack = new ConcurrentStack(); - } - stack.Push(value); - _requestCache.Set(key, stack); - } - - return true; - } - - #endregion #region Ambient Context @@ -270,83 +103,23 @@ namespace Umbraco.Cms.Infrastructure.Scoping /// /// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal) /// - public IScopeContext? AmbientContext - { - get - { - // try http context, fallback onto call context - IScopeContext? value = GetHttpContextObject(s_contextItemKey, false); - if (value != null) - { - return value; - } - - ConcurrentStack? stack = s_scopeContextStack.Value; - if (stack == null || !stack.TryPeek(out IScopeContext? peek)) - { - return null; - } - - return peek; - } - } + public IScopeContext? AmbientContext => _ambientContextStack.AmbientContext; #endregion #region Ambient Scope - IScope? IScopeAccessor.AmbientScope => AmbientScope; - /// /// Gets or set the Ambient (Current) for the current execution context. /// /// /// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal) /// - public Scope? AmbientScope - { - get - { - // try http context, fallback onto call context - IScope? value = GetHttpContextObject(s_scopeItemKey, false); - if (value != null) - { - return (Scope)value; - } + public Scope? AmbientScope => (Scope?)_ambientScopeStack.AmbientScope; - ConcurrentStack? stack = s_scopeStack.Value; - if (stack == null || !stack.TryPeek(out IScope? peek)) - { - return null; - } + IScope? IScopeAccessor.AmbientScope => _ambientScopeStack.AmbientScope; - return (Scope)peek; - } - } - - public void PopAmbientScope(Scope? scope) - { - // pop the stack from all contexts - SetHttpContextObject(s_scopeItemKey, null, false); - SetCallContextScope(null); - - // We need to move the stack to a different context if the parent scope - // is flagged with a different CallContext flag. This is required - // if creating a child scope with callContext: true (thus forcing CallContext) - // when there is actually a current HttpContext available. - // It's weird but is required for Deploy somehow. - bool parentScopeCallContext = (scope?.ParentScope?.CallContext ?? false); - if ((scope?.CallContext ?? false) && !parentScopeCallContext) - { - MoveCallContextScopeToHttpContext(); - MoveCallContextScopeContextToHttpContext(); - } - else if ((!scope?.CallContext ?? false) && parentScopeCallContext) - { - MoveHttpContextScopeToCallContext(); - MoveHttpContextScopeContextToCallContext(); - } - } + public void PopAmbientScope() => _ambientScopeStack.Pop(); #endregion @@ -357,22 +130,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping throw new ArgumentNullException(nameof(scope)); } - if (scope.CallContext != false || !SetHttpContextObject(s_scopeItemKey, scope, false)) - { - // In this case, always ensure that the HttpContext items - // is transfered to CallContext and then cleared since we - // may be migrating context with the callContext = true flag. - // This is a weird case when forcing callContext when HttpContext - // is available. Required by Deploy. - - if (_requestCache.IsAvailable) - { - MoveHttpContextScopeToCallContext(); - MoveHttpContextScopeContextToCallContext(); - } - - SetCallContextScope(scope); - } + _ambientScopeStack.Push(scope); } public void PushAmbientScopeContext(IScopeContext? scopeContext) @@ -381,17 +139,10 @@ namespace Umbraco.Cms.Infrastructure.Scoping { throw new ArgumentNullException(nameof(scopeContext)); } - - SetHttpContextObject(s_contextItemKey, scopeContext, false); - SetCallContextScopeContext(scopeContext); + _ambientContextStack.Push(scopeContext); } - public void PopAmbientScopeContext() - { - // pop stack from all contexts - SetHttpContextObject(s_contextItemKey, null, false); - SetCallContextScopeContext(null); - } + public void PopAmbientScopeContext() => _ambientContextStack.Pop(); /// public IScope CreateDetachedScope( @@ -445,7 +196,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping throw new InvalidOperationException("Ambient scope is not detachable."); } - PopAmbientScope(ambientScope); + PopAmbientScope(); PopAmbientScopeContext(); Scope? originalScope = AmbientScope; @@ -499,9 +250,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping /// public IScopeContext? Context => AmbientContext; - // for testing - internal ConcurrentStack? GetCallContextScopeValue() => s_scopeStack.Value; - #if DEBUG_SCOPES // this code needs TLC // diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs index cfd9e49583..5febb2819b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeTests.cs @@ -226,56 +226,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping Assert.IsNull(scopeProvider.AmbientScope); } - [Test] - public void NestedMigrateScope() - { - // Get the request cache mock and re-configure it to be available and used - var requestCacheDictionary = new Dictionary(); - IRequestCache requestCache = AppCaches.RequestCache; - var requestCacheMock = Mock.Get(requestCache); - requestCacheMock - .Setup(x => x.IsAvailable) - .Returns(true); - requestCacheMock - .Setup(x => x.Set(It.IsAny(), It.IsAny())) - .Returns((string key, object val) => - { - requestCacheDictionary.Add(key, val); - return true; - }); - requestCacheMock - .Setup(x => x.Get(It.IsAny())) - .Returns((string key) => requestCacheDictionary.TryGetValue(key, out var val) ? val : null); - - ScopeProvider scopeProvider = ScopeProvider; - Assert.IsNull(scopeProvider.AmbientScope); - - using (IScope scope = scopeProvider.CreateScope()) - { - Assert.IsInstanceOf(scope); - Assert.IsNotNull(scopeProvider.AmbientScope); - Assert.AreSame(scope, scopeProvider.AmbientScope); - - using (IScope nested = scopeProvider.CreateScope(callContext: true)) - { - Assert.IsInstanceOf(nested); - Assert.IsNotNull(scopeProvider.AmbientScope); - Assert.AreSame(nested, scopeProvider.AmbientScope); - Assert.AreSame(scope, ((Scope)nested).ParentScope); - - // it's moved over to call context - ConcurrentStack callContextScope = scopeProvider.GetCallContextScopeValue(); - - Assert.IsNotNull(callContextScope); - Assert.AreEqual(2, callContextScope.Count); - } - - // it's naturally back in http context - } - - Assert.IsNull(scopeProvider.AmbientScope); - } - [Test] public void NestedCreateScopeContext() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs index 88adef3824..4ee52a6869 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ThreadSafetyServiceTest.cs @@ -134,11 +134,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack - currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", - Thread.CurrentThread.ManagedThreadId, currentStack?.Count); - // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads // against an ambient context which cannot be done due to the rules of scope creation and completion. @@ -234,11 +229,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { try { - ConcurrentStack - currentStack = ((ScopeProvider)ScopeProvider).GetCallContextScopeValue(); - log.LogInformation("[{ThreadId}] Current Stack? {CurrentStack}", - Thread.CurrentThread.ManagedThreadId, currentStack?.Count); - // NOTE: This is NULL because we have supressed the execution context flow. // If we don't do that we will get various exceptions because we're trying to run concurrent threads // against an ambient context which cannot be done due to the rules of scope creation and completion. diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index 7593e52511..8aca86ba80 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Components Mock.Of(), Options.Create(new ContentSettings())); IEventAggregator eventAggregator = Mock.Of(); - var scopeProvider = new ScopeProvider(Mock.Of(),f , fs, new TestOptionsMonitor(coreDebug), mediaFileManager, loggerFactory, NoAppCache.Instance, eventAggregator); + var scopeProvider = new ScopeProvider(new AmbientScopeStack(), new AmbientScopeContextStack(), Mock.Of(),f , fs, new TestOptionsMonitor(coreDebug), mediaFileManager, loggerFactory, eventAggregator); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(logger); mock.Setup(x => x.GetService(typeof(ILogger))).Returns(loggerFactory.CreateLogger); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs index 6ddc506753..79d2f7b3d3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Scoping/ScopedNotificationPublisherTests.cs @@ -93,13 +93,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Scoping eventAggregatorMock = new Mock(); return new ScopeProvider( + new AmbientScopeStack(), + new AmbientScopeContextStack(), Mock.Of(), Mock.Of(), fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), mediaFileManager, loggerFactory, - Mock.Of(), eventAggregatorMock.Object ); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs index 5734622e3e..3d4ded44f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Scoping/ScopeUnitTests.cs @@ -71,13 +71,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Scoping sqlContext.Setup(x => x.SqlSyntax).Returns(syntaxProviderMock.Object); return new ScopeProvider( + new AmbientScopeStack(), + new AmbientScopeContextStack(), lockingMechanismFactory.Object, databaseFactory.Object, fileSystems, new TestOptionsMonitor(new CoreDebugSettings()), mediaFileManager, loggerFactory, - Mock.Of(), Mock.Of()); } From d7ce136107a7867d9c44a6a9fd03d43f80fdbfe5 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 7 Jun 2022 11:41:46 +0100 Subject: [PATCH 07/39] v10: Prefer SQLite primitive types to flexible types (#12541) * Prefer SQLite primitive types to flexible types. * SQLite - column mappings use TEXT for decimals Thanks @mattbrailsford for sense check. --- .../Services/SqliteSyntaxProvider.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs index f7d3a299ef..4f90817e47 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteSyntaxProvider.cs @@ -38,6 +38,20 @@ public class SqliteSyntaxProvider : SqlSyntaxProviderBase [typeof(Guid)] = new SqliteGuidScalarMapper(), [typeof(Guid?)] = new SqliteNullableGuidScalarMapper(), }; + + IntColumnDefinition = "INTEGER"; + LongColumnDefinition = "INTEGER"; + BoolColumnDefinition = "INTEGER"; + + GuidColumnDefinition = "TEXT"; + DateTimeColumnDefinition = "TEXT"; + DateTimeOffsetColumnDefinition = "TEXT"; + TimeColumnDefinition = "TEXT"; + DecimalColumnDefinition = "TEXT"; // REAL would be lossy. - https://docs.microsoft.com/en-us/dotnet/standard/data/sqlite/types + + RealColumnDefinition = "REAL"; + + BlobColumnDefinition = "BLOB"; } /// From a8b68202f3d8e7e94597f85a1f50f2fdef667bc9 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Tue, 7 Jun 2022 12:15:26 +0100 Subject: [PATCH 08/39] Fix issue where languages files are not found in subdir of package dir (#12543) --- .../UmbracoBuilder.LocalizedText.cs | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs index ff817e2f1c..2d18905ee9 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs @@ -1,11 +1,6 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; - using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -17,6 +12,7 @@ namespace Umbraco.Extensions /// public static partial class UmbracoBuilderExtensions { + /// /// Adds the supplementary localized texxt file sources from the various physical and virtual locations supported. /// @@ -69,45 +65,52 @@ namespace Umbraco.Extensions private static IEnumerable GetPluginLanguageFileSources( IFileProvider fileProvider, string folder, bool overwriteCoreKeys) { - // locate all the *.xml files inside Lang folders inside folders of the main folder - // e.g. /app_plugins/plugin-name/lang/*.xml - var fileSources = new List(); - - var pluginFolders = fileProvider.GetDirectoryContents(folder) - .Where(x => x.IsDirectory).ToList(); + IEnumerable pluginFolders = fileProvider + .GetDirectoryContents(folder) + .Where(x => x.IsDirectory); foreach (IFileInfo pluginFolder in pluginFolders) { // get the full virtual path for the plugin folder var pluginFolderPath = WebPath.Combine(folder, pluginFolder.Name); - // get any lang folders in this plugin - IEnumerable langFolders = fileProvider.GetDirectoryContents(pluginFolderPath) - .Where(x => x.IsDirectory && x.Name.InvariantEquals("lang")); - // loop through the lang folder(s) // - there could be multiple on case sensitive file system - foreach (var langFolder in langFolders) + foreach (var langFolder in GetLangFolderPaths(fileProvider, pluginFolderPath)) { - // get the full 'virtual' path of the lang folder - var langFolderPath = WebPath.Combine(pluginFolderPath, langFolder.Name); - // request all the files out of the path, these will have physicalPath set. - var files = fileProvider.GetDirectoryContents(langFolderPath) - .Where(x => x.Name.InvariantEndsWith(".xml") && !string.IsNullOrEmpty(x.PhysicalPath)) - .Select(x => new FileInfo(x.PhysicalPath)) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, overwriteCoreKeys)) - .ToList(); + IEnumerable localizationFiles = fileProvider + .GetDirectoryContents(langFolder) + .Where(x => !string.IsNullOrEmpty(x.PhysicalPath)) + .Where(x => x.Name.InvariantEndsWith(".xml")) + .Select(x => new FileInfo(x.PhysicalPath)); - // add any to our results - if (files.Count > 0) + foreach (FileInfo file in localizationFiles) { - fileSources.AddRange(files); + yield return new LocalizedTextServiceSupplementaryFileSource(file, overwriteCoreKeys); } } } + } - return fileSources; + private static IEnumerable GetLangFolderPaths(IFileProvider fileProvider, string path) + { + IEnumerable directories = fileProvider.GetDirectoryContents(path).Where(x => x.IsDirectory); + + foreach (IFileInfo directory in directories) + { + var virtualPath = WebPath.Combine(path, directory.Name); + + if (directory.Name.InvariantEquals("lang")) + { + yield return virtualPath; + } + + foreach (var nested in GetLangFolderPaths(fileProvider, virtualPath)) + { + yield return nested; + } + } } } } From fffbbbeb5fc5a2aab6a7e862fed19b75c12013c4 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 7 Jun 2022 14:45:42 +0200 Subject: [PATCH 09/39] Make FindContent return type nullable (#12545) --- src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs b/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs index 616611a224..3bf8d3a04a 100644 --- a/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs +++ b/src/Umbraco.Web.Common/Controllers/IVirtualPageController.cs @@ -11,5 +11,5 @@ public interface IVirtualPageController /// /// Returns the to use as the current page for the request /// - IPublishedContent FindContent(ActionExecutingContext actionExecutingContext); + IPublishedContent? FindContent(ActionExecutingContext actionExecutingContext); } From 9ad790f36b766fb59b07735454a804e268bd0642 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 8 Jun 2022 08:19:43 +0200 Subject: [PATCH 10/39] Updated nuget dependencies (07-06-2022) (#12525) * Updated nuget dependencies * Move Nerdbank.GitVersioning update to Directory.Build.props * Updated more dependencies --- Directory.Build.props | 2 +- src/JsonSchema/JsonSchema.csproj | 9 ++++++--- .../Umbraco.Cms.Persistence.Sqlite.csproj | 2 +- src/Umbraco.Cms/Umbraco.Cms.csproj | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 4 ++-- .../Logging/MessageTemplates.cs | 4 ++-- .../Umbraco.Infrastructure.csproj | 15 +++++++++------ .../Umbraco.Web.BackOffice.csproj | 3 +++ src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 10 +++++----- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../Umbraco.Web.Website.csproj | 3 +++ templates/Umbraco.Templates.csproj | 4 ++-- .../Umbraco.Tests.Benchmarks.csproj | 2 +- .../Umbraco.Tests.Common.csproj | 2 +- .../Umbraco.Tests.Integration.csproj | 6 +++--- .../Umbraco.Tests.UnitTests.csproj | 2 +- 16 files changed, 42 insertions(+), 30 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index da9b4e3fa7..fa8410d94d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,7 @@ all - 3.5.103 + 3.5.107 diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 97608c756b..5ab8db1d99 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -7,13 +7,16 @@ - - + + - + + + 3.5.107 + diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj index 57055ec96b..5aa062df17 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj +++ b/src/Umbraco.Cms.Persistence.Sqlite/Umbraco.Cms.Persistence.Sqlite.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/Umbraco.Cms/Umbraco.Cms.csproj b/src/Umbraco.Cms/Umbraco.Cms.csproj index 19f64a60aa..df2474b76c 100644 --- a/src/Umbraco.Cms/Umbraco.Cms.csproj +++ b/src/Umbraco.Cms/Umbraco.Cms.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8bcead30bb..01183739d5 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -16,10 +16,10 @@ - + - + diff --git a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs index 305844d7d6..3e497673ce 100644 --- a/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs +++ b/src/Umbraco.Infrastructure/Logging/MessageTemplates.cs @@ -30,14 +30,14 @@ namespace Umbraco.Cms.Core.Logging if (!bound) throw new FormatException($"Could not format message \"{messageTemplate}\" with {args.Length} args."); - var values = boundProperties.ToDictionary(x => x.Name, x => x.Value); + var values = boundProperties!.ToDictionary(x => x.Name, x => x.Value); // this ends up putting every string parameter between quotes //return parsedTemplate.Render(values); // this does not var tw = new StringWriter(); - foreach (var t in parsedTemplate.Tokens) + foreach (var t in parsedTemplate!.Tokens) { if (t is PropertyToken pt && values.TryGetValue(pt.PropertyName, out var propVal) && diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index c304f4e99a..2c6d379073 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -18,9 +18,9 @@ - + - + @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -37,10 +37,10 @@ - + - + @@ -48,7 +48,7 @@ - + @@ -57,6 +57,9 @@ all + + 3.5.107 + diff --git a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj index 13076bd50c..3f24e2717e 100644 --- a/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj +++ b/src/Umbraco.Web.BackOffice/Umbraco.Web.BackOffice.csproj @@ -28,6 +28,9 @@ all + + 3.5.107 + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index a2c7dcb475..e08f45e5af 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -35,14 +35,14 @@ - - + + - - - + + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2274c73327..42e894a7f3 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -24,7 +24,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index 0bb0b6c0ad..176564eaf8 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -35,6 +35,9 @@ all + + 3.5.107 + diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 182b220167..47b2246835 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -1,7 +1,7 @@  - + net6.0 Template @@ -14,7 +14,7 @@ true false - + diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index db34088068..29e0a36353 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -29,7 +29,7 @@ 6.0.0 - 4.17.2 + 4.18.1 diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index 705f3333f9..05a5554db2 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -16,7 +16,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 1954093f66..a6b04cac2b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -86,12 +86,12 @@ - - + + - + all diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 70bf694128..55d875d1bc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -21,7 +21,7 @@ - + From ce8f5f4724b29e70f84d21f6ad1362b21a6b0ecb Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Wed, 8 Jun 2022 08:25:25 +0100 Subject: [PATCH 11/39] Improve FlagOutOfDateModels property behaviour. (cherry picked from commit 54077725c373495fce0d3fbc5cdb6469aad3b676) --- .../Configuration/Models/ModelsBuilderSettings.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 73d046de32..9e14391557 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -42,18 +42,17 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool FlagOutOfDateModels { - get => _flagOutOfDateModels; - - set + get { if (!ModelsMode.IsAuto()) { - _flagOutOfDateModels = false; - return; + return false; } - _flagOutOfDateModels = value; + return _flagOutOfDateModels; } + + set => _flagOutOfDateModels = value; } /// From cf25d29f904f179098e3f8af189afbe4788c5d60 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Wed, 8 Jun 2022 08:16:26 +0100 Subject: [PATCH 12/39] Fix logic error WRT models builder flag out of date models. (#12548) (cherry picked from commit 6b0149803a879d1c6902a5f61d1f2e9dc8545aac) --- src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs index 9e14391557..0d8908fd1c 100644 --- a/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ModelsBuilderSettings.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Core.Configuration.Models { get { - if (!ModelsMode.IsAuto()) + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsAuto()) { return false; } From 7e8c3cb20df56a5e2b39f47a977c7113201d06ee Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 8 Jun 2022 11:05:53 +0200 Subject: [PATCH 13/39] Fixed issue with expected null value. (#12550) Fixes https://github.com/umbraco/Umbraco-CMS/issues/12526 --- src/Umbraco.Web.Common/Views/UmbracoViewPage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs index b1ac11c77d..0b3132b795 100644 --- a/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/Views/UmbracoViewPage.cs @@ -128,7 +128,7 @@ public abstract class UmbracoViewPage : RazorPage { // filter / add preview banner // ASP.NET default value is text/html - if (Context.Response.ContentType.InvariantContains("text/html")) + if (Context.Response?.ContentType?.InvariantContains("text/html") ?? false) { if (((UmbracoContext?.IsDebug ?? false) || (UmbracoContext?.InPreviewMode ?? false)) && tagHelperOutput.TagName != null From 2cc229ef9afdc966a1f327f7e447fb3aff4c51ae Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 8 Jun 2022 21:24:19 +0200 Subject: [PATCH 14/39] Updated Examine to 3.0.0 --- src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj | 2 +- src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj | 2 +- .../Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj index db5a303efd..833e01be50 100644 --- a/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj +++ b/src/Umbraco.Examine.Lucene/Umbraco.Examine.Lucene.csproj @@ -21,7 +21,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 2c6d379073..fc41bec6c5 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -53,7 +53,7 @@ - + all diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index a6b04cac2b..8397da51b4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -85,7 +85,7 @@ - + From 2ab1518862532124d5442ddd425ee37b42db2d2e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jun 2022 09:46:42 +0200 Subject: [PATCH 15/39] Fixes relation issue, when moving a root item to recycle bin, the "Relate Parent Media Folder On Delete"/"Relate Parent Document On Delete" cannot get the parent node type, because it is a fake root. --- src/Umbraco.Core/Models/RelationItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index 75344914f0..634ee53bb2 100644 --- a/src/Umbraco.Core/Models/RelationItem.cs +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Models public string? NodeType { get; set; } [DataMember(Name = "udi")] - public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + public Udi NodeUdi => NodeType == Constants.UdiEntityType.Unknown ? null : Udi.Create(NodeType, NodeKey); [DataMember(Name = "icon")] public string? ContentTypeIcon { get; set; } From d7b015d8c9fd3dfd7b92c0fd62f12e5d0bab47fe Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jun 2022 09:53:28 +0200 Subject: [PATCH 16/39] Fix possible null error --- src/Umbraco.Core/Models/RelationItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index 634ee53bb2..b37afe4d70 100644 --- a/src/Umbraco.Core/Models/RelationItem.cs +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Core.Models public string? NodeType { get; set; } [DataMember(Name = "udi")] - public Udi NodeUdi => NodeType == Constants.UdiEntityType.Unknown ? null : Udi.Create(NodeType, NodeKey); + public Udi? NodeUdi => NodeType == Constants.UdiEntityType.Unknown ? null : Udi.Create(NodeType, NodeKey); [DataMember(Name = "icon")] public string? ContentTypeIcon { get; set; } From 116d5735fb1b9055535f9bc2da8a87a9193ea224 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 9 Jun 2022 10:12:47 +0200 Subject: [PATCH 17/39] Bump version to 10.0.0 final --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 3e15f3bd2a..0f31ff96d5 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.0.0-rc5", + "version": "10.0.0", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From e3f4b86f1c28e0ec30a1ad2a079e308af4cbfbf7 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 10 Jun 2022 09:03:59 +0100 Subject: [PATCH 18/39] Fix attempting to write lock files to LocalTempPath before it exists (#12563) --- .../Runtime/FileSystemMainDomLock.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs index 37e962a222..a7b1610010 100644 --- a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs @@ -1,8 +1,4 @@ -using System; using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; @@ -14,6 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime internal class FileSystemMainDomLock : IMainDomLock { private readonly ILogger _logger; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IOptionsMonitor _globalSettings; private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly string _lockFilePath; @@ -29,6 +26,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime IOptionsMonitor globalSettings) { _logger = logger; + _hostingEnvironment = hostingEnvironment; _globalSettings = globalSettings; var lockFileName = $"MainDom_{mainDomKeyGenerator.GenerateKey()}.lock"; @@ -45,6 +43,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime { try { + 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(); From 88af609c813edc6ad91b0891d3ee8943f91b0376 Mon Sep 17 00:00:00 2001 From: Olivier Bossaer Date: Thu, 16 Jun 2022 15:15:04 +0200 Subject: [PATCH 19/39] Unescape returnPath. --- .../directives/components/application/umblogin.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From 1ab472472ceca88a0b1a560e043361eea6ae130d Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 21 Jun 2022 08:50:00 +0200 Subject: [PATCH 20/39] Fix redirect after logout bug https://github.com/umbraco/Umbraco-CMS/issues/12592 --- .../src/views/common/login.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 5049e7071b381afe85df66ccd231eeb85451eecb Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 21 Jun 2022 08:50:00 +0200 Subject: [PATCH 21/39] Fix redirect after logout bug https://github.com/umbraco/Umbraco-CMS/issues/12592 --- .../src/views/common/login.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 045a487190b1bbab2b03129ba7ba5dbe34194bbf Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Jun 2022 11:44:28 +0200 Subject: [PATCH 22/39] V10: Fix sending content notification (#12597) * Add mappers to map between ContentItemDisplay and ContentItemDisplayWithSchedule * Ensure SendingContentNotification is always sent * Add custom setup hook for UmbracoTestServerTestBase * Add test showing bug/fix * Test schedule being mapped correctly * Obsolete the old constructor * Removed TODO --- .../ContentEditing/ContentItemDisplay.cs | 6 +- .../OutgoingEditorModelEventAttribute.cs | 50 ++- .../Mapping/ContentMapDefinition.cs | 130 +++++- .../UmbracoTestServerTestBase.cs | 401 +++++++++--------- .../OutgoingEditorModelEventFilterTests.cs | 163 +++++++ 5 files changed, 543 insertions(+), 207 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.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.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 8c4db1a041..c1f9dbac4e 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,6 +92,25 @@ 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; diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index f93512863b..2e2a70f29c 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -95,16 +95,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/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); + } + } +} From 389380d8fa89c4ea515de20233479a2f525b9417 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Jun 2022 11:44:28 +0200 Subject: [PATCH 23/39] V10: Fix sending content notification (#12597) * Add mappers to map between ContentItemDisplay and ContentItemDisplayWithSchedule * Ensure SendingContentNotification is always sent * Add custom setup hook for UmbracoTestServerTestBase * Add test showing bug/fix * Test schedule being mapped correctly * Obsolete the old constructor * Removed TODO --- .../OutgoingEditorModelEventAttribute.cs | 48 +++++- .../Mapping/ContentMapDefinition.cs | 123 +++++++++++++ .../UmbracoTestServerTestBase.cs | 15 +- .../OutgoingEditorModelEventFilterTests.cs | 163 ++++++++++++++++++ 4 files changed, 341 insertions(+), 8 deletions(-) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventFilterTests.cs diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 64ac33b1aa..171e8a1bf9 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -3,12 +3,15 @@ using System.Collections; using System.Collections.Generic; 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.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 @@ -29,20 +32,34 @@ namespace Umbraco.Cms.Web.BackOffice.Filters private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - + private readonly IUmbracoMapper _mapper; private readonly IEventAggregator _eventAggregator; - public OutgoingEditorModelEventFilter( + [ActivatorUtilitiesConstructor]public OutgoingEditorModelEventFilter( IUmbracoContextAccessor umbracoContextAccessor, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, IEventAggregator eventAggregator) - { + 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()) + { + } public void OnActionExecuted(ActionExecutedContext context) { @@ -72,7 +89,26 @@ namespace Umbraco.Cms.Web.BackOffice.Filters case ContentItemDisplay content: _eventAggregator.Publish(new SendingContentNotification(content, umbracoContext)); break; - case MediaItemDisplay media: + 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: diff --git a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs index 24cd1c5cbe..9921b59224 100644 --- a/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web.BackOffice/Mapping/ContentMapDefinition.cs @@ -107,7 +107,130 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping 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 private static void Map(IContent source, ContentPropertyCollectionDto target, MapperContext context) diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index f1f8b124e8..1ec90424f3 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -42,6 +42,16 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest 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) + { + } + [SetUp] public void Setup() { @@ -233,8 +243,9 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddWebsite() .AddUmbracoSqlServerSupport() .AddUmbracoSqliteSupport() - .AddTestServices(TestHelper) // This is the important one! - .Build(); + .AddTestServices(TestHelper); // This is the important one! + CustomTestSetup(builder); + builder.Build(); } /// 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); + } + } +} From 42e2254e30f47e7a1f9ea8795ba226459e8e3d9f Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 22 Jun 2022 08:25:18 +0200 Subject: [PATCH 24/39] Only send telemetry if we're on runtimelevel run (#12600) --- .../HostedServices/ReportSiteTask.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 658b8dd47d..cf209109fc 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"); From 24222fa4d2d96d0b118fb043998fb2ed81c81ac0 Mon Sep 17 00:00:00 2001 From: Olivier Bossaer Date: Thu, 16 Jun 2022 15:15:04 +0200 Subject: [PATCH 25/39] Unescape returnPath. (cherry picked from commit 88af609c813edc6ad91b0891d3ee8943f91b0376) --- .../directives/components/application/umblogin.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From 46d71838f2730573f1cd106a21707b3f2aaeebe5 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 22 Jun 2022 08:25:18 +0200 Subject: [PATCH 26/39] Only send telemetry if we're on runtimelevel run (#12600) --- .../HostedServices/ReportSiteTask.cs | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index 54137fad99..c717569d53 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -6,31 +6,43 @@ 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; -namespace Umbraco.Cms.Infrastructure.HostedServices +namespace Umbraco.Cms.Infrastructure.HostedServices; + +public class ReportSiteTask : RecurringHostedServiceBase { - public class ReportSiteTask : RecurringHostedServiceBase - { - private readonly ILogger _logger; - private readonly ITelemetryService _telemetryService; - private static HttpClient s_httpClient = new(); + 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; - s_httpClient = new HttpClient(); + _telemetryService = telemetryService;_runtimeState = runtimeState; + _httpClient = new HttpClient(); } - [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] + [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, IUmbracoVersion umbracoVersion, @@ -45,7 +57,13 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// public override async Task PerformExecuteAsync(object? state) { - if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false) + 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"); @@ -54,19 +72,19 @@ namespace Umbraco.Cms.Infrastructure.HostedServices try { - if (s_httpClient.BaseAddress is null) + if (_httpClient.BaseAddress is null) { // Send data to LIVE telemetry - s_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); #if DEBUG // Send data to DEBUG telemetry service - s_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); #endif } - s_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { @@ -75,7 +93,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices // Make a HTTP Post to telemetry service // https://telemetry.umbraco.com/installs/ // Fire & Forget, do not need to know if its a 200, 500 etc - using (HttpResponseMessage response = await s_httpClient.SendAsync(request)) + using (HttpResponseMessage response = await _httpClient.SendAsync(request)) { } } From 20f9cfe2ac878c82ef7167a57bfdbc7d9594768b Mon Sep 17 00:00:00 2001 From: Nikolaj Date: Thu, 23 Jun 2022 15:25:25 +0200 Subject: [PATCH 27/39] Fix ReportSiteTask post merge --- .../HostedServices/ReportSiteTask.cs | 116 +++++++++--------- 1 file changed, 57 insertions(+), 59 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs index c717569d53..dc73d0cbbb 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs @@ -1,7 +1,4 @@ -using System; -using System.Net.Http; using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -23,18 +20,19 @@ public class ReportSiteTask : RecurringHostedServiceBase private readonly ITelemetryService _telemetryService; private readonly IRuntimeState _runtimeState; - public ReportSiteTask( - ILogger logger, - ITelemetryService telemetryService, - IRuntimeState runtimeState) + public ReportSiteTask( + ILogger logger, + ITelemetryService telemetryService, + IRuntimeState runtimeState) : base(logger, TimeSpan.FromDays(1), TimeSpan.FromMinutes(5)) - { - _logger = logger; - _telemetryService = telemetryService;_runtimeState = runtimeState; - _httpClient = new HttpClient(); - } + { + _logger = logger; + _telemetryService = telemetryService; + _runtimeState = runtimeState; + _httpClient = new HttpClient(); + } - [Obsolete("Use the constructor that takes IRuntimeState, scheduled for removal in V12")] + [Obsolete("Use the constructor that takes IRuntimeState, scheduled for removal in V12")] public ReportSiteTask( ILogger logger, ITelemetryService telemetryService) @@ -43,68 +41,68 @@ public class ReportSiteTask : RecurringHostedServiceBase } [Obsolete("Use the constructor that takes ITelemetryService instead, scheduled for removal in V11")] - public ReportSiteTask( - ILogger logger, - IUmbracoVersion umbracoVersion, - IOptions globalSettings) - : this(logger, StaticServiceProvider.Instance.GetRequiredService()) - { - } + public ReportSiteTask( + ILogger logger, + IUmbracoVersion umbracoVersion, + IOptions globalSettings) + : this(logger, StaticServiceProvider.Instance.GetRequiredService()) + { + } - /// - /// Runs the background task to send the anonymous ID - /// to telemetry service - /// - public override async Task PerformExecuteAsync(object? state) - { - if (_runtimeState.Level is not RuntimeLevel.Run) + /// + /// Runs the background task to send the anonymous ID + /// to telemetry service + /// + 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"); + { + _logger.LogWarning("No telemetry marker found"); - return; - } + return; + } - try + try + { + if (_httpClient.BaseAddress is null) { - if (_httpClient.BaseAddress is null) - { - // Send data to LIVE telemetry - _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); + // Send data to LIVE telemetry + _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/"); #if DEBUG - // Send data to DEBUG telemetry service - _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); + // Send data to DEBUG telemetry service + _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/"); #endif - } - - - _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); - - using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) - { - request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header - - // Make a HTTP Post to telemetry service - // https://telemetry.umbraco.com/installs/ - // Fire & Forget, do not need to know if its a 200, 500 etc - using (HttpResponseMessage response = await _httpClient.SendAsync(request)) - { - } - } } - catch + + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json"); + + using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/")) { - // Silently swallow - // The user does not need the logs being polluted if our service has fallen over or is down etc - // Hence only logging this at a more verbose level (which users should not be using in production) - _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, + "application/json"); //CONTENT-TYPE header + + // Make a HTTP Post to telemetry service + // https://telemetry.umbraco.com/installs/ + // Fire & Forget, do not need to know if its a 200, 500 etc + using (HttpResponseMessage response = await _httpClient.SendAsync(request)) + { + } } } + catch + { + // Silently swallow + // The user does not need the logs being polluted if our service has fallen over or is down etc + // Hence only logging this at a more verbose level (which users should not be using in production) + _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service"); + } } } From b232ff98a0b11cb163f2f31144cc3f356774bddf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 27 Jun 2022 08:25:54 +0200 Subject: [PATCH 28/39] Fix formatting after merge and fix nullability --- .../Routing/PublicAccessRequestHandler.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) 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"); From 5988a4c87d5826b166b36108f04fd07877c9beb8 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Mon, 27 Jun 2022 12:29:27 +0100 Subject: [PATCH 29/39] Also read the plugin files using the content provider. (#12552) --- .../DependencyInjection/UmbracoBuilder.LocalizedText.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); } From fb35c47c702631a3967a64ef37879a97bb779084 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Mon, 27 Jun 2022 12:29:27 +0100 Subject: [PATCH 30/39] Also read the plugin files using the content provider. (#12552) --- .../DependencyInjection/UmbracoBuilder.LocalizedText.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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); } From 3cfeac94a4a51087f291baa38c56697890b38ff4 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 27 Jun 2022 14:41:59 +0200 Subject: [PATCH 31/39] V10: Fix error when opening recycle bin (#12619) * Make object type parameter nullable in configuration editor * Add delete from recycle bin test * Clean up after test --- .../ContentApps/ListViewContentAppFactory.cs | 11 ++- .../ContentEditing/ContentPropertyDisplay.cs | 5 ++ .../PropertyEditors/IConfigurationEditor.cs | 5 ++ .../cypress/integration/Content/recycleBin.ts | 78 +++++++++++++++++++ 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/recycleBin.ts 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/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/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); + }); +}); From 62eeb420044371358cc7c48594f6b37125745c9f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 29 Jun 2022 16:49:47 +0200 Subject: [PATCH 32/39] Changed const to return "Microsoft.Data.Sqlite" instead of "Microsoft.Data.SQLite" and made our checks case insensitive where possible. Sadly DbProviderFactories is still case sensative so for that we support both strings. (#12639) https://github.com/umbraco/Umbraco-CMS/issues/12604 --- .../Services/SqlServerDistributedLockingMechanism.cs | 2 +- src/Umbraco.Cms.Persistence.Sqlite/Constants.cs | 5 ++++- .../Services/SqliteDistributedLockingMechanism.cs | 2 +- .../UmbracoBuilderExtensions.cs | 5 ++++- .../Persistence/DatabaseProviderMetadataExtensions.cs | 2 +- .../Persistence/DbProviderFactoryCreator.cs | 10 +++++----- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 7c8effb2b3..1777ff68ce 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 76e408423c..ae58a70aa3 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 4a47d41846..1e65afae36 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -36,7 +36,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 8843844818..f9386850fa 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -39,11 +39,14 @@ public static class UmbracoBuilderExtensions DbProviderFactories.UnregisterFactory(Constants.ProviderName); DbProviderFactories.RegisterFactory(Constants.ProviderName, Microsoft.Data.Sqlite.SqliteFactory.Instance); + DbProviderFactories.UnregisterFactory(Constants.ProviderNameLegacy); + DbProviderFactories.RegisterFactory(Constants.ProviderNameLegacy, Microsoft.Data.Sqlite.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.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 0177475609..0efa541040 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -44,10 +44,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence { _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,6 +98,6 @@ namespace Umbraco.Cms.Infrastructure.Persistence } public IEnumerable GetProviderSpecificInterceptors(string providerName) - => _providerSpecificInterceptors.Where(x => x.ProviderName == providerName); + => _providerSpecificInterceptors.Where(x => x.ProviderName.Equals(providerName, StringComparison.InvariantCultureIgnoreCase)); } } From fd0c4fda54b1d5cd22e8ab196801280b09672a26 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 30 Jun 2022 08:34:58 +0200 Subject: [PATCH 33/39] bumb version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 0f31ff96d5..93c727fb19 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.0.0", + "version": "10.0.1", "assemblyVersion": { "precision": "Build" // optional. Use when you want a more precise assembly version than the default major.minor. }, From 5eb5cb7e2b4b9c2970aea2aefdf96bb5a219e9a4 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 30 Jun 2022 09:57:45 +0200 Subject: [PATCH 34/39] Fix up after merge --- .../Runtime/FileSystemMainDomLock.cs | 14 +++++++------- .../Scoping/ScopeProvider.cs | 3 --- .../Filters/OutgoingEditorModelEventAttribute.cs | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs index f9ce2837e0..26b8d55f96 100644 --- a/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/FileSystemMainDomLock.cs @@ -11,8 +11,7 @@ internal class FileSystemMainDomLock : IMainDomLock { private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly IHostingEnvironment _hostingEnvironment; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IOptionsMonitor _globalSettings; + private readonly IOptionsMonitor _globalSettings; private readonly string _lockFilePath; private readonly ILogger _logger; private readonly string _releaseSignalFilePath; @@ -28,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); @@ -44,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); @@ -65,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); } @@ -95,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/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index 9431af4eb9..31249daa67 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -10,9 +10,6 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; -using Umbraco.Cms.Core.DistributedLocking; -using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Web.Common.DependencyInjection; using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings; #if DEBUG_SCOPES diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index bc19688427..3121e654af 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -27,7 +27,7 @@ internal sealed class OutgoingEditorModelEventAttribute : TypeFilterAttribute private class OutgoingEditorModelEventFilter : IActionFilter { - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;private readonly IUmbracoMapper _mapper; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; private readonly IEventAggregator _eventAggregator; private readonly IUmbracoMapper _mapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; From be1fddb9beab4f508384d38a4c60151e5c1bfa22 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 30 Jun 2022 14:08:13 +0200 Subject: [PATCH 35/39] Added configuration to allow RTL cultures to opt-in to reverse the url hierarchy (#12635) * Added configuration to allow RTL cultures to opt-in to reverse the url hierarchy. https://github.com/umbraco/Umbraco-CMS/issues/12621 * Fixed bug.. There is difference between array.Reverse (Linq) and list.Reverse (native) * formatting --- .../Configuration/Models/GlobalSettings.cs | 21 ++++++++++++++++++- .../ContentCache.cs | 15 +++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) 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.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) From 30c5068f835ff3d384213d4458c162fa2c178b18 Mon Sep 17 00:00:00 2001 From: Zeegaan Date: Thu, 30 Jun 2022 15:28:06 +0200 Subject: [PATCH 36/39] Get cultureName from culture info if null --- .../Persistence/Factories/LanguageFactory.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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, From 8de95acec669686d1421f6caaf18ad42cf00ce47 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 30 Jun 2022 15:30:42 +0200 Subject: [PATCH 37/39] Fixed casing --- templates/UmbracoProject/.template.config/template.json | 2 +- tests/Umbraco.Tests.AcceptanceTest/misc/umbraco-linux.docker | 2 +- .../Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs index 11eb292ac2..b9d9574bd6 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(); From 7d545a7dfc68b30adec0c8c3c4db5bd9f24bfc9f Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 30 Jun 2022 22:52:59 +0200 Subject: [PATCH 38/39] Fix test after Sqlite casing change --- src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs | 3 ++- .../Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs index a4b90881b1..b3002fa8fe 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/UmbracoBuilderExtensions.cs @@ -46,8 +46,9 @@ 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, Microsoft.Data.Sqlite.SqliteFactory.Instance); + DbProviderFactories.RegisterFactory(Constants.ProviderNameLegacy, SqliteFactory.Instance); // Prevent accidental creation of SQLite database files builder.Services.PostConfigureAll(options => diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs index b9d9574bd6..cd1ec39dc9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ConfigurationExtensionsTests.cs @@ -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); } From dca4d0f16709eb5ad04372c49e9441c7dd008b89 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Fri, 1 Jul 2022 08:48:05 +0200 Subject: [PATCH 39/39] Add runtime mode (BackofficeDevelopment, Development and Production) (#12631) * Add runtime mode setting * Only enable Razor runtime compilation in development modes * Only enable ModelsBuilder generation in development modes * Fix disabling ModelsBuilder controllers * Add IRuntimeModeValidationService and IRuntimeModeValidator * Add JITOptimizerValidator * Add UmbracoApplicationUrlValidator * Add UseHttpsValidator * Add RuntimeMinificationValidator * Add ModelsBuilderModeValidator * Remove .NET 6 preview 1 fix for Razor runtime compilation * Only allow InMemoryAuto in backoffice development mode * Make runtime mode validators public, so they can be easily removed if required * Add comment to highlight removing RazorCompileOnBuild, RazorCompileOnPublish and CopyRazorGenerateFilesToPublishDirectory when using ModelsMode InMemoryAuto * Add documentation * Update src/Umbraco.Web.Common/ModelsBuilder/NoopModelsBuilderDashboardProvider.cs Co-authored-by: Ronald Barendse Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Configuration/Models/RuntimeMode.cs | 22 + .../Configuration/Models/RuntimeSettings.cs | 12 +- src/Umbraco.Core/Constants-Configuration.cs | 13 +- .../DependencyInjection/UmbracoBuilder.cs | 3 - .../Extensions/ConfigurationExtensions.cs | 13 +- .../HealthCheckSettingsExtensions.cs | 0 .../UmbracoBuilder.CoreServices.cs | 17 +- .../Runtime/IRuntimeModeValidationService.cs | 18 + .../Runtime/IRuntimeModeValidator.cs | 20 + .../Runtime/RuntimeModeValidationService.cs | 49 ++ .../JITOptimizerValidator.cs | 29 + .../ModelsBuilderModeValidator.cs | 43 ++ .../RuntimeMinificationValidator.cs | 34 ++ .../RuntimeModeProductionValidatorBase.cs | 32 + .../UmbracoApplicationUrlValidator.cs | 34 ++ .../UseHttpsValidator.cs | 34 ++ .../Runtime/RuntimeState.cs | 562 ++++++++++-------- .../UmbracoBuilderExtensions.cs | 55 +- ...DisableModelsBuilderNotificationHandler.cs | 2 +- .../ModelsBuilder/UmbracoBuilderExtensions.cs | 45 +- .../UmbracoBuilderExtensions.cs | 37 +- ...acoBuilderDependencyInjectionExtensions.cs | 76 +-- .../NoopModelsBuilderDashboardProvider.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 52 +- .../UmbracoProject/UmbracoProject.csproj | 7 +- 25 files changed, 791 insertions(+), 420 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/RuntimeMode.cs rename src/Umbraco.Core/{Configuration => }/Extensions/HealthCheckSettingsExtensions.cs (100%) create mode 100644 src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidationService.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/IRuntimeModeValidator.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidationService.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/JITOptimizerValidator.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/ModelsBuilderModeValidator.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeMinificationValidator.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/RuntimeModeProductionValidatorBase.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UmbracoApplicationUrlValidator.cs create mode 100644 src/Umbraco.Infrastructure/Runtime/RuntimeModeValidators/UseHttpsValidator.cs 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/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 6ac06ae1b7..fd7923eec7 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.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 94134ab0d1..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,11 +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(); builder.Services.AddSingleton(f => f.GetRequiredService()); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddScoped(); 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.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/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/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/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