From 366a8ba56501bf49ff641852200ced7155bbf548 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 17 Jan 2022 10:38:34 +0000 Subject: [PATCH 01/39] Prevent issues for those who wish use ServiceBasedControllerActivator. Adds registration for CurrentUserController which has ambiguous constructors. Register our controllers so that issues are visible during dev/test. --- .../UmbracoBuilderExtensions.cs | 20 +++++- .../ControllersAsServicesComposer.cs | 61 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 2c801e963b..46002a6c8d 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; @@ -54,7 +55,8 @@ namespace Umbraco.Extensions .AddCoreNotifications() .AddLogViewer() .AddExamine() - .AddExamineIndexes(); + .AddExamineIndexes() + .AddControllersWithAmbiguousConstructors(); public static IUmbracoBuilder AddUnattendedInstallInstallCreateUser(this IUmbracoBuilder builder) { @@ -119,5 +121,21 @@ namespace Umbraco.Extensions return builder; } + + /// + /// Adds explicit registrations for controllers with ambiguous constructors to prevent downstream issues for + /// users who wish to use + /// + public static IUmbracoBuilder AddControllersWithAmbiguousConstructors( + this IUmbracoBuilder builder) + { + builder.Services.TryAddTransient(sp => + { + IUserDataService userDataService = sp.GetRequiredService(); + return ActivatorUtilities.CreateInstance(sp, userDataService); + }); + + return builder; + } } } diff --git a/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs new file mode 100644 index 0000000000..042343df67 --- /dev/null +++ b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs @@ -0,0 +1,61 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.DependencyInjection; + +namespace Umbraco.Cms.Web.UI.Composers +{ + /// + /// Adds controllers to the service collection. + /// + /// + /// + /// Umbraco 9 out of the box, makes use of which doesn't resolve controller + /// instances from the IOC container, instead it resolves the required dependencies of the controller and constructs an instance + /// of the controller. + /// + /// + /// Some users may wish to switch to (perhaps to make use of interception/decoration). + /// + /// + /// This composer exists to help us detect ambiguous constructors in the CMS such that we do not cause unnecessary effort downstream. + /// + /// + /// This Composer is not shipped by the Umbraco.Templates package. + /// + /// + public class ControllersAsServicesComposer : IComposer + { + /// + public void Compose(IUmbracoBuilder builder) => builder.Services + .AddMvc() + .AddControllersAsServicesWithoutChangingActivator(); + } + + internal static class MvcBuilderExtensions + { + /// + /// but without the replacement of + /// . + /// + /// + /// We don't need to opt in to to ensure container validation + /// passes. + /// + public static IMvcBuilder AddControllersAsServicesWithoutChangingActivator(this IMvcBuilder builder) + { + var feature = new ControllerFeature(); + builder.PartManager.PopulateFeature(feature); + + foreach (Type controller in feature.Controllers.Select(c => c.AsType())) + { + builder.Services.TryAddTransient(controller, controller); + } + + return builder; + } + } +} From 8b773c90c20045bc2a336f482c1c1a70e6030b0f Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 19 Jan 2022 15:43:14 +0100 Subject: [PATCH 02/39] Add IHtmlSanitizer --- src/Umbraco.Core/Security/IHtmlSanitizer.cs | 7 +++++++ src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/Umbraco.Core/Security/IHtmlSanitizer.cs create mode 100644 src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs new file mode 100644 index 0000000000..7f3f033ba7 --- /dev/null +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Security +{ + public interface IHtmlSanitizer + { + string Sanitize(string html); + } +} diff --git a/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs new file mode 100644 index 0000000000..f16ce81ce1 --- /dev/null +++ b/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Security +{ + public class NoOpHtmlSanitizer : IHtmlSanitizer + { + public string Sanitize(string html) + { + return html; + } + } +} From e2d0a0f699c755821df3a847aed5f0e1b170d64f Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 24 Jan 2022 08:38:31 +0100 Subject: [PATCH 03/39] Add docstrings to IHtmlSanitizer --- src/Umbraco.Core/Security/IHtmlSanitizer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs index 7f3f033ba7..fa1e0b3ee5 100644 --- a/src/Umbraco.Core/Security/IHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -2,6 +2,11 @@ namespace Umbraco.Core.Security { public interface IHtmlSanitizer { + /// + /// Sanitizes HTML + /// + /// HTML to be sanitized + /// Sanitized HTML string Sanitize(string html); } } From 01c1e68cf023887ef25af6bda2c7b11efb816ad3 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 24 Jan 2022 09:19:06 +0100 Subject: [PATCH 04/39] Fix up namespaces --- src/Umbraco.Core/Security/IHtmlSanitizer.cs | 2 +- src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs index fa1e0b3ee5..9bcfe405dd 100644 --- a/src/Umbraco.Core/Security/IHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.Security +namespace Umbraco.Cms.Core.Security { public interface IHtmlSanitizer { diff --git a/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs index f16ce81ce1..f2e8a48ad0 100644 --- a/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.Security +namespace Umbraco.Cms.Core.Security { public class NoOpHtmlSanitizer : IHtmlSanitizer { From 249774c815345293ea98c56addb1dd6604ee51f8 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 24 Jan 2022 09:23:07 +0100 Subject: [PATCH 05/39] Rename NoOp to Noop To match the rest of the classes --- src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs | 3 +++ .../Security/{NoOpHtmlSanitizer.cs => NoopHtmlSanitizer.cs} | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) rename src/Umbraco.Core/Security/{NoOpHtmlSanitizer.cs => NoopHtmlSanitizer.cs} (73%) diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index eacd615830..c4a95d45e5 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -263,6 +263,9 @@ namespace Umbraco.Cms.Core.DependencyInjection // Register telemetry service used to gather data about installed packages Services.AddUnique(); + + // Register a noop IHtmlSanitizer to be replaced + Services.AddUnique(); } } } diff --git a/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs similarity index 73% rename from src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs rename to src/Umbraco.Core/Security/NoopHtmlSanitizer.cs index f2e8a48ad0..2ada23631a 100644 --- a/src/Umbraco.Core/Security/NoOpHtmlSanitizer.cs +++ b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Core.Security { - public class NoOpHtmlSanitizer : IHtmlSanitizer + public class NoopHtmlSanitizer : IHtmlSanitizer { public string Sanitize(string html) { From 39f7102312f55d08a18c86132a65479250966653 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 24 Jan 2022 09:30:23 +0100 Subject: [PATCH 06/39] Use IHtmlSanitizer in RichTextValueEditor --- .../PropertyEditors/RichTextPropertyEditor.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 1f05da3bde..8eeb935c12 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -81,6 +81,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlSanitizer _htmlSanitizer; public RichTextPropertyValueEditor( DataEditorAttribute attribute, @@ -92,7 +93,8 @@ namespace Umbraco.Cms.Core.PropertyEditors RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) + IIOHelper ioHelper, + IHtmlSanitizer htmlSanitizer) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; @@ -100,6 +102,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _localLinkParser = localLinkParser; _pastedImages = pastedImages; _imageUrlGenerator = imageUrlGenerator; + _htmlSanitizer = htmlSanitizer; } /// @@ -156,8 +159,9 @@ namespace Umbraco.Cms.Core.PropertyEditors var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); + var sanitized = _htmlSanitizer.Sanitize(parsed); - return parsed.NullOrWhiteSpaceAsNull(); + return sanitized.NullOrWhiteSpaceAsNull(); } /// From ca56e0971f41314c416f1f55e89ae6dba553abfd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 25 Jan 2022 06:38:20 +0100 Subject: [PATCH 07/39] Add IsRestarting property to Umbraco application notifications (#11883) * Add IsRestarting property to Umbraco application notifications * Add IUmbracoApplicationLifetimeNotification and update constructors * Only subscribe to events on initial startup * Cleanup CoreRuntime * Do not reset StaticApplicationLogging instance after stopping/during restart --- ...IUmbracoApplicationLifetimeNotification.cs | 17 +++ .../UmbracoApplicationStartedNotification.cs | 15 ++- .../UmbracoApplicationStartingNotification.cs | 27 ++++- .../UmbracoApplicationStoppedNotification.cs | 15 ++- .../UmbracoApplicationStoppingNotification.cs | 27 ++++- .../Runtime/CoreRuntime.cs | 106 +++++++++--------- 6 files changed, 139 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs diff --git a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs new file mode 100644 index 0000000000..4b0ea6826a --- /dev/null +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// + /// + public interface IUmbracoApplicationLifetimeNotification : INotification + { + /// + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). + /// + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs index a3d38720d7..196af7dfe1 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. /// - /// - public class UmbracoApplicationStartedNotification : INotification - { } + /// + public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index dd60f9431c..82b87aa3bf 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,16 +1,34 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). /// - /// - public class UmbracoApplicationStartingNotification : INotification + /// + public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification { /// /// Initializes a new instance of the class. /// /// The runtime level - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) + : this(runtimeLevel, false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) + { + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } /// /// Gets the runtime level. @@ -19,5 +37,8 @@ namespace Umbraco.Cms.Core.Notifications /// The runtime level. /// public RuntimeLevel RuntimeLevel { get; } + + /// + public bool IsRestarting { get; } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index be4c6ccfd4..c6dac40a26 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely shutdown. /// - /// - public class UmbracoApplicationStoppedNotification : INotification - { } + /// + public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index 6d5234bbcc..062ca954d9 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,9 +1,30 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs when Umbraco is shutting down (after all s are terminated). /// - /// - public class UmbracoApplicationStoppingNotification : INotification - { } + /// + public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStoppingNotification() + : this(false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 5dbe78c2f5..851d67e713 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -134,60 +134,48 @@ namespace Umbraco.Cms.Infrastructure.Runtime public IRuntimeState State { get; } /// - public async Task RestartAsync() - { - await StopAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(), _cancellationToken); - await StartAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(), _cancellationToken); - } + public async Task StartAsync(CancellationToken cancellationToken) => await StartAsync(cancellationToken, false); /// - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) => await StopAsync(cancellationToken, false); + + /// + public async Task RestartAsync() + { + await StopAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(true), _cancellationToken); + await StartAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(true), _cancellationToken); + } + + private async Task StartAsync(CancellationToken cancellationToken, bool isRestarting) { // Store token, so we can re-use this during restart _cancellationToken = cancellationToken; - StaticApplicationLogging.Initialize(_loggerFactory); - StaticServiceProvider.Instance = _serviceProvider; - - AppDomain.CurrentDomain.UnhandledException += (_, args) => + if (isRestarting == false) { - var exception = (Exception)args.ExceptionObject; - var isTerminating = args.IsTerminating; // always true? + StaticApplicationLogging.Initialize(_loggerFactory); + StaticServiceProvider.Instance = _serviceProvider; - var msg = "Unhandled exception in AppDomain"; - - if (isTerminating) - { - msg += " (terminating)"; - } - - msg += "."; - - _logger.LogError(exception, msg); - }; - - // Add application started and stopped notifications (only on initial startup, not restarts) - if (_hostApplicationLifetime.ApplicationStarted.IsCancellationRequested == false) - { - _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification())); - _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification())); + AppDomain.CurrentDomain.UnhandledException += (_, args) + => _logger.LogError(args.ExceptionObject as Exception, $"Unhandled exception in AppDomain{(args.IsTerminating ? " (terminating)" : null)}."); } - // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate + // Acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); // TODO (V10): Remove this obsoleted notification publish. await _eventAggregator.PublishAsync(new UmbracoApplicationMainDomAcquiredNotification(), cancellationToken); - // notify for unattended install + // Notify for unattended install await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken); DetermineRuntimeLevel(); if (!State.UmbracoCanBoot()) { - return; // The exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware + return; } IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; @@ -196,7 +184,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + // If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification(); await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken); switch (unattendedUpgradeNotification.UnattendedUpgradeResult) @@ -207,54 +195,59 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered"); } - // we cannot continue here, the exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware return; case RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete: case RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete: - // upgrade is done, set reason to Run + // Upgrade is done, set reason to Run DetermineRuntimeLevel(); break; case RuntimeUnattendedUpgradeNotification.UpgradeResult.NotRequired: break; } - // TODO (V10): Remove this obsoleted notification publish. + // TODO (V10): Remove this obsoleted notification publish await _eventAggregator.PublishAsync(new UmbracoApplicationComponentsInstallingNotification(State.Level), cancellationToken); - // create & initialize the components + // Initialize the components _components.Initialize(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level), cancellationToken); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); + + if (isRestarting == false) + { + // Add application started and stopped notifications last (to ensure they're always published after starting) + _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false))); + _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false))); + } } - public async Task StopAsync(CancellationToken cancellationToken) + private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { _components.Terminate(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(), cancellationToken); - StaticApplicationLogging.Initialize(null); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } private void AcquireMainDom() { - using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired."); + + try { - try - { - _mainDom.Acquire(_applicationShutdownRegistry); - } - catch - { - timer?.Fail(); - throw; - } + _mainDom.Acquire(_applicationShutdownRegistry); + } + catch + { + timer?.Fail(); + throw; } } private void DetermineRuntimeLevel() { - if (State.BootFailedException != null) + if (State.BootFailedException is not null) { - // there's already been an exception so cannot boot and no need to check + // There's already been an exception, so cannot boot and no need to check return; } @@ -277,7 +270,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime State.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException); timer?.Fail(); _logger.LogError(ex, "Boot Failed"); - // We do not throw the exception. It will be rethrown by BootFailedMiddleware + + // We do not throw the exception, it will be rethrown by BootFailedMiddleware } } } From 76593aa7ca2b49add058efe2825d7183cf7d5d36 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 25 Jan 2022 06:38:20 +0100 Subject: [PATCH 08/39] Add IsRestarting property to Umbraco application notifications (#11883) * Add IsRestarting property to Umbraco application notifications * Add IUmbracoApplicationLifetimeNotification and update constructors * Only subscribe to events on initial startup * Cleanup CoreRuntime * Do not reset StaticApplicationLogging instance after stopping/during restart --- ...IUmbracoApplicationLifetimeNotification.cs | 17 +++ .../UmbracoApplicationStartedNotification.cs | 15 ++- .../UmbracoApplicationStartingNotification.cs | 27 ++++- .../UmbracoApplicationStoppedNotification.cs | 15 ++- .../UmbracoApplicationStoppingNotification.cs | 27 ++++- .../Runtime/CoreRuntime.cs | 106 +++++++++--------- 6 files changed, 139 insertions(+), 68 deletions(-) create mode 100644 src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs diff --git a/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs new file mode 100644 index 0000000000..4b0ea6826a --- /dev/null +++ b/src/Umbraco.Core/Notifications/IUmbracoApplicationLifetimeNotification.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Represents an Umbraco application lifetime (starting, started, stopping, stopped) notification. + /// + /// + public interface IUmbracoApplicationLifetimeNotification : INotification + { + /// + /// Gets a value indicating whether Umbraco is restarting (e.g. after an install or upgrade). + /// + /// + /// true if Umbraco is restarting; otherwise, false. + /// + bool IsRestarting { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs index a3d38720d7..196af7dfe1 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely booted up and the request processing pipeline is configured. /// - /// - public class UmbracoApplicationStartedNotification : INotification - { } + /// + public class UmbracoApplicationStartedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index dd60f9431c..82b87aa3bf 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,16 +1,34 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). /// - /// - public class UmbracoApplicationStartingNotification : INotification + /// + public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification { /// /// Initializes a new instance of the class. /// /// The runtime level - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) + : this(runtimeLevel, false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) + { + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } /// /// Gets the runtime level. @@ -19,5 +37,8 @@ namespace Umbraco.Cms.Core.Notifications /// The runtime level. /// public RuntimeLevel RuntimeLevel { get; } + + /// + public bool IsRestarting { get; } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs index be4c6ccfd4..c6dac40a26 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -3,7 +3,16 @@ namespace Umbraco.Cms.Core.Notifications /// /// Notification that occurs when Umbraco has completely shutdown. /// - /// - public class UmbracoApplicationStoppedNotification : INotification - { } + /// + public class UmbracoApplicationStoppedNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppedNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index 6d5234bbcc..062ca954d9 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,9 +1,30 @@ +using System; + namespace Umbraco.Cms.Core.Notifications { /// /// Notification that occurs when Umbraco is shutting down (after all s are terminated). /// - /// - public class UmbracoApplicationStoppingNotification : INotification - { } + /// + public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification + { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use ctor with all params")] + public UmbracoApplicationStoppingNotification() + : this(false) + { + // TODO: Remove this constructor in V10 + } + + /// + /// Initializes a new instance of the class. + /// + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + + /// + public bool IsRestarting { get; } + } } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 5dbe78c2f5..851d67e713 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -134,60 +134,48 @@ namespace Umbraco.Cms.Infrastructure.Runtime public IRuntimeState State { get; } /// - public async Task RestartAsync() - { - await StopAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(), _cancellationToken); - await StartAsync(_cancellationToken); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(), _cancellationToken); - } + public async Task StartAsync(CancellationToken cancellationToken) => await StartAsync(cancellationToken, false); /// - public async Task StartAsync(CancellationToken cancellationToken) + public async Task StopAsync(CancellationToken cancellationToken) => await StopAsync(cancellationToken, false); + + /// + public async Task RestartAsync() + { + await StopAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppedNotification(true), _cancellationToken); + await StartAsync(_cancellationToken, true); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartedNotification(true), _cancellationToken); + } + + private async Task StartAsync(CancellationToken cancellationToken, bool isRestarting) { // Store token, so we can re-use this during restart _cancellationToken = cancellationToken; - StaticApplicationLogging.Initialize(_loggerFactory); - StaticServiceProvider.Instance = _serviceProvider; - - AppDomain.CurrentDomain.UnhandledException += (_, args) => + if (isRestarting == false) { - var exception = (Exception)args.ExceptionObject; - var isTerminating = args.IsTerminating; // always true? + StaticApplicationLogging.Initialize(_loggerFactory); + StaticServiceProvider.Instance = _serviceProvider; - var msg = "Unhandled exception in AppDomain"; - - if (isTerminating) - { - msg += " (terminating)"; - } - - msg += "."; - - _logger.LogError(exception, msg); - }; - - // Add application started and stopped notifications (only on initial startup, not restarts) - if (_hostApplicationLifetime.ApplicationStarted.IsCancellationRequested == false) - { - _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification())); - _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification())); + AppDomain.CurrentDomain.UnhandledException += (_, args) + => _logger.LogError(args.ExceptionObject as Exception, $"Unhandled exception in AppDomain{(args.IsTerminating ? " (terminating)" : null)}."); } - // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate + // Acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); // TODO (V10): Remove this obsoleted notification publish. await _eventAggregator.PublishAsync(new UmbracoApplicationMainDomAcquiredNotification(), cancellationToken); - // notify for unattended install + // Notify for unattended install await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken); DetermineRuntimeLevel(); if (!State.UmbracoCanBoot()) { - return; // The exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware + return; } IApplicationShutdownRegistry hostingEnvironmentLifetime = _applicationShutdownRegistry; @@ -196,7 +184,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade + // If level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade var unattendedUpgradeNotification = new RuntimeUnattendedUpgradeNotification(); await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken); switch (unattendedUpgradeNotification.UnattendedUpgradeResult) @@ -207,54 +195,59 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"Unattended upgrade result was {RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors} but no {nameof(BootFailedException)} was registered"); } - // we cannot continue here, the exception will be rethrown by BootFailedMiddelware + // We cannot continue here, the exception will be rethrown by BootFailedMiddelware return; case RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete: case RuntimeUnattendedUpgradeNotification.UpgradeResult.PackageMigrationComplete: - // upgrade is done, set reason to Run + // Upgrade is done, set reason to Run DetermineRuntimeLevel(); break; case RuntimeUnattendedUpgradeNotification.UpgradeResult.NotRequired: break; } - // TODO (V10): Remove this obsoleted notification publish. + // TODO (V10): Remove this obsoleted notification publish await _eventAggregator.PublishAsync(new UmbracoApplicationComponentsInstallingNotification(State.Level), cancellationToken); - // create & initialize the components + // Initialize the components _components.Initialize(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level), cancellationToken); + await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); + + if (isRestarting == false) + { + // Add application started and stopped notifications last (to ensure they're always published after starting) + _hostApplicationLifetime.ApplicationStarted.Register(() => _eventAggregator.Publish(new UmbracoApplicationStartedNotification(false))); + _hostApplicationLifetime.ApplicationStopped.Register(() => _eventAggregator.Publish(new UmbracoApplicationStoppedNotification(false))); + } } - public async Task StopAsync(CancellationToken cancellationToken) + private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { _components.Terminate(); - await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(), cancellationToken); - StaticApplicationLogging.Initialize(null); + await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } private void AcquireMainDom() { - using (DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) + using DisposableTimer timer = _profilingLogger.DebugDuration("Acquiring MainDom.", "Acquired."); + + try { - try - { - _mainDom.Acquire(_applicationShutdownRegistry); - } - catch - { - timer?.Fail(); - throw; - } + _mainDom.Acquire(_applicationShutdownRegistry); + } + catch + { + timer?.Fail(); + throw; } } private void DetermineRuntimeLevel() { - if (State.BootFailedException != null) + if (State.BootFailedException is not null) { - // there's already been an exception so cannot boot and no need to check + // There's already been an exception, so cannot boot and no need to check return; } @@ -277,7 +270,8 @@ namespace Umbraco.Cms.Infrastructure.Runtime State.Configure(RuntimeLevel.BootFailed, RuntimeLevelReason.BootFailedOnException); timer?.Fail(); _logger.LogError(ex, "Boot Failed"); - // We do not throw the exception. It will be rethrown by BootFailedMiddleware + + // We do not throw the exception, it will be rethrown by BootFailedMiddleware } } } From d70a207d60b6f4eff8cf1b446f0a0cf4f70d03b5 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 07:51:25 +0100 Subject: [PATCH 09/39] V8: Add ability to implement your own HtmlSanitizer (#11897) * Add IHtmlSanitizer * Use IHtmlSanitizer in RTE value editor * Add docstrings to IHtmlSanitizer * Rename NoOp to Noop To match the rest of the classes * Fix tests --- .../CompositionExtensions/Services.cs | 2 + src/Umbraco.Core/Security/IHtmlSanitizer.cs | 12 +++++ .../Security/NoopHtmlSanitizer.cs | 10 ++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../PublishedContent/PublishedContentTests.cs | 3 +- .../PropertyEditors/GridPropertyEditor.cs | 37 ++++++++++++-- .../PropertyEditors/RichTextPropertyEditor.cs | 49 ++++++++++++++++--- 7 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Core/Security/IHtmlSanitizer.cs create mode 100644 src/Umbraco.Core/Security/NoopHtmlSanitizer.cs diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs index e912f7281c..3c9dd9d701 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Packaging; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Core.Telemetry; @@ -81,6 +82,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions new DirectoryInfo(IOHelper.GetRootDirectorySafe()))); composition.RegisterUnique(); + composition.RegisterUnique(); return composition; } diff --git a/src/Umbraco.Core/Security/IHtmlSanitizer.cs b/src/Umbraco.Core/Security/IHtmlSanitizer.cs new file mode 100644 index 0000000000..fa1e0b3ee5 --- /dev/null +++ b/src/Umbraco.Core/Security/IHtmlSanitizer.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Security +{ + public interface IHtmlSanitizer + { + /// + /// Sanitizes HTML + /// + /// HTML to be sanitized + /// Sanitized HTML + string Sanitize(string html); + } +} diff --git a/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs new file mode 100644 index 0000000000..2ea34d52ea --- /dev/null +++ b/src/Umbraco.Core/Security/NoopHtmlSanitizer.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Security +{ + public class NoopHtmlSanitizer : IHtmlSanitizer + { + public string Sanitize(string html) + { + return html; + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6729930174..e27c6eeceb 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -194,6 +194,8 @@ + + diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index cafda161f4..9b8f5a05fd 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -16,6 +16,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; @@ -54,7 +55,7 @@ namespace Umbraco.Tests.PublishedContent var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages, Mock.Of())) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages, Mock.Of(), Mock.Of())) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 6f919868f7..5ed3051e07 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Web.Templates; @@ -32,8 +33,9 @@ namespace Umbraco.Web.PropertyEditors private readonly RichTextEditorPastedImages _pastedImages; private readonly HtmlLocalLinkParser _localLinkParser; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlSanitizer _htmlSanitizer; - [Obsolete("Use the constructor which takes an IImageUrlGenerator")] + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, @@ -43,12 +45,24 @@ namespace Umbraco.Web.PropertyEditors { } + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser, IImageUrlGenerator imageUrlGenerator) + : this(logger, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, imageUrlGenerator, Current.Factory.GetInstance()) + { + } + + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator, + IHtmlSanitizer htmlSanitizer) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; @@ -56,6 +70,7 @@ namespace Umbraco.Web.PropertyEditors _pastedImages = pastedImages; _localLinkParser = localLinkParser; _imageUrlGenerator = imageUrlGenerator; + _htmlSanitizer = htmlSanitizer; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -64,7 +79,7 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated /// /// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser, _imageUrlGenerator); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser, _imageUrlGenerator, _htmlSanitizer); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); @@ -77,7 +92,7 @@ namespace Umbraco.Web.PropertyEditors private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; private readonly IImageUrlGenerator _imageUrlGenerator; - [Obsolete("Use the constructor which takes an IImageUrlGenerator")] + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, @@ -87,20 +102,32 @@ namespace Umbraco.Web.PropertyEditors { } + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser, IImageUrlGenerator imageUrlGenerator) + : this(attribute, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, imageUrlGenerator, Current.Factory.GetInstance()) + { + } + + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator, + IHtmlSanitizer htmlSanitizer) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; - _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, _imageUrlGenerator); - _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); _imageUrlGenerator = imageUrlGenerator; + _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, _imageUrlGenerator, htmlSanitizer); + _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); } /// diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 2d698835b0..4a6c0c09b6 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -7,6 +7,7 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Macros; @@ -32,21 +33,46 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlSanitizer _htmlSanitizer; /// /// The constructor will setup the property editor based on the attribute if one is found /// - [Obsolete("Use the constructor which takes an IImageUrlGenerator")] - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] + public RichTextPropertyEditor( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages) : this(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Current.ImageUrlGenerator) { } + [Obsolete("Use the constructor which takes an IHtmlSanitizer")] + public RichTextPropertyEditor( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IImageUrlGenerator imageUrlGenerator) + : this(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, imageUrlGenerator, Current.Factory.GetInstance()) + { + } + /// /// The constructor will setup the property editor based on the attribute if one is found /// - public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) + public RichTextPropertyEditor( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IImageUrlGenerator imageUrlGenerator, + IHtmlSanitizer htmlSanitizer) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; @@ -54,13 +80,14 @@ namespace Umbraco.Web.PropertyEditors _localLinkParser = localLinkParser; _pastedImages = pastedImages; _imageUrlGenerator = imageUrlGenerator; + _htmlSanitizer = htmlSanitizer; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages, _imageUrlGenerator); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages, _imageUrlGenerator, _htmlSanitizer); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -76,8 +103,16 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly IImageUrlGenerator _imageUrlGenerator; + private readonly IHtmlSanitizer _htmlSanitizer; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) + public RichTextPropertyValueEditor( + DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + HtmlLocalLinkParser localLinkParser, + RichTextEditorPastedImages pastedImages, + IImageUrlGenerator imageUrlGenerator, + IHtmlSanitizer htmlSanitizer) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; @@ -85,6 +120,7 @@ namespace Umbraco.Web.PropertyEditors _localLinkParser = localLinkParser; _pastedImages = pastedImages; _imageUrlGenerator = imageUrlGenerator; + _htmlSanitizer = htmlSanitizer; } /// @@ -141,8 +177,9 @@ namespace Umbraco.Web.PropertyEditors var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); + var sanitized = _htmlSanitizer.Sanitize(parsed); - return parsed.NullOrWhiteSpaceAsNull(); + return sanitized.NullOrWhiteSpaceAsNull(); } /// From 3ade2b6de32faa062a14f1a20a487e9d23c2966d Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 08:43:43 +0100 Subject: [PATCH 10/39] Add RC to version --- build/templates/UmbracoPackage/.template.config/template.json | 2 +- build/templates/UmbracoProject/.template.config/template.json | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index 32f0c924dd..1a4dd16fd7 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0", + "defaultValue": "9.3.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index b09050a2a4..fd41de8d1c 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -57,7 +57,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0", + "defaultValue": "9.3.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 55448806ef..995c8afebd 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ 9.3.0 9.3.0 - 9.3.0 + 9.3.0-rc 9.3.0 9.0 en-US From 2ddb3e13e567a1b452ac0a4f11c37342a183514d Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 10:05:01 +0100 Subject: [PATCH 11/39] Fix breaking changes --- src/JsonSchema/AppSettings.cs | 1 + .../Configuration/Models/ContentDashboardSettings.cs | 3 ++- .../Configuration/Models/RequestHandlerSettings.cs | 10 ++++++++++ .../UmbracoBuilder.Configuration.cs | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 048513a5da..62817bdec7 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Deploy.Core.Configuration.DeployConfiguration; using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration; diff --git a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 3f8546a1ad..768f7c2088 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,6 +1,7 @@ using System.ComponentModel; +using Umbraco.Cms.Core.Configuration.Models; -namespace Umbraco.Cms.Core.Configuration.Models +namespace Umbraco.Cms.Core.Configuration { /// /// Typed configuration options for content dashboard settings. diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index 051c31dc26..c820ea191b 100644 --- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs @@ -85,5 +85,15 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Add additional character replacements, or override defaults /// public IEnumerable UserDefinedCharCollection { get; set; } + + [Obsolete("Use CharItem in the Umbraco.Cms.Core.Configuration.Models namespace instead.")] + public class CharItem : IChar + { + /// + public string Char { get; set; } + + /// + public string Replacement { get; set; } + } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index f0cbf7f95d..8baf34f9cb 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -4,6 +4,7 @@ using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.Models.Validation; using Umbraco.Extensions; From 72533d29c89f1ece658e230fd12484fb68f8474c Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 10:20:12 +0100 Subject: [PATCH 12/39] Specify namespace for CharITem --- .../Configuration/Models/RequestHandlerSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index c820ea191b..2bdcaef7f3 100644 --- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const string StaticConvertUrlsToAscii = "try"; internal const bool StaticEnableDefaultCharReplacements = true; - internal static readonly CharItem[] DefaultCharCollection = + internal static readonly Umbraco.Cms.Core.Configuration.Models.CharItem[] DefaultCharCollection = { new () { Char = " ", Replacement = "-" }, new () { Char = "\"", Replacement = string.Empty }, @@ -84,9 +84,9 @@ namespace Umbraco.Cms.Core.Configuration.Models /// /// Add additional character replacements, or override defaults /// - public IEnumerable UserDefinedCharCollection { get; set; } + public IEnumerable UserDefinedCharCollection { get; set; } - [Obsolete("Use CharItem in the Umbraco.Cms.Core.Configuration.Models namespace instead.")] + [Obsolete("Use CharItem in the Umbraco.Cms.Core.Configuration.Models namespace instead. Scheduled for removal in V10.")] public class CharItem : IChar { /// From 9dbe2d211c11fc69702dee3eb244af511cd9dc53 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 10:57:49 +0100 Subject: [PATCH 13/39] Add allowlist for HelpPage --- src/Umbraco.Core/ConfigsExtensions.cs | 3 +++ src/Umbraco.Core/Constants-AppSettings.cs | 5 ++++ src/Umbraco.Core/Help/HelpPageSettings.cs | 12 ++++++++++ src/Umbraco.Core/Help/IHelpPageSettings.cs | 10 ++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 ++ src/Umbraco.Web.UI/web.Template.config | 1 + src/Umbraco.Web/Editors/HelpController.cs | 28 ++++++++++++++++++++++ 7 files changed, 61 insertions(+) create mode 100644 src/Umbraco.Core/Help/HelpPageSettings.cs create mode 100644 src/Umbraco.Core/Help/IHelpPageSettings.cs diff --git a/src/Umbraco.Core/ConfigsExtensions.cs b/src/Umbraco.Core/ConfigsExtensions.cs index 25c69899c0..01247cc69e 100644 --- a/src/Umbraco.Core/ConfigsExtensions.cs +++ b/src/Umbraco.Core/ConfigsExtensions.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Configuration.Grid; using Umbraco.Core.Configuration.HealthChecks; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dashboards; +using Umbraco.Core.Help; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; @@ -50,6 +51,8 @@ namespace Umbraco.Core factory.GetInstance().Debug)); configs.Add(() => new ContentDashboardSettings()); + + configs.Add(() => new HelpPageSettings()); } } } diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 4e5619813e..6a58675e91 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -125,6 +125,11 @@ namespace Umbraco.Core /// public const string ContentDashboardUrlAllowlist = "Umbraco.Core.ContentDashboardUrl-Allowlist"; + /// + /// A list of allowed addresses to fetch content for the help page. + /// + public const string HelpPageUrlAllowList = "Umbraco.Core.HelpPage-Allowlist"; + /// /// TODO: FILL ME IN /// diff --git a/src/Umbraco.Core/Help/HelpPageSettings.cs b/src/Umbraco.Core/Help/HelpPageSettings.cs new file mode 100644 index 0000000000..d2a4a3a0f5 --- /dev/null +++ b/src/Umbraco.Core/Help/HelpPageSettings.cs @@ -0,0 +1,12 @@ +using System.Configuration; + +namespace Umbraco.Core.Help +{ + public class HelpPageSettings : IHelpPageSettings + { + public string HelpPageUrlAllowList => + ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.HelpPageUrlAllowList) + ? ConfigurationManager.AppSettings[Constants.AppSettings.HelpPageUrlAllowList] + : null; + } +} diff --git a/src/Umbraco.Core/Help/IHelpPageSettings.cs b/src/Umbraco.Core/Help/IHelpPageSettings.cs new file mode 100644 index 0000000000..5643e47a30 --- /dev/null +++ b/src/Umbraco.Core/Help/IHelpPageSettings.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Help +{ + public interface IHelpPageSettings + { + /// + /// Gets the allowed addresses to retrieve data for the help page. + /// + string HelpPageUrlAllowList { get; } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e27c6eeceb..35948ede91 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -137,6 +137,8 @@ + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index e4e3e19bcb..32e624e400 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -39,6 +39,7 @@ + diff --git a/src/Umbraco.Web/Editors/HelpController.cs b/src/Umbraco.Web/Editors/HelpController.cs index 39dbbc435c..77e1675f87 100644 --- a/src/Umbraco.Web/Editors/HelpController.cs +++ b/src/Umbraco.Web/Editors/HelpController.cs @@ -1,16 +1,33 @@ using Newtonsoft.Json; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Runtime.Serialization; using System.Threading.Tasks; +using System.Web.Http; +using Umbraco.Core.Help; +using Umbraco.Core.Logging; namespace Umbraco.Web.Editors { public class HelpController : UmbracoAuthorizedJsonController { + private readonly IHelpPageSettings _helpPageSettings; + + public HelpController(IHelpPageSettings helpPageSettings) + { + _helpPageSettings = helpPageSettings; + } + private static HttpClient _httpClient; public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { + if (IsAllowedUrl(baseUrl) is false) + { + Logger.Error($"The following URL is not listed in the allowlist for HelpPage in web.config: {baseUrl}"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "HelpPage source not permitted")); + } + var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); try @@ -33,6 +50,17 @@ namespace Umbraco.Web.Editors return new List(); } + + private bool IsAllowedUrl(string url) + { + if (string.IsNullOrEmpty(_helpPageSettings.HelpPageUrlAllowList) || + _helpPageSettings.HelpPageUrlAllowList.Contains(url)) + { + return true; + } + + return false; + } } [DataContract(Name = "HelpPage")] From 2f17d766be1894340637a8f2f1a7ebaf7cab2638 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 10:57:49 +0100 Subject: [PATCH 14/39] Cherry pick Add allowlist for HelpPage --- src/Umbraco.Core/Help/HelpPageSettings.cs | 12 ++++++++++ src/Umbraco.Core/Help/IHelpPageSettings.cs | 10 ++++++++ .../Controllers/HelpController.cs | 24 +++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Help/HelpPageSettings.cs create mode 100644 src/Umbraco.Core/Help/IHelpPageSettings.cs diff --git a/src/Umbraco.Core/Help/HelpPageSettings.cs b/src/Umbraco.Core/Help/HelpPageSettings.cs new file mode 100644 index 0000000000..d2a4a3a0f5 --- /dev/null +++ b/src/Umbraco.Core/Help/HelpPageSettings.cs @@ -0,0 +1,12 @@ +using System.Configuration; + +namespace Umbraco.Core.Help +{ + public class HelpPageSettings : IHelpPageSettings + { + public string HelpPageUrlAllowList => + ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.HelpPageUrlAllowList) + ? ConfigurationManager.AppSettings[Constants.AppSettings.HelpPageUrlAllowList] + : null; + } +} diff --git a/src/Umbraco.Core/Help/IHelpPageSettings.cs b/src/Umbraco.Core/Help/IHelpPageSettings.cs new file mode 100644 index 0000000000..5643e47a30 --- /dev/null +++ b/src/Umbraco.Core/Help/IHelpPageSettings.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Help +{ + public interface IHelpPageSettings + { + /// + /// Gets the allowed addresses to retrieve data for the help page. + /// + string HelpPageUrlAllowList { get; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index 3bc45703fa..ecec8f864d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; using System.Runtime.Serialization; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Core.Help; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers @@ -13,8 +14,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class HelpController : UmbracoAuthorizedJsonController { private readonly ILogger _logger; + private readonly IHelpPageSettings _helpPageSettings; - public HelpController(ILogger logger) + public HelpController(ILogger logger, + IHelpPageSettings helpPageSettings) { _logger = logger; } @@ -22,6 +25,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private static HttpClient _httpClient; public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { + if (IsAllowedUrl(baseUrl) is false) + { + Logger.Error($"The following URL is not listed in the allowlist for HelpPage in web.config: {baseUrl}"); + throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "HelpPage source not permitted")); + } + var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); try @@ -44,6 +53,17 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new List(); } + + private bool IsAllowedUrl(string url) + { + if (string.IsNullOrEmpty(_helpPageSettings.HelpPageUrlAllowList) || + _helpPageSettings.HelpPageUrlAllowList.Contains(url)) + { + return true; + } + + return false; + } } [DataContract(Name = "HelpPage")] From 3261a6f71dd519ab0c17b9df6a1a773e044f2a87 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 26 Jan 2022 12:12:59 +0100 Subject: [PATCH 15/39] Fix up for V9 --- src/JsonSchema/AppSettings.cs | 2 ++ .../Configuration/Models/HelpPageSettings.cs | 11 ++++++ src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 3 +- src/Umbraco.Core/Help/HelpPageSettings.cs | 12 ------- src/Umbraco.Core/Help/IHelpPageSettings.cs | 10 ------ .../Controllers/HelpController.cs | 34 +++++++++++++++---- 7 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs delete mode 100644 src/Umbraco.Core/Help/HelpPageSettings.cs delete mode 100644 src/Umbraco.Core/Help/IHelpPageSettings.cs diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 62817bdec7..73c5ea18f5 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -89,6 +89,8 @@ namespace JsonSchema public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } public ContentDashboardSettings ContentDashboard { get; set; } + + public HelpPageSettings HelpPage { get; set; } } /// diff --git a/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs new file mode 100644 index 0000000000..3bd518b37e --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/HelpPageSettings.cs @@ -0,0 +1,11 @@ +namespace Umbraco.Cms.Core.Configuration.Models +{ + [UmbracoOptions(Constants.Configuration.ConfigHelpPage)] + public class HelpPageSettings + { + /// + /// Gets or sets the allowed addresses to retrieve data for the content dashboard. + /// + public string[] HelpPageUrlAllowList { get; set; } + } +} diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index ab951618e3..bdbd13b2a4 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -55,6 +55,7 @@ namespace Umbraco.Cms.Core public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; + public const string ConfigHelpPage = ConfigPrefix + "HelpPage"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 8baf34f9cb..91e6f71415 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -87,7 +87,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); builder.Services.Configure(options => options.MergeReplacements(builder.Config)); diff --git a/src/Umbraco.Core/Help/HelpPageSettings.cs b/src/Umbraco.Core/Help/HelpPageSettings.cs deleted file mode 100644 index d2a4a3a0f5..0000000000 --- a/src/Umbraco.Core/Help/HelpPageSettings.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Configuration; - -namespace Umbraco.Core.Help -{ - public class HelpPageSettings : IHelpPageSettings - { - public string HelpPageUrlAllowList => - ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.HelpPageUrlAllowList) - ? ConfigurationManager.AppSettings[Constants.AppSettings.HelpPageUrlAllowList] - : null; - } -} diff --git a/src/Umbraco.Core/Help/IHelpPageSettings.cs b/src/Umbraco.Core/Help/IHelpPageSettings.cs deleted file mode 100644 index 5643e47a30..0000000000 --- a/src/Umbraco.Core/Help/IHelpPageSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Umbraco.Core.Help -{ - public interface IHelpPageSettings - { - /// - /// Gets the allowed addresses to retrieve data for the help page. - /// - string HelpPageUrlAllowList { get; } - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index ecec8f864d..dd01f9621f 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -1,11 +1,17 @@ +using System; using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Net.Http; using System.Runtime.Serialization; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Newtonsoft.Json; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Web.Common.Attributes; -using Umbraco.Core.Help; +using Umbraco.Cms.Web.Common.DependencyInjection; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Controllers @@ -14,21 +20,35 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class HelpController : UmbracoAuthorizedJsonController { private readonly ILogger _logger; - private readonly IHelpPageSettings _helpPageSettings; + private readonly HelpPageSettings _helpPageSettings; - public HelpController(ILogger logger, - IHelpPageSettings helpPageSettings) + [Obsolete("Use constructor that takes IOptions")] + public HelpController(ILogger logger) + : this(logger, StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + [ActivatorUtilitiesConstructor] + public HelpController( + ILogger logger, + IOptions helpPageSettings) { _logger = logger; + _helpPageSettings = helpPageSettings.Value; } private static HttpClient _httpClient; + public async Task> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com") { if (IsAllowedUrl(baseUrl) is false) { - Logger.Error($"The following URL is not listed in the allowlist for HelpPage in web.config: {baseUrl}"); - throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "HelpPage source not permitted")); + _logger.LogError($"The following URL is not listed in the allowlist for HelpPage in web.config: {baseUrl}"); + HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + // Ideally we'd want to return a BadRequestResult here, + // however, since we're not returning ActionResult this is not possible and changing it would be a breaking change. + return new List(); } var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree); @@ -56,7 +76,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private bool IsAllowedUrl(string url) { - if (string.IsNullOrEmpty(_helpPageSettings.HelpPageUrlAllowList) || + if (_helpPageSettings.HelpPageUrlAllowList is null || _helpPageSettings.HelpPageUrlAllowList.Contains(url)) { return true; From 4d4aff4c67893ba9ae360cc0a88d3655bc319d6c Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 26 Jan 2022 12:22:05 +0100 Subject: [PATCH 16/39] Apply changes from #11805 and #11806 to v9 (#11904) * Apply changes from #11805 and #11806 to v9 * Update documentation and cleanup code styling --- .../Models/PropertyTagsExtensions.cs | 8 +- .../PropertyEditors/DataValueEditor.cs | 157 ++++++++++-------- .../PropertyEditors/GridPropertyEditor.cs | 9 +- .../ImageCropperPropertyEditor.cs | 7 +- .../ImageCropperPropertyValueEditor.cs | 36 ++-- .../MediaPicker3PropertyEditor.cs | 7 +- .../MultiUrlPickerValueEditor.cs | 6 +- .../MultipleTextStringPropertyEditor.cs | 4 +- .../PropertyEditors/MultipleValueEditor.cs | 3 +- .../NestedContentPropertyEditor.cs | 6 +- .../PropertyEditors/RichTextPropertyEditor.cs | 4 +- .../PropertyEditors/TagsPropertyEditor.cs | 2 +- .../ValueConverters/ImageCropperValue.cs | 34 ++-- .../Serialization/JsonNetSerializer.cs | 21 +-- .../Serialization/JsonToStringConverter.cs | 3 +- .../BlockEditorComponentTests.cs | 2 +- 16 files changed, 159 insertions(+), 150 deletions(-) diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 7168f99078..a9da454986 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; @@ -75,8 +75,7 @@ namespace Umbraco.Extensions var updatedTags = currentTags.Union(trimmedTags).ToArray(); var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); property.SetValue(updatedValue, culture); // json array - break; - property.SetValue(serializer.Serialize(currentTags.Union(trimmedTags).ToArray()), culture); // json array + break; } } else @@ -88,7 +87,8 @@ namespace Umbraco.Extensions break; case TagsStorageType.Json: - property.SetValue(serializer.Serialize(trimmedTags), culture); // json array + var updatedValue = trimmedTags.Length == 0 ? null : serializer.Serialize(trimmedTags); + property.SetValue(updatedValue, culture); // json array break; } } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 6d3e40067e..6f9e1b6611 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; @@ -149,84 +149,90 @@ namespace Umbraco.Cms.Core.PropertyEditors public virtual bool IsReadOnly => false; /// - /// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor + /// Used to try to convert the string value to the correct CLR type based on the specified for this value editor. /// - /// - /// + /// The value. + /// + /// The result of the conversion attempt. + /// + /// ValueType was out of range. internal Attempt TryConvertValueToCrlType(object value) { - // if (value is JValue) - // value = value.ToString(); - - //this is a custom check to avoid any errors, if it's a string and it's empty just make it null + // Ensure empty string values are converted to null if (value is string s && string.IsNullOrWhiteSpace(s)) + { value = null; + } + // Ensure JSON is serialized properly (without indentation or converted to null when empty) + if (value is not null && ValueType.InvariantEquals(ValueTypes.Json)) + { + var jsonValue = _jsonSerializer.Serialize(value); + + if (jsonValue.DetectIsEmptyJson()) + { + value = null; + } + else + { + value = jsonValue; + } + } + + // Convert the string to a known type Type valueType; - //convert the string to a known type switch (ValueTypes.ToStorageType(ValueType)) { case ValueStorageType.Ntext: case ValueStorageType.Nvarchar: valueType = typeof(string); break; - case ValueStorageType.Integer: - //ensure these are nullable so we can return a null if required - //NOTE: This is allowing type of 'long' because I think json.net will deserialize a numerical value as long - // instead of int. Even though our db will not support this (will get truncated), we'll at least parse to this. + case ValueStorageType.Integer: + // Ensure these are nullable so we can return a null if required + // NOTE: This is allowing type of 'long' because I think JSON.NEt will deserialize a numerical value as long instead of int + // Even though our DB will not support this (will get truncated), we'll at least parse to this valueType = typeof(long?); - //if parsing is successful, we need to return as an Int, we're only dealing with long's here because of json.net, we actually - //don't support long values and if we return a long value it will get set as a 'long' on the Property.Value (object) and then - //when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. + // If parsing is successful, we need to return as an int, we're only dealing with long's here because of JSON.NET, + // we actually don't support long values and if we return a long value, it will get set as a 'long' on the Property.Value (object) and then + // when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match. var result = value.TryConvertTo(valueType); + return result.Success && result.Result != null ? Attempt.Succeed((int)(long)result.Result) : result; case ValueStorageType.Decimal: - //ensure these are nullable so we can return a null if required + // Ensure these are nullable so we can return a null if required valueType = typeof(decimal?); break; case ValueStorageType.Date: - //ensure these are nullable so we can return a null if required + // Ensure these are nullable so we can return a null if required valueType = typeof(DateTime?); break; + default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("ValueType was out of range."); } return value.TryConvertTo(valueType); } - /// - /// A method to deserialize the string value that has been saved in the content editor - /// to an object to be stored in the database. - /// - /// - /// - /// The current value that has been persisted to the database for this editor. This value may be useful for - /// how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. - /// - /// - /// - /// - /// - /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. - /// - /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the - /// value to the DB will fail when it tries to validate the value type. - /// + /// + /// A method to deserialize the string value that has been saved in the content editor to an object to be stored in the database. + /// + /// The value returned by the editor. + /// The current value that has been persisted to the database for this editor. This value may be useful for how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used. + /// The value that gets persisted to the database. + /// + /// By default this will attempt to automatically convert the string value to the value type supplied by ValueType. + /// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the + /// value to the DB will fail when it tries to validate the value type. + /// public virtual object FromEditor(ContentPropertyData editorValue, object currentValue) { - //if it's json but it's empty json, then return null - if (ValueType.InvariantEquals(ValueTypes.Json) && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson()) - { - return null; - } - var result = TryConvertValueToCrlType(editorValue.Value); if (result.Success == false) { @@ -238,64 +244,71 @@ namespace Umbraco.Cms.Core.PropertyEditors } /// - /// A method used to format the database value to a value that can be used by the editor + /// A method used to format the database value to a value that can be used by the editor. /// - /// - /// - /// - /// + /// The property. + /// The culture. + /// The segment. /// + /// ValueType was out of range. /// - /// The object returned will automatically be serialized into json notation. For most property editors - /// the value returned is probably just a string but in some cases a json structure will be returned. + /// The object returned will automatically be serialized into JSON notation. For most property editors + /// the value returned is probably just a string, but in some cases a JSON structure will be returned. /// public virtual object ToEditor(IProperty property, string culture = null, string segment = null) { - var val = property.GetValue(culture, segment); - if (val == null) return string.Empty; + var value = property.GetValue(culture, segment); + if (value == null) + { + return string.Empty; + } switch (ValueTypes.ToStorageType(ValueType)) { case ValueStorageType.Ntext: case ValueStorageType.Nvarchar: - //if it is a string type, we will attempt to see if it is json stored data, if it is we'll try to convert - //to a real json object so we can pass the true json object directly to angular! - var asString = val.ToString(); - if (asString.DetectIsJson()) + // If it is a string type, we will attempt to see if it is JSON stored data, if it is we'll try to convert + // to a real JSON object so we can pass the true JSON object directly to Angular! + var stringValue = value as string ?? value.ToString(); + if (stringValue.DetectIsJson()) { try { - var json = _jsonSerializer.Deserialize(asString); - return json; + return _jsonSerializer.Deserialize(stringValue); } catch { - //swallow this exception, we thought it was json but it really isn't so continue returning a string + // Swallow this exception, we thought it was JSON but it really isn't so continue returning a string } } - return asString; + + return stringValue; + case ValueStorageType.Integer: case ValueStorageType.Decimal: - //Decimals need to be formatted with invariant culture (dots, not commas) - //Anything else falls back to ToString() - var decim = val.TryConvertTo(); - return decim.Success - ? decim.Result.ToString(NumberFormatInfo.InvariantInfo) - : val.ToString(); + // Decimals need to be formatted with invariant culture (dots, not commas) + // Anything else falls back to ToString() + var decimalValue = value.TryConvertTo(); + + return decimalValue.Success + ? decimalValue.Result.ToString(NumberFormatInfo.InvariantInfo) + : value.ToString(); + case ValueStorageType.Date: - var date = val.TryConvertTo(); - if (date.Success == false || date.Result == null) + var dateValue = value.TryConvertTo(); + if (dateValue.Success == false || dateValue.Result == null) { return string.Empty; } - //Dates will be formatted as yyyy-MM-dd HH:mm:ss - return date.Result.Value.ToIsoString(); + + // Dates will be formatted as yyyy-MM-dd HH:mm:ss + return dateValue.Result.Value.ToIsoString(); + default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException("ValueType was out of range."); } } - // TODO: the methods below should be replaced by proper property value convert ToXPath usage! /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index 52a1e50fc4..f149757919 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -1,12 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; @@ -153,7 +151,8 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object ToEditor(IProperty property, string culture = null, string segment = null) { var val = property.GetValue(culture, segment)?.ToString(); - if (val.IsNullOrWhiteSpace()) return string.Empty; + if (val.IsNullOrWhiteSpace()) + return string.Empty; var grid = DeserializeGridValue(val, out var rtes, out _); @@ -199,7 +198,7 @@ namespace Umbraco.Cms.Core.PropertyEditors _richTextPropertyValueEditor.GetReferences(x.Value))) yield return umbracoEntityReference; - foreach (var umbracoEntityReference in mediaValues.Where(x=>x.Value.HasValues) + foreach (var umbracoEntityReference in mediaValues.Where(x => x.Value.HasValues) .SelectMany(x => _mediaPickerPropertyValueEditor.GetReferences(x.Value["udi"]))) yield return umbracoEntityReference; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index 8a38a85551..ed194038d9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -208,6 +208,7 @@ namespace Umbraco.Cms.Core.PropertyEditors { continue; } + var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(src); var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath); jo["src"] = _mediaFileManager.FileSystem.GetUrl(copyPath); @@ -273,10 +274,8 @@ namespace Umbraco.Cms.Core.PropertyEditors // the property value will be the file source eg '/media/23454/hello.jpg' and we // are fixing that anomaly here - does not make any sense at all but... bah... src = svalue; - property.SetValue(JsonConvert.SerializeObject(new - { - src = svalue - }, Formatting.None), pvalue.Culture, pvalue.Segment); + + property.SetValue(JsonConvert.SerializeObject(new { src = svalue }, Formatting.None), pvalue.Culture, pvalue.Segment); } else { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index 5ae10bc178..d24a46f815 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -86,31 +86,42 @@ namespace Umbraco.Cms.Core.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - // get the current path + // Get the current path var currentPath = string.Empty; try { var svalue = currentValue as string; var currentJson = string.IsNullOrWhiteSpace(svalue) ? null : JObject.Parse(svalue); - if (currentJson != null && currentJson["src"] != null) - currentPath = currentJson["src"].Value(); + if (currentJson != null && currentJson.TryGetValue("src", out var src)) + { + currentPath = src.Value(); + } } catch (Exception ex) { - // for some reason the value is invalid so continue as if there was no value there + // For some reason the value is invalid so continue as if there was no value there _logger.LogWarning(ex, "Could not parse current db value to a JObject."); } + if (string.IsNullOrWhiteSpace(currentPath) == false) currentPath = _mediaFileManager.FileSystem.GetRelativePath(currentPath); - // get the new json and path - JObject editorJson = null; + // Get the new JSON and file path var editorFile = string.Empty; - if (editorValue.Value != null) + if (editorValue.Value is JObject editorJson) { - editorJson = editorValue.Value as JObject; - if (editorJson != null && editorJson["src"] != null) + // Populate current file + if (editorJson["src"] != null) + { editorFile = editorJson["src"].Value(); + } + + // Clean up redundant/default data + ImageCropperValue.Prune(editorJson); + } + else + { + editorJson = null; } // ensure we have the required guids @@ -138,7 +149,7 @@ namespace Umbraco.Cms.Core.PropertyEditors return null; // clear } - return editorJson?.ToString(); // unchanged + return editorJson?.ToString(Formatting.None); // unchanged } // process the file @@ -159,7 +170,7 @@ namespace Umbraco.Cms.Core.PropertyEditors // update json and return if (editorJson == null) return null; editorJson["src"] = filepath == null ? string.Empty : _mediaFileManager.FileSystem.GetUrl(filepath); - return editorJson.ToString(); + return editorJson.ToString(Formatting.None); } private string ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid) @@ -186,7 +197,6 @@ namespace Umbraco.Cms.Core.PropertyEditors return filepath; } - public override string ConvertDbToString(IPropertyType propertyType, object value) { if (value == null || string.IsNullOrEmpty(value.ToString())) @@ -205,7 +215,7 @@ namespace Umbraco.Cms.Core.PropertyEditors { src = val, crops = crops - },new JsonSerializerSettings() + }, new JsonSerializerSettings() { Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index 2cfe5dd56e..25174d5599 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; @@ -157,7 +157,6 @@ namespace Umbraco.Cms.Core.PropertyEditors } } - /// /// Model/DTO that represents the JSON that the MediaPicker3 stores. /// @@ -176,7 +175,6 @@ namespace Umbraco.Cms.Core.PropertyEditors [DataMember(Name = "focalPoint")] public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } - /// /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. /// @@ -214,9 +212,6 @@ namespace Umbraco.Cms.Core.PropertyEditors /// Removes redundant crop data/default focal point. /// /// The media with crops DTO. - /// - /// The cleaned up value. - /// /// /// Because the DTO uses the same JSON keys as the image cropper value for crops and focal point, we can re-use the prune method. /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs index f6d8a598b0..db55792a31 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Cms.Core.PropertyEditors try { - var links = JsonConvert.DeserializeObject>(value); + var links = JsonConvert.DeserializeObject>(value); var documentLinks = links.FindAll(link => link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Document); var mediaLinks = links.FindAll(link => link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Media); @@ -158,11 +158,13 @@ namespace Umbraco.Cms.Core.PropertyEditors { var links = JsonConvert.DeserializeObject>(value); if (links.Count == 0) + { return null; + } return JsonConvert.SerializeObject( from link in links - select new MultiUrlPickerValueEditor.LinkDto + select new LinkDto { Name = link.Name, QueryString = link.QueryString, diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs index 97cb677d4c..f0d5907e8e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -1,14 +1,12 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Exceptions; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Editors; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs index 47f8c9a169..8177c9ffeb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -65,7 +65,6 @@ namespace Umbraco.Cms.Core.PropertyEditors } var values = json.Select(item => item.Value()).ToArray(); - if (values.Length == 0) { return null; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 835431820c..a3d30d0578 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -105,7 +105,9 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(propertyValue); if (rows.Count == 0) + { return null; + } foreach (var row in rows.ToList()) { @@ -141,8 +143,6 @@ namespace Umbraco.Cms.Core.PropertyEditors #endregion - - #region Convert database // editor // note: there is NO variant support here diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 8eeb935c12..1cfbc3449e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -148,7 +148,9 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { if (editorValue.Value == null) + { return null; + } var userId = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser?.Id ?? Constants.Security.SuperUserId; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 42f6424bfa..30911b0866 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs index 97f1b8398c..af9e820d66 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -21,14 +21,14 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// [JsonConverter(typeof(NoTypeConverterJsonConverter))] [TypeConverter(typeof(ImageCropperValueTypeConverter))] - [DataContract(Name="imageCropDataSet")] + [DataContract(Name = "imageCropDataSet")] public class ImageCropperValue : IHtmlEncodedString, IEquatable { /// /// Gets or sets the value source image. /// - [DataMember(Name="src")] - public string Src { get; set;} + [DataMember(Name = "src")] + public string Src { get; set; } /// /// Gets or sets the value focal point. @@ -44,9 +44,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// public override string ToString() - { - return Crops != null ? (Crops.Any() ? JsonConvert.SerializeObject(this) : Src) : string.Empty; - } + => HasCrops() || HasFocalPoint() ? JsonConvert.SerializeObject(this, Formatting.None) : Src; /// public string ToHtmlString() => Src; @@ -178,12 +176,10 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// Removes redundant crop data/default focal point. /// /// The image cropper value. - /// - /// The cleaned up value. - /// public static void Prune(JObject value) { - if (value is null) throw new ArgumentNullException(nameof(value)); + if (value is null) + throw new ArgumentNullException(nameof(value)); if (value.TryGetValue("crops", out var crops)) { @@ -252,8 +248,8 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = Src?.GetHashCode() ?? 0; - hashCode = (hashCode*397) ^ (FocalPoint?.GetHashCode() ?? 0); - hashCode = (hashCode*397) ^ (Crops?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (FocalPoint?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ (Crops?.GetHashCode() ?? 0); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } @@ -298,7 +294,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode - return (Left.GetHashCode()*397) ^ Top.GetHashCode(); + return (Left.GetHashCode() * 397) ^ Top.GetHashCode(); // ReSharper restore NonReadonlyMemberInGetHashCode } } @@ -352,9 +348,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = Alias?.GetHashCode() ?? 0; - hashCode = (hashCode*397) ^ Width; - hashCode = (hashCode*397) ^ Height; - hashCode = (hashCode*397) ^ (Coordinates?.GetHashCode() ?? 0); + hashCode = (hashCode * 397) ^ Width; + hashCode = (hashCode * 397) ^ Height; + hashCode = (hashCode * 397) ^ (Coordinates?.GetHashCode() ?? 0); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } @@ -409,9 +405,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters // properties are, practically, readonly // ReSharper disable NonReadonlyMemberInGetHashCode var hashCode = X1.GetHashCode(); - hashCode = (hashCode*397) ^ Y1.GetHashCode(); - hashCode = (hashCode*397) ^ X2.GetHashCode(); - hashCode = (hashCode*397) ^ Y2.GetHashCode(); + hashCode = (hashCode * 397) ^ Y1.GetHashCode(); + hashCode = (hashCode * 397) ^ X2.GetHashCode(); + hashCode = (hashCode * 397) ^ Y2.GetHashCode(); return hashCode; // ReSharper restore NonReadonlyMemberInGetHashCode } diff --git a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs index 5c5377c0a1..dd228ac008 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -15,25 +15,20 @@ namespace Umbraco.Cms.Infrastructure.Serialization { new StringEnumConverter() }, - Formatting = Formatting.None + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore }; - public string Serialize(object input) - { - return JsonConvert.SerializeObject(input, JsonSerializerSettings); - } - public T Deserialize(string input) - { - return JsonConvert.DeserializeObject(input, JsonSerializerSettings); - } + public string Serialize(object input) => JsonConvert.SerializeObject(input, JsonSerializerSettings); + + public T Deserialize(string input) => JsonConvert.DeserializeObject(input, JsonSerializerSettings); public T DeserializeSubset(string input, string key) { if (key == null) throw new ArgumentNullException(nameof(key)); - var root = JsonConvert.DeserializeObject(input); - - var jToken = root.SelectToken(key); + var root = Deserialize(input); + var jToken = root?.SelectToken(key); return jToken switch { diff --git a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs index 2e7416b2d2..3cf23154c8 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.Serialization { return reader.Value; } + // Load JObject from stream JObject jObject = JObject.Load(reader); return jObject.ToString(Formatting.None); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs index b76719888f..efd753296a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings { Formatting = Formatting.None, - NullValueHandling = NullValueHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore }; private const string ContentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; From d4c43b69f77f5a7fd2a3ab45f61c85c7cfe0b5c0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 26 Jan 2022 12:40:14 +0100 Subject: [PATCH 17/39] Updated to use IOptionsMonitor --- .../Controllers/HelpController.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index dd01f9621f..d79001d0f8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -20,21 +20,28 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers public class HelpController : UmbracoAuthorizedJsonController { private readonly ILogger _logger; - private readonly HelpPageSettings _helpPageSettings; + private HelpPageSettings _helpPageSettings; [Obsolete("Use constructor that takes IOptions")] public HelpController(ILogger logger) - : this(logger, StaticServiceProvider.Instance.GetRequiredService>()) + : this(logger, StaticServiceProvider.Instance.GetRequiredService>()) { } [ActivatorUtilitiesConstructor] public HelpController( ILogger logger, - IOptions helpPageSettings) + IOptionsMonitor helpPageSettings) { _logger = logger; - _helpPageSettings = helpPageSettings.Value; + + ResetHelpPageSettings(helpPageSettings.CurrentValue); + helpPageSettings.OnChange(ResetHelpPageSettings); + } + + private void ResetHelpPageSettings(HelpPageSettings settings) + { + _helpPageSettings = settings; } private static HttpClient _httpClient; From c73d0bf6a6b97ecc95f3d1e85db635ee0353deb5 Mon Sep 17 00:00:00 2001 From: Mole Date: Thu, 27 Jan 2022 09:53:16 +0100 Subject: [PATCH 18/39] Update logging message in HelpController --- src/Umbraco.Web.BackOffice/Controllers/HelpController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs index d79001d0f8..f431b1a827 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/HelpController.cs @@ -50,7 +50,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { if (IsAllowedUrl(baseUrl) is false) { - _logger.LogError($"The following URL is not listed in the allowlist for HelpPage in web.config: {baseUrl}"); + _logger.LogError($"The following URL is not listed in the allowlist for HelpPage in HelpPageSettings: {baseUrl}"); HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; // Ideally we'd want to return a BadRequestResult here, From 7971f36b78e333aa61cb94e9fc3003b70459c6ff Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 27 Jan 2022 10:32:56 +0100 Subject: [PATCH 19/39] Add check for PluginControllerAttribute and compare area name (#11911) * Add check for PluginControllerAttribute and compare area name * Added null check --- .../Runtime/RuntimeState.cs | 43 ++++++++++--------- .../Services/ConflictingRouteService.cs | 18 ++++++-- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 73b6692e3a..c81041849a 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -44,27 +44,6 @@ namespace Umbraco.Cms.Infrastructure.Runtime { } - /// - /// Initializes a new instance of the class. - /// - 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, @@ -83,6 +62,28 @@ namespace Umbraco.Cms.Infrastructure.Runtime _conflictingRouteService = conflictingRouteService; } + /// + /// 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; diff --git a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs index 2951ace9e1..af8d0d877e 100644 --- a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs +++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs @@ -1,7 +1,9 @@ using System; using System.Linq; +using System.Reflection; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Controllers; namespace Umbraco.Cms.Web.BackOffice.Services @@ -21,10 +23,20 @@ namespace Umbraco.Cms.Web.BackOffice.Services var controllers = _typeLoader.GetTypes().ToList(); foreach (Type controller in controllers) { - if (controllers.Count(x => x.Name == controller.Name) > 1) + var potentialConflicting = controllers.Where(x => x.Name == controller.Name).ToArray(); + if (potentialConflicting.Length > 1) { - controllerName = controller.Name; - return true; + //If we have any with same controller name and located in the same area, then it is a confict. + var conflicting = potentialConflicting + .Select(x => x.GetCustomAttribute()) + .GroupBy(x => x?.AreaName) + .Any(x => x?.Count() > 1); + + if (conflicting) + { + controllerName = controller.Name; + return true; + } } } From 1b98f8985ec45c20ed23ab7d4bd7c71d5cba0dfd Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 21 Dec 2021 07:23:08 +0100 Subject: [PATCH 20/39] Use current request for emails (#11775) * Use current request for emails * Fix tests --- src/Umbraco.Core/Sync/ApplicationUrlHelper.cs | 17 ++++++++++ .../AuthenticationControllerTests.cs | 4 ++- .../Web/Controllers/UsersControllerTests.cs | 13 ++++--- .../Editors/AuthenticationController.cs | 19 +++++++++-- src/Umbraco.Web/Editors/UsersController.cs | 34 +++++++++++++------ 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 52af734f1c..d934e24575 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -102,5 +102,22 @@ namespace Umbraco.Core.Sync return url.TrimEnd(Constants.CharArrays.ForwardSlash); } + + /// + /// Will get the application URL from configuration, if none is specified will fall back to URL from request. + /// + /// + /// + /// + /// + public static Uri GetApplicationUriUncached( + HttpRequestBase request, + IUmbracoSettingsSection umbracoSettingsSection) + { + var settingUrl = umbracoSettingsSection.WebRouting.UmbracoApplicationUrl; + return string.IsNullOrEmpty(settingUrl) + ? new Uri(request.Url, IOHelper.ResolveUrl(SystemDirectories.Umbraco)) + : new Uri(settingUrl); + } } } diff --git a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs index 3d264663b5..9bd9ee73ed 100644 --- a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs @@ -16,6 +16,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; @@ -82,7 +83,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - helper); + helper, + Factory.GetInstance()); return usersController; } diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index 85dd303432..b9289c1392 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -14,6 +14,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; @@ -84,7 +85,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - helper); + helper, + Factory.GetInstance()); return usersController; } @@ -148,7 +150,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - helper); + helper, + Factory.GetInstance()); return usersController; } @@ -183,7 +186,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - helper); + helper, + Factory.GetInstance()); return usersController; } @@ -253,7 +257,8 @@ namespace Umbraco.Tests.Web.Controllers Factory.GetInstance(), Factory.GetInstance(), Factory.GetInstance(), - helper); + helper, + Factory.GetInstance()); return usersController; } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 3ecc6b64a4..54612377e0 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -27,6 +27,8 @@ using Umbraco.Web.Composing; using IUser = Umbraco.Core.Models.Membership.IUser; using Umbraco.Web.Editors.Filters; using Microsoft.Owin.Security; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Sync; namespace Umbraco.Web.Editors { @@ -40,12 +42,23 @@ namespace Umbraco.Web.Editors [DisableBrowserCache] public class AuthenticationController : UmbracoApiController { + private readonly IUmbracoSettingsSection _umbracoSettingsSection; private BackOfficeUserManager _userManager; private BackOfficeSignInManager _signInManager; - public AuthenticationController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) + public AuthenticationController( + IGlobalSettings globalSettings, + IUmbracoContextAccessor umbracoContextAccessor, + ISqlContext sqlContext, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger logger, + IRuntimeState runtimeState, + UmbracoHelper umbracoHelper, + IUmbracoSettingsSection umbracoSettingsSection) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, Current.Mapper) { + _umbracoSettingsSection = umbracoSettingsSection; } protected BackOfficeUserManager UserManager => _userManager @@ -552,8 +565,8 @@ namespace Umbraco.Web.Editors r = code }); - // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = Current.RuntimeState.ApplicationUrl; + // Construct full URL using configured application URL (which will fall back to current request) + var applicationUri = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index dda0dfc933..4bfd72854f 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -17,6 +17,7 @@ using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -27,6 +28,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Security; using Umbraco.Core.Services; +using Umbraco.Core.Sync; using Umbraco.Web.Editors.Filters; using Umbraco.Web.Models; using Umbraco.Web.Models.ContentEditing; @@ -46,9 +48,21 @@ namespace Umbraco.Web.Editors [IsCurrentUserModelFilter] public class UsersController : UmbracoAuthorizedJsonController { - public UsersController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) + private readonly IUmbracoSettingsSection _umbracoSettingsSection; + + public UsersController( + IGlobalSettings globalSettings, + IUmbracoContextAccessor umbracoContextAccessor, + ISqlContext sqlContext, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger logger, + IRuntimeState runtimeState, + UmbracoHelper umbracoHelper, + IUmbracoSettingsSection umbracoSettingsSection) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { + _umbracoSettingsSection = umbracoSettingsSection; } /// @@ -390,7 +404,7 @@ namespace Umbraco.Web.Editors user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); var userMgr = TryGetOwinContext().Result.GetBackOfficeUserManager(); - + if (!EmailSender.CanSendRequiredEmail && !userMgr.HasSendingUserInviteEventHandler) { throw new HttpResponseException( @@ -462,12 +476,12 @@ namespace Umbraco.Web.Editors Email = userSave.Email, Username = userSave.Username }; - } + } } else { //send the email - await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); + await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); } display.AddSuccessNotification(Services.TextService.Localize("speechBubbles", "resendInviteHeader"), Services.TextService.Localize("speechBubbles", "resendInviteSuccess", new[] { user.Name })); @@ -525,9 +539,9 @@ namespace Umbraco.Web.Editors invite = inviteToken }); - // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = RuntimeState.ApplicationUrl; - var inviteUri = new Uri(applicationUri, action); + // Construct full URL will use the value in settings if specified, otherwise will use the current request URL + var requestUrl = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection); + var inviteUri = new Uri(requestUrl, action); var emailSubject = Services.TextService.Localize("user", "inviteEmailCopySubject", //Ensure the culture of the found user is used for the email! @@ -622,7 +636,7 @@ namespace Umbraco.Web.Editors if (Current.Configs.Settings().Security.UsernameIsEmail && found.Username == found.Email && userSave.Username != userSave.Email) { userSave.Username = userSave.Email; - } + } if (hasErrors) throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState)); @@ -647,13 +661,13 @@ namespace Umbraco.Web.Editors } /// - /// + /// /// /// /// public async Task> PostChangePassword(ChangingPasswordModel changingPasswordModel) { - changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel)); + changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel)); if (ModelState.IsValid == false) { From a779803763d45c079579f4216d4f516d6e8e9648 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 27 Jan 2022 13:18:21 +0100 Subject: [PATCH 21/39] Bump version to 8.17.2 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a7cfdaa562..a8a04a0679 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.17.1")] -[assembly: AssemblyInformationalVersion("8.17.1")] +[assembly: AssemblyFileVersion("8.17.2")] +[assembly: AssemblyInformationalVersion("8.17.2")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index afe9ade5f7..6114a84b2a 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,9 +348,9 @@ False True - 8171 + 8172 / - http://localhost:8171 + http://localhost:8172 False False @@ -433,4 +433,4 @@ - \ No newline at end of file + From c79380191bfa206d081fcd08ca9008be39a79d6c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 27 Jan 2022 15:57:22 +0100 Subject: [PATCH 22/39] Use Umbraco Path instead of constant --- src/Umbraco.Core/Sync/ApplicationUrlHelper.cs | 21 +++++++++++++++---- .../Editors/AuthenticationController.cs | 2 +- src/Umbraco.Web/Editors/UsersController.cs | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index d934e24575..f3cc5a6db6 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -112,12 +112,25 @@ namespace Umbraco.Core.Sync /// public static Uri GetApplicationUriUncached( HttpRequestBase request, - IUmbracoSettingsSection umbracoSettingsSection) + IUmbracoSettingsSection umbracoSettingsSection, + IGlobalSettings globalSettings) { var settingUrl = umbracoSettingsSection.WebRouting.UmbracoApplicationUrl; - return string.IsNullOrEmpty(settingUrl) - ? new Uri(request.Url, IOHelper.ResolveUrl(SystemDirectories.Umbraco)) - : new Uri(settingUrl); + + + if (string.IsNullOrEmpty(settingUrl)) + { + if (!Uri.TryCreate(request.Url, VirtualPathUtility.ToAbsolute(globalSettings.Path), out var result)) + { + throw new InvalidOperationException( + $"Could not create an url from {request.Url} and {globalSettings.Path}"); + } + return result; + } + else + { + return new Uri(settingUrl); + } } } } diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 54612377e0..85889c5869 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -566,7 +566,7 @@ namespace Umbraco.Web.Editors }); // Construct full URL using configured application URL (which will fall back to current request) - var applicationUri = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection); + var applicationUri = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection, GlobalSettings); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); } diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index 4bfd72854f..58bb5dcb0a 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -540,7 +540,7 @@ namespace Umbraco.Web.Editors }); // Construct full URL will use the value in settings if specified, otherwise will use the current request URL - var requestUrl = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection); + var requestUrl = ApplicationUrlHelper.GetApplicationUriUncached(http.Request, _umbracoSettingsSection, GlobalSettings); var inviteUri = new Uri(requestUrl, action); var emailSubject = Services.TextService.Localize("user", "inviteEmailCopySubject", From a71529a0583fc357616494d4ce36a1961391e7e6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 27 Jan 2022 17:37:32 +0100 Subject: [PATCH 23/39] V9/feature/merge v8 27012022 (#11915) * Add allowlist for HelpPage * Use current request for emails (#11775) * Use current request for emails * Fix tests * Bump version to 8.17.2 * Use Umbraco Path instead of constant Co-authored-by: Mole --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 69f812b6e6..e1067f31e3 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -91,8 +91,8 @@ - - + + From c0dfb3391656589a8c85364bac97f1adfc35921a Mon Sep 17 00:00:00 2001 From: Mole Date: Fri, 28 Jan 2022 12:30:44 +0100 Subject: [PATCH 24/39] Fix importing DocType if parent folder already exists (#11885) * Don't try to create parent folder if it already exists * Fix typo Co-authored-by: Elitsa Marinovska --- .../Packaging/PackageDataInstallation.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs index defea0ea51..790cefe7e9 100644 --- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs @@ -575,21 +575,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging { var importedFolders = new Dictionary(); var trackEntityContainersInstalled = new List(); - foreach (var documentType in unsortedDocumentTypes) + + foreach (XElement documentType in unsortedDocumentTypes) { - var foldersAttribute = documentType.Attribute("Folders"); - var infoElement = documentType.Element("Info"); + XAttribute foldersAttribute = documentType.Attribute("Folders"); + XElement infoElement = documentType.Element("Info"); if (foldersAttribute != null && infoElement != null - //don't import any folder if this is a child doc type - the parent doc type will need to - //exist which contains it's folders + // don't import any folder if this is a child doc type - the parent doc type will need to + // exist which contains it's folders && ((string)infoElement.Element("Master")).IsNullOrWhiteSpace()) { var alias = documentType.Element("Info").Element("Alias").Value; var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash); - var folderKeysAttribute = documentType.Attribute("FolderKeys"); + XAttribute folderKeysAttribute = documentType.Attribute("FolderKeys"); - var folderKeys = Array.Empty(); + Guid[] folderKeys = Array.Empty(); if (folderKeysAttribute != null) { folderKeys = folderKeysAttribute.Value.Split(Constants.CharArrays.ForwardSlash).Select(x=>Guid.Parse(x)).ToArray(); @@ -597,22 +598,22 @@ namespace Umbraco.Cms.Infrastructure.Packaging var rootFolder = WebUtility.UrlDecode(folders[0]); - EntityContainer current; + EntityContainer current = null; Guid? rootFolderKey = null; if (folderKeys.Length == folders.Length && folderKeys.Length > 0) { rootFolderKey = folderKeys[0]; current = _contentTypeService.GetContainer(rootFolderKey.Value); } - else - { - //level 1 = root level folders, there can only be one with the same name - current = _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); - } + + // The folder might already exist, but with a different key, so check if it exists, even if there is a key. + // Level 1 = root level folders, there can only be one with the same name + current ??= _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault(); if (current == null) { - var tryCreateFolder = _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + Attempt> tryCreateFolder = _contentTypeService.CreateContainer(-1, rootFolderKey ?? Guid.NewGuid(), rootFolder); + if (tryCreateFolder == false) { _logger.LogError(tryCreateFolder.Exception, "Could not create folder: {FolderName}", rootFolder); @@ -644,7 +645,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging private EntityContainer CreateContentTypeChildFolder(string folderName, Guid folderKey, IUmbracoEntity current) { var children = _entityService.GetChildren(current.Id).ToArray(); - var found = children.Any(x => x.Name.InvariantEquals(folderName) ||x.Key.Equals(folderKey)); + var found = children.Any(x => x.Name.InvariantEquals(folderName) || x.Key.Equals(folderKey)); if (found) { var containerId = children.Single(x => x.Name.InvariantEquals(folderName)).Id; From 1810cec80a2af07027c9c036c1a126089a840ab4 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Mon, 31 Jan 2022 08:39:33 +0000 Subject: [PATCH 25/39] Simplify registration for ambiguous ctor ActivatorUtilitiesConstructor respected. --- .../DependencyInjection/UmbracoBuilderExtensions.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 46002a6c8d..63027a3c9e 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -130,10 +130,7 @@ namespace Umbraco.Extensions this IUmbracoBuilder builder) { builder.Services.TryAddTransient(sp => - { - IUserDataService userDataService = sp.GetRequiredService(); - return ActivatorUtilities.CreateInstance(sp, userDataService); - }); + ActivatorUtilities.CreateInstance(sp)); return builder; } From bdcb5d859e3a2524f8b43ade340dc9ac15e873c4 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 31 Jan 2022 10:12:02 +0100 Subject: [PATCH 26/39] Get site name from appsettings if possible --- .../Configuration/Models/HostingSettings.cs | 7 ++++++- .../AspNetCoreHostingEnvironment.cs | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs index b9e11e99ca..cbe1fa6965 100644 --- a/src/Umbraco.Core/Configuration/Models/HostingSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HostingSettings.cs @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Core.Configuration.Models /// /// Gets or sets a value for the location of temporary files. /// - [DefaultValue(StaticLocalTempStorageLocation)] + [DefaultValue(StaticLocalTempStorageLocation)] public LocalTempStorage LocalTempStorageLocation { get; set; } = Enum.Parse(StaticLocalTempStorageLocation); /// @@ -31,5 +31,10 @@ namespace Umbraco.Cms.Core.Configuration.Models /// true if [debug mode]; otherwise, false. [DefaultValue(StaticDebug)] public bool Debug { get; set; } = StaticDebug; + + /// + /// Gets or sets a value specifying the name of the site. + /// + public string SiteName { get; set; } } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 9e5919c1e2..13f73e1b41 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -37,7 +37,16 @@ namespace Umbraco.Cms.Web.Common.AspNetCore _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; - SiteName = webHostEnvironment.ApplicationName; + SetSiteName(hostingSettings.CurrentValue.SiteName); + + // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack + // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. + // See summery of OptionsMonitorAdapter for more information. + if (hostingSettings is OptionsMonitor) + { + hostingSettings.OnChange(settings => SetSiteName(settings.SiteName)); + } + ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) @@ -53,7 +62,7 @@ namespace Umbraco.Cms.Web.Common.AspNetCore public Uri ApplicationMainUrl { get; private set; } /// - public string SiteName { get; } + public string SiteName { get; private set; } /// public string ApplicationId @@ -198,7 +207,10 @@ namespace Umbraco.Cms.Web.Common.AspNetCore } } } + + private void SetSiteName(string siteName) => + SiteName = string.IsNullOrWhiteSpace(siteName) + ? _webHostEnvironment.ApplicationName + : siteName; } - - } From 22dd3a214cef510eb5764382b1e209decb1ab5aa Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 31 Jan 2022 14:39:29 +0100 Subject: [PATCH 27/39] Stop TouchServerTask is registered role accessor is not elected --- .../RecurringHostedServiceBase.cs | 3 +- .../ServerRegistration/TouchServerTask.cs | 34 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs index 70dcb3a04e..b6f2b469f6 100644 --- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs +++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs @@ -21,7 +21,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// protected static readonly TimeSpan DefaultDelay = TimeSpan.FromMinutes(3); - private readonly TimeSpan _period; + private TimeSpan _period; private readonly TimeSpan _delay; private Timer _timer; @@ -73,6 +73,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices /// public Task StopAsync(CancellationToken cancellationToken) { + _period = Timeout.InfiniteTimeSpan; _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index 282847963f..f650ce9c94 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -2,13 +2,17 @@ // See LICENSE for more details. using System; +using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration @@ -22,6 +26,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration private readonly IServerRegistrationService _serverRegistrationService; private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; + private readonly IServerRoleAccessor _serverRoleAccessor; private readonly GlobalSettings _globalSettings; /// @@ -37,7 +42,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration IServerRegistrationService serverRegistrationService, IHostingEnvironment hostingEnvironment, ILogger logger, - IOptions globalSettings) + IOptions globalSettings, + IServerRoleAccessor serverRoleAccessor) : base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { _runtimeState = runtimeState; @@ -45,6 +51,24 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration _hostingEnvironment = hostingEnvironment; _logger = logger; _globalSettings = globalSettings.Value; + _serverRoleAccessor = serverRoleAccessor; + } + + [Obsolete("Use constructor that takes an IServerRoleAccessor")] + public TouchServerTask( + IRuntimeState runtimeState, + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptions globalSettings) + : this( + runtimeState, + serverRegistrationService, + hostingEnvironment, + logger, + globalSettings, + StaticServiceProvider.Instance.GetRequiredService()) + { } public override Task PerformExecuteAsync(object state) @@ -54,6 +78,14 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration return Task.CompletedTask; } + // If we're the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor + // this task no longer makes sense, since all it's used for is to allow the ElectedServerRoleAccessor + // to figure out what role a given server has, so we just stop this task. + if (_serverRoleAccessor is not ElectedServerRoleAccessor) + { + return StopAsync(CancellationToken.None); + } + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); if (serverAddress.IsNullOrWhiteSpace()) { From febdac5713fe7c648bd7824e6614d7956163c60b Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 31 Jan 2022 14:46:25 +0100 Subject: [PATCH 28/39] Fix tests --- .../ServerRegistration/TouchServerTaskTests.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs index 29b011a5a6..c690f35b7a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration @@ -51,7 +52,15 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.Serv VerifyServerTouched(); } - private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = ApplicationUrl) + [Test] + public async Task Does_Not_Execute_When_Role_Accessor_Is_Not_Elected() + { + TouchServerTask sut = CreateTouchServerTask(useElection: false); + await sut.PerformExecuteAsync(null); + VerifyServerNotTouched(); + } + + private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = ApplicationUrl, bool useElection = true) { var mockRequestAccessor = new Mock(); mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(ApplicationUrl) : null); @@ -71,12 +80,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.Serv } }; + IServerRoleAccessor roleAccessor = useElection + ? new ElectedServerRoleAccessor(_mockServerRegistrationService.Object) + : new SingleServerRoleAccessor(); + return new TouchServerTask( mockRunTimeState.Object, _mockServerRegistrationService.Object, mockRequestAccessor.Object, mockLogger.Object, - Options.Create(settings)); + Options.Create(settings), + roleAccessor); } private void VerifyServerNotTouched() => VerifyServerTouchedTimes(Times.Never()); From 824bc2ac285651999492378a3c04890d6ca823f8 Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 31 Jan 2022 14:58:45 +0100 Subject: [PATCH 29/39] Fix word salad in comment --- .../HostedServices/ServerRegistration/TouchServerTask.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index f650ce9c94..d54d67338e 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -78,8 +78,8 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration return Task.CompletedTask; } - // If we're the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor - // this task no longer makes sense, since all it's used for is to allow the ElectedServerRoleAccessor + // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense, + // since all it's used for is to allow the ElectedServerRoleAccessor // to figure out what role a given server has, so we just stop this task. if (_serverRoleAccessor is not ElectedServerRoleAccessor) { From 3af6645ad89a8b541e81284deab9bae765c4b73e Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 31 Jan 2022 15:02:25 +0100 Subject: [PATCH 30/39] Fix URL with culture (#11886) * Ignore casing when comparing default culture * Ignore casing in GetAssignedWithCulture Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Routing/DefaultUrlProvider.cs | 13 +++++++++---- .../DomainCacheExtensions.cs | 4 +++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index ae2c3d7f3a..5c27760b2a 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -134,8 +134,13 @@ namespace Umbraco.Cms.Core.Routing return GetUrlFromRoute(route, umbracoContext, content.Id, current, mode, culture); } - internal UrlInfo GetUrlFromRoute(string route, IUmbracoContext umbracoContext, int id, Uri current, - UrlMode mode, string culture) + internal UrlInfo GetUrlFromRoute( + string route, + IUmbracoContext umbracoContext, + int id, + Uri current, + UrlMode mode, + string culture) { if (string.IsNullOrWhiteSpace(route)) { @@ -149,12 +154,12 @@ namespace Umbraco.Cms.Core.Routing // route is / or / var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); - var domainUri = pos == 0 + DomainAndUri domainUri = pos == 0 ? null : DomainUtilities.DomainForNode(umbracoContext.PublishedSnapshot.Domains, _siteDomainMapper, int.Parse(route.Substring(0, pos), CultureInfo.InvariantCulture), current, culture); var defaultCulture = _localizationService.GetDefaultLanguageIsoCode(); - if (domainUri is not null || culture == defaultCulture || string.IsNullOrEmpty(culture)) + if (domainUri is not null || culture is null || culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase)) { var url = AssembleUrl(domainUri, path, current, mode).ToString(); return UrlInfo.Url(url, culture); diff --git a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs b/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs index 61f10917fd..47cc427217 100644 --- a/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs +++ b/src/Umbraco.PublishedCache.NuCache/DomainCacheExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Umbraco.Cms.Core.PublishedCache; @@ -9,7 +10,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache { var assigned = domainCache.GetAssigned(documentId, includeWildcards); - return culture is null ? assigned.Any() : assigned.Any(x => x.Culture == culture); + // It's super important that we always compare cultures with ignore case, since we can't be sure of the casing! + return culture is null ? assigned.Any() : assigned.Any(x => x.Culture.Equals(culture, StringComparison.InvariantCultureIgnoreCase)); } } } From 158f4d29f6e7854e596d434078eae765d884a0b1 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 4 Feb 2022 13:02:19 +0100 Subject: [PATCH 31/39] Bump version to 9.4.0-rc --- .../UmbracoPackage/.template.config/template.json | 2 +- .../UmbracoProject/.template.config/template.json | 2 +- src/Directory.Build.props | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index 1a4dd16fd7..082f9301bf 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0-rc", + "defaultValue": "9.4.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index fd41de8d1c..810940c4eb 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -57,7 +57,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0-rc", + "defaultValue": "9.4.0-rc", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 995c8afebd..68962caef4 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,10 +3,10 @@ - 9.3.0 - 9.3.0 - 9.3.0-rc - 9.3.0 + 9.4.0 + 9.4.0 + 9.4.0-rc + 9.4.0 9.0 en-US Umbraco CMS From 58b75c58aa2116a53760ef090cddbaab55cb180f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 7 Feb 2022 09:21:26 +0100 Subject: [PATCH 32/39] Fixes issue with miniprofiler losing the information when doing a redirect, e.g. in a successful surfacecontroller call. (#11939) Now we store the profiler in a cookie on local redirects and pick it up on next request and add it as a child to that profiler --- .../Profiler/WebProfiler.cs | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 7c5a89fa71..688414b3de 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,11 +1,16 @@ using System; using System.Linq; +using System.Net; using System.Threading; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.Extensions.DependencyInjection; using StackExchange.Profiling; +using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Profiler @@ -13,6 +18,8 @@ namespace Umbraco.Cms.Web.Common.Profiler public class WebProfiler : IProfiler { + private const string WebProfileCookieKey = "umbracoWebProfiler"; + public static readonly AsyncLocal MiniProfilerContext = new AsyncLocal(x => { _ = x; @@ -39,7 +46,6 @@ namespace Umbraco.Cms.Web.Common.Profiler public void Stop(bool discardResults = false) => MiniProfilerContext.Value?.Stop(discardResults); - public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -50,9 +56,13 @@ namespace Umbraco.Cms.Web.Common.Profiler if (ShouldProfile(context.Request)) { Start(); + ICookieManager cookieManager = GetCookieManager(context); + cookieManager.ExpireCookie(WebProfileCookieKey); //Ensure we expire the cookie, so we do not reuse the old potential value saved } } + private static ICookieManager GetCookieManager(HttpContext context) => context.RequestServices.GetRequiredService(); + public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) { if (runtimeLevel != RuntimeLevel.Run) @@ -70,19 +80,42 @@ namespace Umbraco.Cms.Web.Common.Profiler var first = Interlocked.Exchange(ref _first, 1) == 0; if (first) { - - var startupDuration = _startupProfiler.Root.DurationMilliseconds.GetValueOrDefault(); - MiniProfilerContext.Value.DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; - MiniProfilerContext.Value.Root.AddChild(_startupProfiler.Root); + AddSubProfiler(_startupProfiler); _startupProfiler = null; } + + ICookieManager cookieManager = GetCookieManager(context); + var cookieValue = cookieManager.GetCookieValue(WebProfileCookieKey); + + if (cookieValue is not null) + { + AddSubProfiler(MiniProfiler.FromJson(cookieValue)); + } + + //If it is a redirect to a relative path (local redirect) + if (context.Response.StatusCode == (int)HttpStatusCode.Redirect + && context.Response.Headers.TryGetValue(Microsoft.Net.Http.Headers.HeaderNames.Location, out var location) + && !location.Contains("://")) + { + MiniProfilerContext.Value.Root.Name = "Before Redirect"; + cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson()); + } + } } } + private void AddSubProfiler(MiniProfiler subProfiler) + { + var startupDuration = subProfiler.Root.DurationMilliseconds.GetValueOrDefault(); + MiniProfilerContext.Value.DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.GetTimingHierarchy().First().DurationMilliseconds += startupDuration; + MiniProfilerContext.Value.Root.AddChild(subProfiler.Root); + + } + private static bool ShouldProfile(HttpRequest request) { if (request.IsClientSideRequest()) return false; From d7fef7cd0cbfe824220b27f3bed0f07bdead64fa Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 7 Feb 2022 12:09:13 +0100 Subject: [PATCH 33/39] Check blockObject.content for null --- .../blockeditormodelobject.service.js | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 22baed8472..09c1659775 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -101,11 +101,30 @@ /** * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockObject BlockObject to recive data values from. + * @param {Object} blockObject BlockObject to receive data values from. */ function getBlockLabel(blockObject) { if (blockObject.labelInterpolator !== undefined) { - var labelVars = Object.assign({"$contentTypeName": blockObject.content.contentTypeName, "$settings": blockObject.settingsData || {}, "$layout": blockObject.layout || {}, "$index": (blockObject.index || 0)+1 }, blockObject.data); + // blockobject.content may be null if the block is no longer allowed, + // so try and fall back to the label in the config, + // if that too is null, there's not much we can do, so just default to empty string. + var contentTypeName; + if(blockObject.content != null){ + contentTypeName = blockObject.content.contentTypeName; + } + else if(blockObject.config != null && blockObject.config.label != null){ + contentTypeName = blockObject.config.label; + } + else { + contentTypeName = ""; + } + + var labelVars = Object.assign({ + "$contentTypeName": contentTypeName, + "$settings": blockObject.settingsData || {}, + "$layout": blockObject.layout || {}, + "$index": (blockObject.index || 0)+1 + }, blockObject.data); var label = blockObject.labelInterpolator(labelVars); if (label) { return label; @@ -511,10 +530,10 @@ * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve a Block Object for the given layout entry. * The Block Object offers the necessary data to display and edit a block. - * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. - * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. + * The Block Object setups live synchronization of content and settings models back to the data of your Property Editor model. + * The returned object, named ´BlockObject´, contains several useful models to make editing of this block happen. * The ´BlockObject´ contains the following properties: - * - key {string}: runtime generated key, usefull for tracking of this object + * - key {string}: runtime generated key, useful for tracking of this object * - content {Object}: Content model, the content data in a ElementType model. * - settings {Object}: Settings model, the settings data in a ElementType model. * - config {Object}: A local deep copy of the block configuration model. @@ -522,12 +541,11 @@ * - updateLabel {Method}: Method to trigger an update of the label for this block. * - data {Object}: A reference to the content data object from your property editor model. * - settingsData {Object}: A reference to the settings data object from your property editor model. - * - layout {Object}: A refernce to the layout entry from your property editor model. + * - layout {Object}: A reference to the layout entry from your property editor model. * @param {Object} layoutEntry the layout entry object to build the block model from. - * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. + * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasn't found for this block. */ getBlockObject: function (layoutEntry) { - var contentUdi = layoutEntry.contentUdi; var dataModel = getDataByUdi(contentUdi, this.value.contentData); From 23803a44b7c32c446decceff3794c4bb7d60f327 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 7 Feb 2022 14:32:58 +0100 Subject: [PATCH 34/39] Fixed minor issues and added xml docs (#11943) --- .../Services/ITwoFactorLoginService.cs | 41 ++++++++++++++-- .../Implement/TwoFactorLoginService.cs | 49 ++++++++++++++++--- .../Security/TwoFactorValidationProvider.cs | 2 +- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index dd11f864fb..33a96ad751 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -5,24 +5,57 @@ using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Services { + /// + /// Service handling 2FA logins. + /// public interface ITwoFactorLoginService : IService { /// - /// Deletes all user logins - normally used when a member is deleted + /// Deletes all user logins - normally used when a member is deleted. /// Task DeleteUserLoginsAsync(Guid userOrMemberKey); - Task IsTwoFactorEnabledAsync(Guid userKey); - Task GetSecretForUserAndProviderAsync(Guid userKey, string providerName); + /// + /// Checks whether 2FA is enabled for the user or member with the specified key. + /// + Task IsTwoFactorEnabledAsync(Guid userOrMemberKey); + /// + /// Gets the secret for user or member and a specific provider. + /// + Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName); + + /// + /// Gets the setup info for a specific user or member and a specific provider. + /// + /// + /// The returned type can be anything depending on the setup providers. You will need to cast it to the type handled by the provider. + /// Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); + /// + /// Gets all registered providers names. + /// IEnumerable GetAllProviderNames(); + + /// + /// Disables the 2FA provider with the specified provider name for the specified user or member. + /// Task DisableAsync(Guid userOrMemberKey, string providerName); + /// + /// Validates the setup of the provider using the secret and code. + /// bool ValidateTwoFactorSetup(string providerName, string secret, string code); + + /// + /// Saves the 2FA login information. + /// Task SaveAsync(TwoFactorLogin twoFactorLogin); + + /// + /// Gets all the enabled 2FA providers for the user or member with the specified key. + /// Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); } - } diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs index 713a73c1df..cdcc6b19e9 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -11,31 +11,41 @@ using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Services { + /// public class TwoFactorLoginService : ITwoFactorLoginService { private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; private readonly IScopeProvider _scopeProvider; private readonly IOptions _identityOptions; + private readonly IOptions _backOfficeIdentityOptions; private readonly IDictionary _twoFactorSetupGenerators; + /// + /// Initializes a new instance of the class. + /// public TwoFactorLoginService( ITwoFactorLoginRepository twoFactorLoginRepository, IScopeProvider scopeProvider, IEnumerable twoFactorSetupGenerators, - IOptions identityOptions) + IOptions identityOptions, + IOptions backOfficeIdentityOptions + ) { _twoFactorLoginRepository = twoFactorLoginRepository; _scopeProvider = scopeProvider; _identityOptions = identityOptions; - _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x=>x.ProviderName); + _backOfficeIdentityOptions = backOfficeIdentityOptions; + _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); } + /// public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey); } + /// public async Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey) { return await GetEnabledProviderNamesAsync(userOrMemberKey); @@ -47,26 +57,46 @@ namespace Umbraco.Cms.Core.Services var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) .Select(x => x.ProviderName).ToArray(); - return providersOnUser.Where(x => _identityOptions.Value.Tokens.ProviderMap.ContainsKey(x)); + return providersOnUser.Where(IsKnownProviderName); } + /// + /// The provider needs to be registered as either a member provider or backoffice provider to show up. + /// + private bool IsKnownProviderName(string providerName) + { + if (_identityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + if (_backOfficeIdentityOptions.Value.Tokens.ProviderMap.ContainsKey(providerName)) + { + return true; + } + + return false; + } + + /// public async Task IsTwoFactorEnabledAsync(Guid userOrMemberKey) { return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any(); } + /// public async Task GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x=>x.ProviderName == providerName)?.Secret; + return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x => x.ProviderName == providerName)?.Secret; } + /// public async Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName) { var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); - //Dont allow to generate a new secrets if user already has one + // Dont allow to generate a new secrets if user already has one if (!string.IsNullOrEmpty(secret)) { return default; @@ -82,14 +112,17 @@ namespace Umbraco.Cms.Core.Services return await generator.GetSetupDataAsync(userOrMemberKey, secret); } + /// public IEnumerable GetAllProviderNames() => _twoFactorSetupGenerators.Keys; + + /// public async Task DisableAsync(Guid userOrMemberKey, string providerName) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); - return (await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName)); - + return await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName); } + /// public bool ValidateTwoFactorSetup(string providerName, string secret, string code) { if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) @@ -100,6 +133,7 @@ namespace Umbraco.Cms.Core.Services return generator.ValidateTwoFactorSetup(secret, code); } + /// public Task SaveAsync(TwoFactorLogin twoFactorLogin) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); @@ -108,7 +142,6 @@ namespace Umbraco.Cms.Core.Services return Task.CompletedTask; } - /// /// Generates a new random unique secret. /// diff --git a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs index 32b3226440..d4272515e5 100644 --- a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs +++ b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Infrastructure.Security public class TwoFactorBackOfficeValidationProvider : TwoFactorValidationProvider where TTwoFactorSetupGenerator : ITwoFactorProvider { - protected TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) + public TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) { } From b89cd86d850c2cc3e817b0ee78d9b190fd19f11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 8 Feb 2022 10:35:06 +0100 Subject: [PATCH 35/39] Apply the Umbraco logo to BackOffice (#11949) * adding logo as text in replacement of umbraco_logo_white, this will enable existing configuration to continue to work and minimise the breaking change of this PR. * adjust logo position to fit with logged-in logomark. Allowing for the logo and customised logo to be very wide. * adding logomark in topbar of backoffice * login box style * correction of shadow * Logo modal, to display more information about the product including linking to the website * rename to modal * stop hidden when mouse is out * Version line without Umbraco * focus link and use blur as the indication for closing. * correcting to rgba * focus and click outside needs a little help to work well * use @zindexUmbOverlay to ensure right depth going forward. * adding large logo svg * append ; * tidy logo svg file --- .../application/umbraco_logo_large_blue.svg | 41 +++++ .../img/application/umbraco_logo_white.svg | 44 ++++- .../application/umbraco_logomark_white.svg | 3 + .../application/umbappheader.directive.js | 33 +++- .../application/umb-app-header.less | 55 +++++- .../src/less/pages/login.less | 13 +- .../application/umb-app-header.html | 100 +++++++++-- .../src/views/errors/BootFailed.html | 160 +++++++++--------- 8 files changed, 343 insertions(+), 106 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg create mode 100644 src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg new file mode 100644 index 0000000000..ce15dd3092 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_large_blue.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg index b27ae89e91..c0bdbdd40c 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logo_white.svg @@ -1,3 +1,41 @@ - - - + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg new file mode 100644 index 0000000000..b27ae89e91 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/assets/img/application/umbraco_logomark_white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index b52b0a5763..6cf6dd85f3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -1,9 +1,9 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService, backdropService, overlayService) { + function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) { - function link(scope, el, attr, ctrl) { + function link(scope, element) { var evts = []; @@ -84,6 +84,35 @@ overlayService.open(dialog); }; + scope.logoModal = { + show: false, + text: "", + timer: null + }; + scope.showLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.show = true; + scope.logoModal.text = "version "+Umbraco.Sys.ServerVariables.application.version; + $timeout(function () { + const anchorLink = element[0].querySelector('.umb-app-header__logo-modal'); + if(anchorLink) { + anchorLink.focus(); + } + }); + }; + scope.keepLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + }; + scope.hideLogoModal = function() { + $timeout.cancel(scope.logoModal.timer); + scope.logoModal.timer = $timeout(function () { + scope.logoModal.show = false; + }, 100); + }; + scope.stopClickEvent = function($event) { + $event.stopPropagation(); + }; + } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less index 68a29df89e..bb346fc402 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-app-header.less @@ -2,12 +2,65 @@ background: @blueExtraDark; display: flex; align-items: center; - justify-content: space-between; max-width: 100%; height: @appHeaderHeight; padding: 0 20px; } +.umb-app-header__logo { + margin-right: 30px; + button { + img { + height: 30px; + } + } +} + +.umb-app-header__logo-modal { + position: absolute; + z-index: @zindexUmbOverlay; + top: 50px; + left: 17px; + font-size: 13px; + + border-radius: 6px; + + width: 160px; + padding: 20px 20px; + background-color:@white; + color: @blueExtraDark; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, .14), 0 1px 6px 1px rgba(0, 0, 0, .14); + text-decoration: none; + + text-align: center; + + &::before { + content:''; + position: absolute; + transform: rotate(45deg); + background-color:@white; + top: -4px; + left: 14px; + width: 8px; + height: 8px; + } + + img { + display: block; + height: auto; + width: 120px; + margin-left: auto; + margin-right: auto; + margin-bottom: 3px; + } +} + +.umb-app-header__right { + display: flex; + align-items: center; + margin-left: auto; +} + .umb-app-header__actions { display: flex; list-style: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 2763a879ea..015c291564 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -28,14 +28,14 @@ .login-overlay__logo { position: absolute; - top: 22px; - left: 25px; + top: 12.5px; + left: 20px; + right: 25px; height: 30px; z-index: 1; } - -.login-overlay__logo > img { - max-height:100%; +.login-overlay__logo img { + height: 100%; } .login-overlay .umb-modalcolumn { @@ -69,7 +69,8 @@ margin-right: 25px; margin-top: auto; margin-bottom: auto; - border-radius: @baseBorderRadius; + border-radius: @doubleBorderRadius; + box-shadow: 0 1px 6px 1px rgba(0, 0, 0, 0.12); } .login-overlay .form input[type="text"], diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index e0fb4aeb77..98b8d88869 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -1,34 +1,99 @@ -
-
- - + -
+ + +
  • -
  • -
  • -
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html index c08627739a..7b91125e09 100644 --- a/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html +++ b/src/Umbraco.Web.UI.Client/src/views/errors/BootFailed.html @@ -1,79 +1,87 @@ - - - - Boot Failed - - - -
- -
-

Boot Failed

-

Umbraco failed to boot, if you are the owner of the website please see the log file for more details.

-
-
- + + + + Boot Failed + + + +
+ +
+

Boot Failed

+

+ Umbraco failed to boot, if you are the owner of the website + please see the log file for more details. +

+
+
+ From 3d28552a77f110fe9e69ee85fbfa89914cb154a8 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 9 Feb 2022 10:19:39 +0100 Subject: [PATCH 36/39] Add settings to bypass 2fa for external logins (#11959) * Added settings for bypassing 2fa for external logins * Fixed issue with saving roles using member ID before the member had an ID. * Added missing extension method * Removed test classes from git * rollback csproj --- .../Configuration/Models/SecuritySettings.cs | 14 +++++ .../Security/MemberUserStore.cs | 3 + .../Controllers/BackOfficeController.cs | 55 ++++++++++++++++++- .../UmbracoBuilder.BackOfficeIdentity.cs | 10 ++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 1 + .../Controllers/UmbExternalLoginController.cs | 9 ++- 6 files changed, 88 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs index 7d4dd45fb8..982ba8c63e 100644 --- a/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SecuritySettings.cs @@ -11,6 +11,8 @@ namespace Umbraco.Cms.Core.Configuration.Models [UmbracoOptions(Constants.Configuration.ConfigSecurity)] public class SecuritySettings { + internal const bool StaticMemberBypassTwoFactorForExternalLogins = true; + internal const bool StaticUserBypassTwoFactorForExternalLogins = true; internal const bool StaticKeepUserLoggedIn = false; internal const bool StaticHideDisabledUsersInBackOffice = false; internal const bool StaticAllowPasswordReset = true; @@ -66,5 +68,17 @@ namespace Umbraco.Cms.Core.Configuration.Models /// Gets or sets a value for the member password settings. /// public MemberPasswordConfigurationSettings MemberPassword { get; set; } + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for members. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticMemberBypassTwoFactorForExternalLogins)] + public bool MemberBypassTwoFactorForExternalLogins { get; set; } = StaticMemberBypassTwoFactorForExternalLogins; + + /// + /// Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login for users. Thereby rely on the External login and potential 2FA at that provider. + /// + [DefaultValue(StaticUserBypassTwoFactorForExternalLogins)] + public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins; } } diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 4fba880e81..420d66b0b4 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -112,6 +112,9 @@ namespace Umbraco.Cms.Core.Security // create the member _memberService.Save(memberEntity); + //We need to add roles now that the member has an Id. It do not work implicit in UpdateMemberProperties + _memberService.AssignRoles(new[] { memberEntity.Id }, user.Roles.Select(x => x.RoleId).ToArray()); + if (!memberEntity.HasIdentity) { throw new DataException("Could not create the member, check logs for details"); diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index aa5fc83e1e..e866409c17 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -31,6 +32,7 @@ using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -68,7 +70,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; private readonly IManifestParser _manifestParser; private readonly ServerVariablesParser _serverVariables; + private readonly IOptions _securitySettings; + + [ActivatorUtilitiesConstructor] public BackOfficeController( IBackOfficeUserManager userManager, IRuntimeState runtimeState, @@ -87,7 +92,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHttpContextAccessor httpContextAccessor, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, IManifestParser manifestParser, - ServerVariablesParser serverVariables) + ServerVariablesParser serverVariables, + IOptions securitySettings) { _userManager = userManager; _runtimeState = runtimeState; @@ -107,6 +113,51 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; _manifestParser = manifestParser; _serverVariables = serverVariables; + _securitySettings = securitySettings; + } + + [Obsolete("Use ctor with all params. This overload will be removed in Umbraco 10.")] + public BackOfficeController( + IBackOfficeUserManager userManager, + IRuntimeState runtimeState, + IRuntimeMinifier runtimeMinifier, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + IGridConfig gridConfig, + BackOfficeServerVariables backOfficeServerVariables, + AppCaches appCaches, + IBackOfficeSignInManager signInManager, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + ILogger logger, + IJsonSerializer jsonSerializer, + IBackOfficeExternalLoginProviders externalLogins, + IHttpContextAccessor httpContextAccessor, + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + IManifestParser manifestParser, + ServerVariablesParser serverVariables) + : this(userManager, + runtimeState, + runtimeMinifier, + globalSettings, + hostingEnvironment, + textService, + gridConfig, + backOfficeServerVariables, + appCaches, + signInManager, + backofficeSecurityAccessor, + logger, + jsonSerializer, + externalLogins, + httpContextAccessor, + backOfficeTwoFactorOptions, + manifestParser, + serverVariables, + StaticServiceProvider.Instance.GetRequiredService>() + ) + { + } [HttpGet] @@ -458,7 +509,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (response == null) throw new ArgumentNullException(nameof(response)); // Sign in the user with this external login provider (which auto links, etc...) - SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false); + SignInResult result = await _signInManager.ExternalLoginSignInAsync(loginInfo, isPersistent: false, bypassTwoFactor: _securitySettings.Value.UserBypassTwoFactorForExternalLogins); var errors = new List(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index e9cc213598..1dc5bda7a9 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Security; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Security; @@ -77,5 +78,14 @@ namespace Umbraco.Extensions return umbracoBuilder; } + public static BackOfficeIdentityBuilder AddTwoFactorProvider(this BackOfficeIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider + { + identityBuilder.Services.AddSingleton(); + identityBuilder.Services.AddSingleton(); + identityBuilder.AddTokenProvider>(providerName); + + return identityBuilder; + } + } } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 69f812b6e6..46ff558aee 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs index c43754e170..cb9188f5d0 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs @@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; @@ -29,6 +31,7 @@ namespace Umbraco.Cms.Web.Website.Controllers { private readonly IMemberManager _memberManager; private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly IOptions _securitySettings; private readonly ILogger _logger; private readonly IMemberSignInManagerExternalLogins _memberSignInManager; @@ -42,7 +45,8 @@ namespace Umbraco.Cms.Web.Website.Controllers IPublishedUrlProvider publishedUrlProvider, IMemberSignInManagerExternalLogins memberSignInManager, IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService) + ITwoFactorLoginService twoFactorLoginService, + IOptions securitySettings) : base( umbracoContextAccessor, databaseFactory, @@ -55,6 +59,7 @@ namespace Umbraco.Cms.Web.Website.Controllers _memberSignInManager = memberSignInManager; _memberManager = memberManager; _twoFactorLoginService = twoFactorLoginService; + _securitySettings = securitySettings; } /// @@ -95,7 +100,7 @@ namespace Umbraco.Cms.Web.Website.Controllers } else { - SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false); + SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.MemberBypassTwoFactorForExternalLogins); if (result == SignInResult.Success) { From eea02137ae0b709861b45ded11882279a990c421 Mon Sep 17 00:00:00 2001 From: nikolajlauridsen Date: Wed, 9 Feb 2022 10:26:31 +0100 Subject: [PATCH 37/39] Bump version to non-rc --- build/templates/UmbracoPackage/.template.config/template.json | 2 +- build/templates/UmbracoProject/.template.config/template.json | 2 +- src/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json index 1a4dd16fd7..32f0c924dd 100644 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ b/build/templates/UmbracoPackage/.template.config/template.json @@ -24,7 +24,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0-rc", + "defaultValue": "9.3.0", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index fd41de8d1c..b09050a2a4 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -57,7 +57,7 @@ "version": { "type": "parameter", "datatype": "string", - "defaultValue": "9.3.0-rc", + "defaultValue": "9.3.0", "description": "The version of Umbraco to load using NuGet", "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" }, diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 995c8afebd..55448806ef 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ 9.3.0 9.3.0 - 9.3.0-rc + 9.3.0 9.3.0 9.0 en-US From cf410ab91e7d21620a971a219280886d852d10a8 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Thu, 10 Feb 2022 12:03:35 +0000 Subject: [PATCH 38/39] Attempt to make app local icu setup less problematic. (#11961) * Attempt to make app local icu setup less problematic. Prevents issues for windows build agent -> linux app server. On Windows version is split at first '.' e.g. 68.2.0.9 -> icuuc68.dll https://github.com/dotnet/runtime/blob/205f70e/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Windows.cs On Linux we are looking for libicuuc.so.68.2.0.9 https://github.com/dotnet/runtime/blob/205f70e/src/libraries/System.Private.CoreLib/src/System/Globalization/GlobalizationMode.Unix.cs On macos we don't have a native library in a shiny nuget package so hope folks are building on their macs or setting the rid until we have a better solution. * Combine elements --- build/templates/UmbracoProject/UmbracoProject.csproj | 10 +++++++--- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 11 ++++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj index 3fa1eb2f36..6b47686415 100644 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ b/build/templates/UmbracoProject/UmbracoProject.csproj @@ -12,9 +12,13 @@ - - - + + + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ec4d3c1798..b584606f4f 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -19,6 +19,15 @@ + + + + + + @@ -92,7 +101,7 @@ - + From 91c4c776767a8851317b339ebecc1a76ffdc9827 Mon Sep 17 00:00:00 2001 From: Paul Johnson Date: Fri, 11 Feb 2022 16:24:53 +0000 Subject: [PATCH 39/39] Switch a lot of warnings to suggestions until we are able to resolve. (#11974) * Switch a lot of warnings to suggestions until we are able to resolve. * Make stylecop respect more csharp_style rules e.g. csharp_using_directive_placement * Added cheatsheet * Drop sorting requirements for using directives. --- .editorconfig | 43 ----------------------- .globalconfig | 81 +++++++++++++++++++++++++++++++++++++++++++ Directory.Build.props | 8 +---- codeanalysis.ruleset | 18 ---------- stylecop.json | 16 --------- 5 files changed, 82 insertions(+), 84 deletions(-) create mode 100644 .globalconfig delete mode 100644 codeanalysis.ruleset delete mode 100644 stylecop.json diff --git a/.editorconfig b/.editorconfig index d4094b2cf3..eba04ad326 100644 --- a/.editorconfig +++ b/.editorconfig @@ -306,48 +306,6 @@ dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols dotnet_naming_rule.other_public_protected_fields_disallowed_rule.style = disallowed_style dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity = error -########################################## -# StyleCop Field Naming Rules -# Naming rules for fields follow the StyleCop analyzers -# This does not override any rules using disallowed_style above -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers -########################################## - -# All constant fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private -dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const -dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning - -# All static readonly fields must be PascalCase -# Ajusted to ignore private fields. -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly -dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style -dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning - -# No non-private instance fields are allowed -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected -dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style -dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error - -# Local variables must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md -dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local -dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style -dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent - # This rule should never fire. However, it's included for at least two purposes: # First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. # Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). @@ -357,7 +315,6 @@ dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_chec dotnet_naming_rule.sanity_check_uncovered_field_case_rule.style = internal_error_style dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error - ########################################## # Other Naming Rules ########################################## diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000000..8342ab4580 --- /dev/null +++ b/.globalconfig @@ -0,0 +1,81 @@ +is_global = true + +########################################## +# StyleCopAnalyzers Settings +########################################## + +# All constant fields must be PascalCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private +dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const +dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols = stylecop_constant_fields_group +dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style + +# All static readonly fields must be PascalCase +# Ajusted to ignore private fields. +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly +dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symbols = stylecop_static_readonly_fields_group +dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style + +# No non-private instance fields are allowed +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected +dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group +dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style + +# Local variables must be camelCase +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local +dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group +dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style + +########################################## +# StyleCopAnalyzers rule severity +# https://github.com/DotNetAnalyzers/StyleCopAnalyzers +########################################## + +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.DocumentationRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.ReadabilityRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.NamingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.MaintainabilityRules.severity = suggestion +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = suggestion + +dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match + +dotnet_diagnostic.SA1503.severity = warning # BracesMustNotBeOmitted +dotnet_diagnostic.SA1117.severity = warning # ParametersMustBeOnSameLineOrSeparateLines +dotnet_diagnostic.SA1116.severity = warning # SplitParametersMustStartOnLineAfterDeclaration +dotnet_diagnostic.SA1122.severity = warning # UseStringEmptyForEmptyStrings +dotnet_diagnostic.SA1028.severity = warning # CodeMustNotContainTrailingWhitespace +dotnet_diagnostic.SA1500.severity = warning # BracesForMultiLineStatementsMustNotShareLine +dotnet_diagnostic.SA1401.severity = warning # FieldsMustBePrivate +dotnet_diagnostic.SA1519.severity = warning # BracesMustNotBeOmittedFromMultiLineChildStatement +dotnet_diagnostic.SA1111.severity = warning # ClosingParenthesisMustBeOnLineOfLastParameter +dotnet_diagnostic.SA1520.severity = warning # UseBracesConsistently +dotnet_diagnostic.SA1407.severity = warning # ArithmeticExpressionsMustDeclarePrecedence +dotnet_diagnostic.SA1400.severity = warning # AccessModifierMustBeDeclared +dotnet_diagnostic.SA1119.severity = warning # StatementMustNotUseUnnecessaryParenthesis +dotnet_diagnostic.SA1649.severity = warning # FileNameMustMatchTypeName +dotnet_diagnostic.SA1121.severity = warning # UseBuiltInTypeAlias +dotnet_diagnostic.SA1132.severity = warning # DoNotCombineFields +dotnet_diagnostic.SA1134.severity = warning # AttributesMustNotShareLine +dotnet_diagnostic.SA1106.severity = warning # CodeMustNotContainEmptyStatements +dotnet_diagnostic.SA1312.severity = warning # VariableNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1303.severity = warning # ConstFieldNamesMustBeginWithUpperCaseLetter +dotnet_diagnostic.SA1310.severity = warning # FieldNamesMustNotContainUnderscore +dotnet_diagnostic.SA1130.severity = warning # UseLambdaSyntax +dotnet_diagnostic.SA1405.severity = warning # DebugAssertMustProvideMessageText +dotnet_diagnostic.SA1205.severity = warning # PartialElementsMustDeclareAccess +dotnet_diagnostic.SA1306.severity = warning # FieldNamesMustBeginWithLowerCaseLetter +dotnet_diagnostic.SA1209.severity = warning # UsingAliasDirectivesMustBePlacedAfterOtherUsingDirectives +dotnet_diagnostic.SA1216.severity = warning # UsingStaticDirectivesMustBePlacedAtTheCorrectLocation +dotnet_diagnostic.SA1133.severity = warning # DoNotCombineAttributes +dotnet_diagnostic.SA1135.severity = warning # UsingDirectivesMustBeQualified diff --git a/Directory.Build.props b/Directory.Build.props index 74f1ebad3d..fcf605f555 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,12 +2,6 @@ - - + - - - - $(MSBuildThisFileDirectory)codeanalysis.ruleset - diff --git a/codeanalysis.ruleset b/codeanalysis.ruleset deleted file mode 100644 index ab5ad88f57..0000000000 --- a/codeanalysis.ruleset +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/stylecop.json b/stylecop.json deleted file mode 100644 index b2f7771470..0000000000 --- a/stylecop.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", - "settings": { - "orderingRules": { - "usingDirectivesPlacement": "outsideNamespace", - "elementOrder": [ - "kind" - ] - }, - "documentationRules": { - "xmlHeader": false, - "documentInternalElements": false, - "copyrightText": "Copyright (c) Umbraco.\nSee LICENSE for more details." - } - } -}