diff --git a/.github/config/codeql-config.yml b/.github/config/codeql-config.yml index 7bac345491..77b390d392 100644 --- a/.github/config/codeql-config.yml +++ b/.github/config/codeql-config.yml @@ -1,14 +1,8 @@ name: "CodeQL config" -on: - push: - branches: [v8/contrib,v8/dev] -paths-ignore: - - node_modules - - Umbraco.TestData - - Umbraco.Tests - - Umbraco.Tests.AcceptanceTest - - Umbraco.Tests.Benchmarks - - bin - - build.tmp + paths: - - src + - src + +paths-ignore: + - '**/node_modules' + - 'src/Umbraco.Web.UI/wwwroot' \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ee912262d7..33d3e851c7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -2,67 +2,33 @@ name: "Code scanning - action" on: push: - branches: [v8/contrib,v8/dev,v8/bug,v8/feature] + branches: ['*/dev','*/contrib'] pull_request: # The branches below must be a subset of the branches above - schedule: - - cron: '0 7 * * 2' + branches: ['*/dev','*/contrib'] jobs: CodeQL-Build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v2 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - - name: configure Pagefile - uses: al-cheb/configure-pagefile-action@v1.2 with: - minimum-size: 8GB - maximum-size: 32GB - - - run: | - echo "Run Umbraco-CMS build" - pwsh -command .\build\build.ps1 + config-file: ./.github/config/codeql-config.yml + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + + - name: dotnet build + run: dotnet build umbraco-netcore-only.sln # also runs npm build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v1 - with: - config-file: ./.github/config/codeql-config.yml - - # This job is to prevent the workflow status from showing as failed when all other jobs are skipped - See https://github.community/t/workflow-is-failing-if-no-job-can-be-ran-due-to-condition/16873 - always_job: - name: Always run job - runs-on: ubuntu-latest - steps: - - name: Always run - run: echo "This job is to prevent the workflow status from showing as failed when all other jobs are skipped" - diff --git a/tests/Umbraco.Tests/App.config b/legacy/Umbraco.Tests/App.config similarity index 100% rename from tests/Umbraco.Tests/App.config rename to legacy/Umbraco.Tests/App.config diff --git a/tests/Umbraco.Tests/Properties/AssemblyInfo.cs b/legacy/Umbraco.Tests/Properties/AssemblyInfo.cs similarity index 100% rename from tests/Umbraco.Tests/Properties/AssemblyInfo.cs rename to legacy/Umbraco.Tests/Properties/AssemblyInfo.cs diff --git a/tests/Umbraco.Tests/Published/ModelTypeTests.cs b/legacy/Umbraco.Tests/Published/ModelTypeTests.cs similarity index 100% rename from tests/Umbraco.Tests/Published/ModelTypeTests.cs rename to legacy/Umbraco.Tests/Published/ModelTypeTests.cs diff --git a/tests/Umbraco.Tests/Routing/BaseUrlProviderTest.cs b/legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs similarity index 100% rename from tests/Umbraco.Tests/Routing/BaseUrlProviderTest.cs rename to legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs diff --git a/tests/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs similarity index 100% rename from tests/Umbraco.Tests/Routing/MediaUrlProviderTests.cs rename to legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs diff --git a/tests/Umbraco.Tests/Umbraco.Tests.csproj b/legacy/Umbraco.Tests/Umbraco.Tests.csproj similarity index 100% rename from tests/Umbraco.Tests/Umbraco.Tests.csproj rename to legacy/Umbraco.Tests/Umbraco.Tests.csproj diff --git a/tests/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs similarity index 100% rename from tests/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs rename to legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs diff --git a/tests/Umbraco.Tests/unit-test-log4net.CI.config b/legacy/Umbraco.Tests/unit-test-log4net.CI.config similarity index 100% rename from tests/Umbraco.Tests/unit-test-log4net.CI.config rename to legacy/Umbraco.Tests/unit-test-log4net.CI.config diff --git a/tests/Umbraco.Tests/unit-test-logger.config b/legacy/Umbraco.Tests/unit-test-logger.config similarity index 100% rename from tests/Umbraco.Tests/unit-test-logger.config rename to legacy/Umbraco.Tests/unit-test-logger.config diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7cbbf59e19..9fd8703f84 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,4 +1,4 @@ - + diff --git a/src/JsonSchema/AppSettings.cs b/src/JsonSchema/AppSettings.cs index 8db2bbb8c7..048513a5da 100644 --- a/src/JsonSchema/AppSettings.cs +++ b/src/JsonSchema/AppSettings.cs @@ -1,7 +1,9 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Deploy.Core.Configuration.DeployConfiguration; +using Umbraco.Deploy.Core.Configuration.DeployProjectConfiguration; using Umbraco.Forms.Core.Configuration; using SecuritySettings = Umbraco.Cms.Core.Configuration.Models.SecuritySettings; @@ -82,7 +84,10 @@ namespace JsonSchema public BasicAuthSettings BasicAuth { get; set; } public PackageMigrationSettings PackageMigration { get; set; } + public LegacyPasswordMigrationSettings LegacyPasswordMigration { get; set; } + + public ContentDashboardSettings ContentDashboard { get; set; } } /// @@ -116,6 +121,9 @@ namespace JsonSchema /// public class DeployDefinition { + public DeploySettings Settings { get; set; } + + public DeployProjectConfig Project { get; set; } } } } diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index 441abba8ba..f0652e7e98 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -11,6 +11,8 @@ + + diff --git a/src/Umbraco.Core/Actions/ActionAssignDomain.cs b/src/Umbraco.Core/Actions/ActionAssignDomain.cs index 2a609e365f..0638f605af 100644 --- a/src/Umbraco.Core/Actions/ActionAssignDomain.cs +++ b/src/Umbraco.Core/Actions/ActionAssignDomain.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Cms.Core.Actions; diff --git a/src/Umbraco.Core/Configuration/Models/CharItem.cs b/src/Umbraco.Core/Configuration/Models/CharItem.cs new file mode 100644 index 0000000000..e269e0a83e --- /dev/null +++ b/src/Umbraco.Core/Configuration/Models/CharItem.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Cms.Core.Configuration.Models +{ + public class CharItem : IChar + { + /// + /// The character to replace + /// + public string Char { get; set; } + + /// + /// The replacement character + /// + public string Replacement { get; set; } + } +} diff --git a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs similarity index 55% rename from src/Umbraco.Core/Configuration/ContentDashboardSettings.cs rename to src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs index 7bef36dba4..3f8546a1ad 100644 --- a/src/Umbraco.Core/Configuration/ContentDashboardSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentDashboardSettings.cs @@ -1,6 +1,11 @@ - -namespace Umbraco.Cms.Core.Configuration +using System.ComponentModel; + +namespace Umbraco.Cms.Core.Configuration.Models { + /// + /// Typed configuration options for content dashboard settings. + /// + [UmbracoOptions(Constants.Configuration.ConfigContentDashboard)] public class ContentDashboardSettings { private const string DefaultContentDashboardPath = "cms"; @@ -18,6 +23,13 @@ namespace Umbraco.Cms.Core.Configuration /// Gets the path to use when constructing the URL for retrieving data for the content dashboard. /// /// The URL path. - public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; + [DefaultValue(DefaultContentDashboardPath)] + public string ContentDashboardPath { get; set; } = DefaultContentDashboardPath; + + /// + /// Gets the allowed addresses to retrieve data for the content dashboard. + /// + /// The URLs. + public string[] ContentDashboardUrlAllowlist { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index 97fb91b0ec..7e3e1a2700 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -31,21 +31,19 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticSanitizeTinyMce = false; /// - /// Gets or sets a value for the reserved URLs. - /// It must end with a comma + /// Gets or sets a value for the reserved URLs (must end with a comma). /// [DefaultValue(StaticReservedUrls)] public string ReservedUrls { get; set; } = StaticReservedUrls; /// - /// Gets or sets a value for the reserved paths. - /// It must end with a comma + /// Gets or sets a value for the reserved paths (must end with a comma). /// [DefaultValue(StaticReservedPaths)] public string ReservedPaths { get; set; } = StaticReservedPaths; /// - /// Gets or sets a value for the timeout + /// Gets or sets a value for the back-office login timeout. /// [DefaultValue(StaticTimeOut)] public TimeSpan TimeOut { get; set; } = TimeSpan.Parse(StaticTimeOut); @@ -104,11 +102,19 @@ namespace Umbraco.Cms.Core.Configuration.Models public string UmbracoScriptsPath { get; set; } = StaticUmbracoScriptsPath; /// - /// Gets or sets a value for the Umbraco media path. + /// Gets or sets a value for the Umbraco media request path. /// [DefaultValue(StaticUmbracoMediaPath)] public string UmbracoMediaPath { get; set; } = StaticUmbracoMediaPath; + /// + /// Gets or sets a value for the physical Umbraco media root path (falls back to when empty). + /// + /// + /// If the value is a virtual path, it's resolved relative to the webroot. + /// + public string UmbracoMediaPhysicalRootPath { get; set; } + /// /// Gets or sets a value indicating whether to install the database when it is missing. /// @@ -131,6 +137,9 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public string MainDomLock { get; set; } = string.Empty; + /// + /// Gets or sets the telemetry ID. + /// public string Id { get; set; } = string.Empty; /// @@ -164,19 +173,19 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool IsPickupDirectoryLocationConfigured => !string.IsNullOrWhiteSpace(Smtp?.PickupDirectoryLocation); - /// Gets a value indicating whether TinyMCE scripting sanitization should be applied + /// + /// Gets a value indicating whether TinyMCE scripting sanitization should be applied. /// [DefaultValue(StaticSanitizeTinyMce)] public bool SanitizeTinyMce => StaticSanitizeTinyMce; /// - /// An int value representing the time in milliseconds to lock the database for a write operation + /// Gets a value representing the time in milliseconds to lock the database for a write operation. /// /// - /// The default value is 5000 milliseconds + /// The default value is 5000 milliseconds. /// - /// The timeout in milliseconds. [DefaultValue(StaticSqlWriteLockTimeOut)] public TimeSpan SqlWriteLockTimeOut { get; } = TimeSpan.Parse(StaticSqlWriteLockTimeOut); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs index ee223b36c6..051c31dc26 100644 --- a/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/RequestHandlerSettings.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using System.Collections.Generic; using System.ComponentModel; using Umbraco.Cms.Core.Configuration.UmbracoSettings; @@ -16,33 +17,34 @@ namespace Umbraco.Cms.Core.Configuration.Models { internal const bool StaticAddTrailingSlash = true; internal const string StaticConvertUrlsToAscii = "try"; + internal const bool StaticEnableDefaultCharReplacements = true; internal static readonly CharItem[] DefaultCharCollection = { - new CharItem { Char = " ", Replacement = "-" }, - new CharItem { Char = "\"", Replacement = string.Empty }, - new CharItem { Char = "'", Replacement = string.Empty }, - new CharItem { Char = "%", Replacement = string.Empty }, - new CharItem { Char = ".", Replacement = string.Empty }, - new CharItem { Char = ";", Replacement = string.Empty }, - new CharItem { Char = "/", Replacement = string.Empty }, - new CharItem { Char = "\\", Replacement = string.Empty }, - new CharItem { Char = ":", Replacement = string.Empty }, - new CharItem { Char = "#", Replacement = string.Empty }, - new CharItem { Char = "+", Replacement = "plus" }, - new CharItem { Char = "*", Replacement = "star" }, - new CharItem { Char = "&", Replacement = string.Empty }, - new CharItem { Char = "?", Replacement = string.Empty }, - new CharItem { Char = "æ", Replacement = "ae" }, - new CharItem { Char = "ä", Replacement = "ae" }, - new CharItem { Char = "ø", Replacement = "oe" }, - new CharItem { Char = "ö", Replacement = "oe" }, - new CharItem { Char = "å", Replacement = "aa" }, - new CharItem { Char = "ü", Replacement = "ue" }, - new CharItem { Char = "ß", Replacement = "ss" }, - new CharItem { Char = "|", Replacement = "-" }, - new CharItem { Char = "<", Replacement = string.Empty }, - new CharItem { Char = ">", Replacement = string.Empty } + new () { Char = " ", Replacement = "-" }, + new () { Char = "\"", Replacement = string.Empty }, + new () { Char = "'", Replacement = string.Empty }, + new () { Char = "%", Replacement = string.Empty }, + new () { Char = ".", Replacement = string.Empty }, + new () { Char = ";", Replacement = string.Empty }, + new () { Char = "/", Replacement = string.Empty }, + new () { Char = "\\", Replacement = string.Empty }, + new () { Char = ":", Replacement = string.Empty }, + new () { Char = "#", Replacement = string.Empty }, + new () { Char = "+", Replacement = "plus" }, + new () { Char = "*", Replacement = "star" }, + new () { Char = "&", Replacement = string.Empty }, + new () { Char = "?", Replacement = string.Empty }, + new () { Char = "æ", Replacement = "ae" }, + new () { Char = "ä", Replacement = "ae" }, + new () { Char = "ø", Replacement = "oe" }, + new () { Char = "ö", Replacement = "oe" }, + new () { Char = "å", Replacement = "aa" }, + new () { Char = "ü", Replacement = "ue" }, + new () { Char = "ß", Replacement = "ss" }, + new () { Char = "|", Replacement = "-" }, + new () { Char = "<", Replacement = string.Empty }, + new () { Char = ">", Replacement = string.Empty } }; /// @@ -67,41 +69,21 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool ShouldTryConvertUrlsToAscii => ConvertUrlsToAscii.InvariantEquals("try"); - // We need to special handle ":", as this character is special in keys - - // TODO: implement from configuration - - //// var collection = _configuration.GetSection(Prefix + "CharCollection").GetChildren() - //// .Select(x => new CharItem() - //// { - //// Char = x.GetValue("Char"), - //// Replacement = x.GetValue("Replacement"), - //// }).ToArray(); - - //// if (collection.Any() || _configuration.GetSection("Prefix").GetChildren().Any(x => - //// x.Key.Equals("CharCollection", StringComparison.OrdinalIgnoreCase))) - //// { - //// return collection; - //// } - - //// return DefaultCharCollection; + /// + /// Disable all default character replacements + /// + [DefaultValue(StaticEnableDefaultCharReplacements)] + public bool EnableDefaultCharReplacements { get; set; } = StaticEnableDefaultCharReplacements; /// - /// Gets or sets a value for the default character collection for replacements. + /// Add additional character replacements, or override defaults /// - /// WB-TODO + [Obsolete("Use the GetCharReplacements extension method in the Umbraco.Extensions namespace instead. Scheduled for removal in V11")] public IEnumerable CharCollection { get; set; } = DefaultCharCollection; /// - /// Defines a character replacement. + /// Add additional character replacements, or override defaults /// - public class CharItem : IChar - { - /// - public string Char { get; set; } - - /// - public string Replacement { get; set; } - } + public IEnumerable UserDefinedCharCollection { get; set; } } } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs new file mode 100644 index 0000000000..a916febb93 --- /dev/null +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/CharacterReplacementEqualityComparer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +{ + public class CharacterReplacementEqualityComparer : IEqualityComparer + { + public bool Equals(IChar x, IChar y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x is null) + { + return false; + } + + if (y is null) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + return x.Char == y.Char && x.Replacement == y.Replacement; + } + + public int GetHashCode(IChar obj) + { + unchecked + { + return ((obj.Char != null ? obj.Char.GetHashCode() : 0) * 397) ^ (obj.Replacement != null ? obj.Replacement.GetHashCode() : 0); + } + } + } +} diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs index 4073a12149..61e840245c 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IChar.cs @@ -1,8 +1,9 @@ -namespace Umbraco.Cms.Core.Configuration.UmbracoSettings +namespace Umbraco.Cms.Core.Configuration.UmbracoSettings { public interface IChar { string Char { get; } + string Replacement { get; } } } diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 063d733821..ab951618e3 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { public static partial class Constants { @@ -54,6 +54,7 @@ public const string ConfigUserPassword = ConfigPrefix + "Security:UserPassword"; public const string ConfigRichTextEditor = ConfigPrefix + "RichTextEditor"; public const string ConfigPackageMigration = ConfigPrefix + "PackageMigration"; + public const string ConfigContentDashboard = ConfigPrefix + "ContentDashboard"; } } } diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs index da82746445..1fd16133e5 100644 --- a/src/Umbraco.Core/Constants-DatabaseProviders.cs +++ b/src/Umbraco.Core/Constants-DatabaseProviders.cs @@ -5,7 +5,7 @@ public static class DatabaseProviders { public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "System.Data.SqlClient"; + public const string SqlServer = "Microsoft.Data.SqlClient"; } } } diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs index 5770bd07e4..5a8ea401cb 100644 --- a/src/Umbraco.Core/Constants-HealthChecks.cs +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { /// /// Defines constants. @@ -20,15 +20,16 @@ public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; } + public static class Configuration { public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; } + public static class FolderAndFilePermissionsCheck { - public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; @@ -37,7 +38,7 @@ public static class Security { - + public const string UmbracoApplicationUrlCheck = "https://umbra.co/healthchecks-umbraco-application-url"; public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; @@ -46,7 +47,6 @@ public static class HttpsCheck { - public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index b509c12ff5..68601a78b0 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -50,6 +50,7 @@ namespace Umbraco.Cms.Core /// providers need to be setup differently and each auth type for the back office will be prefixed with this value /// public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers."; public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; diff --git a/src/Umbraco.Core/Constants-SystemDirectories.cs b/src/Umbraco.Core/Constants-SystemDirectories.cs index 80b49781ec..bf34aab989 100644 --- a/src/Umbraco.Core/Constants-SystemDirectories.cs +++ b/src/Umbraco.Core/Constants-SystemDirectories.cs @@ -43,6 +43,8 @@ namespace Umbraco.Cms.Core public const string AppPlugins = "/App_Plugins"; public static string AppPluginIcons => "/Backoffice/Icons"; + public const string CreatedPackages = "/created-packages"; + public const string MvcViews = "~/Views"; diff --git a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs index 871a0bbe02..5cb9a7137f 100644 --- a/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Umbraco.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -7,50 +7,88 @@ namespace Umbraco.Extensions { public static class ServiceCollectionExtensions { - public static void AddUnique(this IServiceCollection services) + /// + /// Adds a service of type with an implementation type of to the specified . + /// + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TService : class where TImplementing : class, TService - => services.Replace(ServiceDescriptor.Singleton()); + { + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), typeof(TImplementing), lifetime)); + } /// - /// Registers a singleton instance against multiple interfaces. + /// Adds services of types & with a shared implementation type of to the specified . /// - public static void AddMultipleUnique(this IServiceCollection services) + /// + /// Removes all previous registrations for the types & . + /// + public static void AddMultipleUnique( + this IServiceCollection services, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TService1 : class where TService2 : class where TImplementing : class, TService1, TService2 { - services.AddUnique(); - services.AddUnique(factory => (TImplementing)factory.GetRequiredService()); + services.AddUnique(lifetime); + services.AddUnique(factory => (TImplementing)factory.GetRequiredService(), lifetime); } // TODO(V11): Remove this function. [Obsolete("This method is functionally equivalent to AddSingleton() please use that instead.")] public static void AddUnique(this IServiceCollection services) where TImplementing : class - => services.Replace(ServiceDescriptor.Singleton()); + { + services.RemoveAll(); + services.AddSingleton(); + } /// - /// Registers a unique service with an implementation factory. + /// Adds a service of type with an implementation factory method to the specified . /// - /// Unique services have one single implementation, and a Singleton lifetime. - public static void AddUnique(this IServiceCollection services, Func factory) + /// + /// Removes all previous registrations for the type . + /// + public static void AddUnique( + this IServiceCollection services, + Func factory, + ServiceLifetime lifetime = ServiceLifetime.Singleton) where TService : class - => services.Replace(ServiceDescriptor.Singleton(factory)); + { + services.RemoveAll(); + services.Add(ServiceDescriptor.Describe(typeof(TService), factory, lifetime)); + } /// - /// Registers a unique service with an implementing instance. + /// Adds a singleton service of the type specified by to the specified . /// - /// Unique services have one single implementation, and a Singleton lifetime. + /// + /// Removes all previous registrations for the type specified by . + /// public static void AddUnique(this IServiceCollection services, Type serviceType, object instance) - => services.Replace(ServiceDescriptor.Singleton(serviceType, instance)); + { + services.RemoveAll(serviceType); + services.AddSingleton(serviceType, instance); + } /// - /// Registers a unique service with an implementing instance. + /// Adds a singleton service of type to the specified . /// + /// + /// Removes all previous registrations for the type type . + /// public static void AddUnique(this IServiceCollection services, TService instance) where TService : class - => services.Replace(ServiceDescriptor.Singleton(instance)); + { + services.RemoveAll(); + services.AddSingleton(instance); + } internal static IServiceCollection AddLazySupport(this IServiceCollection services) { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 6ef87464e8..f0cbf7f95d 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; using System.Reflection; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.Models.Validation; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection { @@ -13,23 +16,25 @@ namespace Umbraco.Cms.Core.DependencyInjection public static partial class UmbracoBuilderExtensions { - private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder) + private static IUmbracoBuilder AddUmbracoOptions(this IUmbracoBuilder builder, Action> configure = null) where TOptions : class { var umbracoOptionsAttribute = typeof(TOptions).GetCustomAttribute(); - if (umbracoOptionsAttribute is null) { - throw new ArgumentException("typeof(TOptions) do not have the UmbracoOptionsAttribute"); + throw new ArgumentException($"{typeof(TOptions)} do not have the UmbracoOptionsAttribute."); } - - builder.Services.AddOptions() - .Bind(builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), - o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties) + var optionsBuilder = builder.Services.AddOptions() + .Bind( + builder.Config.GetSection(umbracoOptionsAttribute.ConfigurationKey), + o => o.BindNonPublicProperties = umbracoOptionsAttribute.BindNonPublicProperties + ) .ValidateDataAnnotations(); - return builder; + configure?.Invoke(optionsBuilder); + + return builder; } /// @@ -52,7 +57,13 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions() + .AddUmbracoOptions(optionsBuilder => optionsBuilder.PostConfigure(options => + { + if (string.IsNullOrEmpty(options.UmbracoMediaPhysicalRootPath)) + { + options.UmbracoMediaPhysicalRootPath = options.UmbracoMediaPath; + } + })) .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() @@ -74,7 +85,10 @@ namespace Umbraco.Cms.Core.DependencyInjection .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); + + builder.Services.Configure(options => options.MergeReplacements(builder.Config)); return builder; } diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index 58ccb06ed0..44eac93641 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -7,6 +7,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; namespace Umbraco.Cms.Core.Events @@ -84,17 +86,56 @@ namespace Umbraco.Cms.Core.Events internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper where TNotification : INotification { + /// + /// + /// Background - During v9 build we wanted an in-process message bus to facilitate removal of the old static event handlers.
+ /// Instead of taking a dependency on MediatR we (the community) implemented our own using MediatR as inspiration. + ///
+ /// + /// + /// Some things worth knowing about MediatR. + /// + /// All handlers are by default registered with transient lifetime, but can easily depend on services with state. + /// Both the Mediatr instance and its handler resolver are registered transient and as such it is always possible to depend on scoped services in a handler. + /// + /// + /// + /// + /// Our EventAggregator started out registered with a transient lifetime but later (before initial release) the registration was changed to singleton, presumably + /// because there are a lot of singleton services in Umbraco which like to publish notifications and it's a pain to use scoped services from a singleton. + ///
+ /// The problem with a singleton EventAggregator is it forces handlers to create a service scope and service locate any scoped services + /// they wish to make use of e.g. a unit of work (think entity framework DBContext). + ///
+ /// + /// + /// Moving forwards it probably makes more sense to register EventAggregator transient but doing so now would mean an awful lot of service location to avoid breaking changes. + ///
+ /// For now we can do the next best thing which is to create a scope for each published notification, thus enabling the transient handlers to take a dependency on a scoped service. + ///
+ /// + /// + /// Did discuss using HttpContextAccessor/IScopedServiceProvider to enable sharing of scopes when publisher has http context, + /// but decided against because it's inconsistent with what happens in background threads and will just cause confusion. + /// + ///
public override Task HandleAsync( INotification notification, CancellationToken cancellationToken, ServiceFactory serviceFactory, Func>, INotification, CancellationToken, Task> publish) { - IEnumerable> handlers = serviceFactory - .GetInstances>() + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() .Select(x => new Func( (theNotification, theToken) => - x.HandleAsync((TNotification)theNotification, theToken))); + x.HandleAsync((TNotification)theNotification, theToken))); return publish(handlers, notification, cancellationToken); } @@ -103,13 +144,23 @@ namespace Umbraco.Cms.Core.Events internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper where TNotification : INotification { + /// + /// See remarks on for explanation on + /// what's going on with the IServiceProvider stuff here. + /// public override void Handle( INotification notification, ServiceFactory serviceFactory, Action>, INotification> publish) { - IEnumerable> handlers = serviceFactory - .GetInstances>() + // Create a new service scope from which to resolve handlers and ensure it's disposed when it goes out of scope. + // TODO: go back to using ServiceFactory to resolve + IServiceScopeFactory scopeFactory = serviceFactory.GetInstance(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider container = scope.ServiceProvider; + + IEnumerable> handlers = container + .GetServices>() .Select(x => new Action( (theNotification) => x.Handle((TNotification)theNotification))); diff --git a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs index 9b3674b07b..bceddf1fd6 100644 --- a/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs +++ b/src/Umbraco.Core/Extensions/ClaimsIdentityExtensions.cs @@ -62,6 +62,19 @@ namespace Umbraco.Extensions return username; } + public static string GetEmail(this IIdentity identity) + { + if (identity == null) throw new ArgumentNullException(nameof(identity)); + + string email = null; + if (identity is ClaimsIdentity claimsIdentity) + { + email = claimsIdentity.FindFirstValue(ClaimTypes.Email); + } + + return email; + } + /// /// Returns the first claim value found in the for the given claimType /// diff --git a/src/Umbraco.Core/Extensions/ContentExtensions.cs b/src/Umbraco.Core/Extensions/ContentExtensions.cs index b9d1c0b7b4..fa20bc02ba 100644 --- a/src/Umbraco.Core/Extensions/ContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/ContentExtensions.cs @@ -367,7 +367,7 @@ namespace Umbraco.Extensions /// to generate xml for /// /// Xml representation of the passed in - internal static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) + public static XElement ToDeepXml(this IContent content, IEntityXmlSerializer serializer) { return serializer.Serialize(content, false, true); } diff --git a/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs new file mode 100644 index 0000000000..e9e6618f8c --- /dev/null +++ b/src/Umbraco.Core/Extensions/RequestHandlerSettingsExtension.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Extensions +{ + /// + /// Get concatenated user and default character replacements + /// taking into account + /// + public static class RequestHandlerSettingsExtension + { + /// + /// Get concatenated user and default character replacements + /// taking into account + /// + public static IEnumerable GetCharReplacements(this RequestHandlerSettings requestHandlerSettings) + { + if (requestHandlerSettings.EnableDefaultCharReplacements is false) + { + return requestHandlerSettings.UserDefinedCharCollection ?? Enumerable.Empty(); + } + + if (requestHandlerSettings.UserDefinedCharCollection == null || requestHandlerSettings.UserDefinedCharCollection.Any() is false) + { + return RequestHandlerSettings.DefaultCharCollection; + } + + return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection); + } + + /// + /// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection + /// + internal static void MergeReplacements(this RequestHandlerSettings requestHandlerSettings, IConfiguration configuration) + { + string sectionKey = $"{Constants.Configuration.ConfigRequestHandler}:"; + + IEnumerable charCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(RequestHandlerSettings.CharCollection)}"); + + IEnumerable userDefinedCharCollection = GetReplacements( + configuration, + $"{sectionKey}{nameof(requestHandlerSettings.UserDefinedCharCollection)}"); + + IEnumerable mergedCollection = MergeUnique(userDefinedCharCollection, charCollection); + + requestHandlerSettings.UserDefinedCharCollection = mergedCollection; + } + + private static IEnumerable GetReplacements(IConfiguration configuration, string key) + { + var replacements = new List(); + IEnumerable config = configuration.GetSection(key).GetChildren(); + + foreach (IConfigurationSection section in config) + { + var @char = section.GetValue(nameof(CharItem.Char)); + var replacement = section.GetValue(nameof(CharItem.Replacement)); + replacements.Add(new CharItem { Char = @char, Replacement = replacement }); + } + + return replacements; + } + + /// + /// Merges two IEnumerable of CharItem without any duplicates, items in priorityReplacements will override those in alternativeReplacements + /// + private static IEnumerable MergeUnique( + IEnumerable priorityReplacements, + IEnumerable alternativeReplacements) + { + var priorityReplacementsList = priorityReplacements.ToList(); + var alternativeReplacementsList = alternativeReplacements.ToList(); + + foreach (CharItem alternativeReplacement in alternativeReplacementsList) + { + foreach (CharItem priorityReplacement in priorityReplacementsList) + { + if (priorityReplacement.Char == alternativeReplacement.Char) + { + alternativeReplacement.Replacement = priorityReplacement.Replacement; + } + } + } + + return priorityReplacementsList.Union( + alternativeReplacementsList, + new CharacterReplacementEqualityComparer()); + } + } +} diff --git a/src/Umbraco.Core/Extensions/TypeExtensions.cs b/src/Umbraco.Core/Extensions/TypeExtensions.cs index 67a6dd1dce..c5bc99cae8 100644 --- a/src/Umbraco.Core/Extensions/TypeExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeExtensions.cs @@ -430,10 +430,16 @@ namespace Umbraco.Extensions where T : Attribute { if (type == null) return Enumerable.Empty(); - return type.GetCustomAttributes(typeof (T), inherited).OfType(); + return type.GetCustomAttributes(typeof(T), inherited).OfType(); } - /// + public static bool HasCustomAttribute(this Type type, bool inherit) + where T : Attribute + { + return type.GetCustomAttribute(inherit) != null; + } + + /// /// Tries to return a value based on a property name for an object but ignores case sensitivity /// /// diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs new file mode 100644 index 0000000000..44b10ba0e3 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -0,0 +1,68 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.HealthChecks.Checks.Security +{ + [HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] + public class UmbracoApplicationUrlCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IOptionsMonitor _webRoutingSettings; + + public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IOptionsMonitor webRoutingSettings) + { + _textService = textService; + _webRoutingSettings = webRoutingSettings; + } + + /// + /// Executes the action and returns its status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("UmbracoApplicationUrlCheck has no executable actions"); + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() => + Task.FromResult(CheckUmbracoApplicationUrl().Yield()); + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _webRoutingSettings.CurrentValue.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var success = false; + + if (url.IsNullOrWhiteSpace()) + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + success = true; + } + + return new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.UmbracoApplicationUrlCheck + }; + } + } +} diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 23be195e4b..c95d37e1c3 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -3,6 +3,7 @@ using System.IO; using System.Security.Cryptography; using System.Text; using System.Threading; +using Microsoft.Extensions.FileProviders; using Umbraco.Cms.Core.IO; namespace Umbraco.Extensions @@ -87,5 +88,24 @@ namespace Umbraco.Extensions } fs.DeleteFile(tempFile); } + + /// + /// Creates a new from the file system. + /// + /// The file system. + /// When this method returns, contains an created from the file system. + /// + /// true if the was successfully created; otherwise, false. + /// + public static bool TryCreateFileProvider(this IFileSystem fileSystem, out IFileProvider fileProvider) + { + fileProvider = fileSystem switch + { + IFileProviderFactory fileProviderFactory => fileProviderFactory.Create(), + _ => null + }; + + return fileProvider != null; + } } } diff --git a/src/Umbraco.Core/IO/IFileProviderFactory.cs b/src/Umbraco.Core/IO/IFileProviderFactory.cs new file mode 100644 index 0000000000..742467ccc8 --- /dev/null +++ b/src/Umbraco.Core/IO/IFileProviderFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Cms.Core.IO +{ + /// + /// Factory for creating instances. + /// + public interface IFileProviderFactory + { + /// + /// Creates a new instance. + /// + /// + /// The newly created instance (or null if not supported). + /// + IFileProvider Create(); + } +} diff --git a/src/Umbraco.Core/IO/MediaFileManager.cs b/src/Umbraco.Core/IO/MediaFileManager.cs index 96680d3f84..c769b9801e 100644 --- a/src/Umbraco.Core/IO/MediaFileManager.cs +++ b/src/Umbraco.Core/IO/MediaFileManager.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -22,13 +21,22 @@ namespace Umbraco.Cms.Core.IO private readonly IShortStringHelper _shortStringHelper; private readonly IServiceProvider _serviceProvider; private MediaUrlGeneratorCollection _mediaUrlGenerators; - private readonly ContentSettings _contentSettings; - /// - /// Gets the media filesystem. - /// - public IFileSystem FileSystem { get; } + public MediaFileManager( + IFileSystem fileSystem, + IMediaPathScheme mediaPathScheme, + ILogger logger, + IShortStringHelper shortStringHelper, + IServiceProvider serviceProvider) + { + _mediaPathScheme = mediaPathScheme; + _logger = logger; + _shortStringHelper = shortStringHelper; + _serviceProvider = serviceProvider; + FileSystem = fileSystem; + } + [Obsolete("Use the ctr that doesn't include unused parameters.")] public MediaFileManager( IFileSystem fileSystem, IMediaPathScheme mediaPathScheme, @@ -36,14 +44,13 @@ namespace Umbraco.Cms.Core.IO IShortStringHelper shortStringHelper, IServiceProvider serviceProvider, IOptions contentSettings) - { - _mediaPathScheme = mediaPathScheme; - _logger = logger; - _shortStringHelper = shortStringHelper; - _serviceProvider = serviceProvider; - _contentSettings = contentSettings.Value; - FileSystem = fileSystem; - } + : this(fileSystem, mediaPathScheme, logger, shortStringHelper, serviceProvider) + { } + + /// + /// Gets the media filesystem. + /// + public IFileSystem FileSystem { get; } /// /// Delete media files. diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index db10cae416..3da09a499c 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -3,14 +3,17 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - public interface IPhysicalFileSystem : IFileSystem {} - public class PhysicalFileSystem : IPhysicalFileSystem + public interface IPhysicalFileSystem : IFileSystem + { } + + public class PhysicalFileSystem : IPhysicalFileSystem, IFileProviderFactory { private readonly IIOHelper _ioHelper; private readonly ILogger _logger; @@ -28,7 +31,7 @@ namespace Umbraco.Cms.Core.IO // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; - public PhysicalFileSystem(IIOHelper ioHelper,IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) + public PhysicalFileSystem(IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, ILogger logger, string rootPath, string rootUrl) { _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -270,7 +273,7 @@ namespace Umbraco.Cms.Core.IO return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash); // unchanged - what else? - return path; + return path.TrimStart(Constants.CharArrays.ForwardSlash); } /// @@ -285,7 +288,7 @@ namespace Umbraco.Cms.Core.IO public string GetFullPath(string path) { // normalize - var opath = path; + var originalPath = path; path = EnsureDirectorySeparatorChar(path); // FIXME: this part should go! @@ -318,7 +321,7 @@ namespace Umbraco.Cms.Core.IO // nothing prevents us to reach the file, security-wise, yet it is outside // this filesystem's root - throw - throw new UnauthorizedAccessException($"File original: [{opath}] full: [{path}] is outside this filesystem's root."); + throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root."); } /// @@ -450,6 +453,9 @@ namespace Umbraco.Cms.Core.IO } } + /// + public IFileProvider Create() => new PhysicalFileProvider(_rootPath); + #endregion } } diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index cda61cf7b5..7bf315a575 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -1,14 +1,15 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Hosting; using Umbraco.Extensions; namespace Umbraco.Cms.Core.IO { - internal class ShadowWrapper : IFileSystem + internal class ShadowWrapper : IFileSystem, IFileProviderFactory { private static readonly string ShadowFsPath = Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "ShadowFs"; @@ -220,5 +221,8 @@ namespace Umbraco.Cms.Core.IO { FileSystem.AddFile(path, physicalPath, overrideIfExists, copy); } + + /// + public IFileProvider Create() => _innerFileSystem.TryCreateFileProvider(out IFileProvider fileProvider) ? fileProvider : null; } } diff --git a/src/Umbraco.Core/Models/ITwoFactorLogin.cs b/src/Umbraco.Core/Models/ITwoFactorLogin.cs new file mode 100644 index 0000000000..ca005309b2 --- /dev/null +++ b/src/Umbraco.Core/Models/ITwoFactorLogin.cs @@ -0,0 +1,12 @@ +using System; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + public interface ITwoFactorLogin: IEntity, IRememberBeingDirty + { + string ProviderName { get; } + string Secret { get; } + Guid UserOrMemberKey { get; } + } +} diff --git a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs index d61e32d88a..d8ac8d635d 100644 --- a/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/MemberTabsAndPropertiesMapper.cs @@ -65,14 +65,11 @@ namespace Umbraco.Cms.Core.Models.Mapping var resolved = base.Map(source, context); - // This is kind of a hack because a developer is supposed to be allowed to set their property editor - would have been much easier - // if we just had all of the membership provider fields on the member table :( - // TODO: But is there a way to map the IMember.IsLockedOut to the property ? i dunno. + // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock) var isLockedOutProperty = resolved.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut); if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1") { - isLockedOutProperty.View = "readonlyvalue"; - isLockedOutProperty.Value = _localizedTextService.Localize("general", "no"); + isLockedOutProperty.Readonly = true; } return resolved; @@ -191,20 +188,6 @@ namespace Umbraco.Cms.Core.Models.Mapping { var properties = new List { - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}id", - Label = _localizedTextService.Localize("general","id"), - Value = new List {member.Id.ToString(), member.Key.ToString()}, - View = "idwithguid" - }, - new ContentPropertyDisplay - { - Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}doctype", - Label = _localizedTextService.Localize("content","membertype"), - Value = _localizedTextService.UmbracoDictionaryTranslate(CultureDictionary, member.ContentType.Name), - View = _propertyEditorCollection[Constants.PropertyEditors.Aliases.Label].GetValueEditor().View - }, GetLoginProperty(member, _localizedTextService), new ContentPropertyDisplay { @@ -212,7 +195,7 @@ namespace Umbraco.Cms.Core.Models.Mapping Label = _localizedTextService.Localize("general","email"), Value = member.Email, View = "email", - Validation = {Mandatory = true} + Validation = { Mandatory = true } }, new ContentPropertyDisplay { @@ -221,12 +204,10 @@ namespace Umbraco.Cms.Core.Models.Mapping Value = new Dictionary { // TODO: why ignoreCase, what are we doing here?! - {"newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null)}, + { "newPassword", member.GetAdditionalDataValueIgnoreCase("NewPassword", null) } }, - // TODO: Hard coding this because the changepassword doesn't necessarily need to be a resolvable (real) property editor View = "changepassword", - // Initialize the dictionary with the configuration from the default membership provider - Config = GetPasswordConfig(member) + Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider }, new ContentPropertyDisplay { @@ -234,7 +215,10 @@ namespace Umbraco.Cms.Core.Models.Mapping Label = _localizedTextService.Localize("content","membergroup"), Value = GetMemberGroupValue(member.Username), View = "membergroups", - Config = new Dictionary {{"IsRequired", true}} + Config = new Dictionary + { + { "IsRequired", true } + } } }; diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 390f644831..7168f99078 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -68,12 +68,15 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: + 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 @@ -81,7 +84,7 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: @@ -124,11 +127,13 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(serializer.Serialize(currentTags.Except(trimmedTags).ToArray()), culture); // json array + var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array break; } } @@ -160,7 +165,7 @@ namespace Umbraco.Extensions case TagsStorageType.Json: try { - return serializer.Deserialize(value).Select(x => x.ToString().Trim()); + return serializer.Deserialize(value).Select(x => x.Trim()); } catch (Exception) { diff --git a/src/Umbraco.Core/Models/TwoFactorLogin.cs b/src/Umbraco.Core/Models/TwoFactorLogin.cs new file mode 100644 index 0000000000..6ede9606e8 --- /dev/null +++ b/src/Umbraco.Core/Models/TwoFactorLogin.cs @@ -0,0 +1,13 @@ +using System; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + public class TwoFactorLogin : EntityBase, ITwoFactorLogin + { + public string ProviderName { get; set; } + public string Secret { get; set; } + public Guid UserOrMemberKey { get; set; } + public bool Confirmed { get; set; } + } +} diff --git a/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs new file mode 100644 index 0000000000..980a531ffd --- /dev/null +++ b/src/Umbraco.Core/Notifications/MemberTwoFactorRequestedNotification.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Cms.Core.Notifications +{ + public class MemberTwoFactorRequestedNotification : INotification + { + public MemberTwoFactorRequestedNotification(Guid memberKey) + { + MemberKey = memberKey; + } + + public Guid MemberKey { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs new file mode 100644 index 0000000000..a3d38720d7 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartedNotification.cs @@ -0,0 +1,9 @@ +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 + { } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index 4cbf0a55c6..dd60f9431c 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,23 +1,23 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - namespace Umbraco.Cms.Core.Notifications { /// - /// Notification that occurs at the very end of the Umbraco boot - /// process and after all initialize. + /// Notification that occurs at the very end of the Umbraco boot process (after all s are initialized). /// + /// public class UmbracoApplicationStartingNotification : INotification { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The runtime level public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel) => RuntimeLevel = runtimeLevel; /// - /// Gets the runtime level of execution. + /// Gets the runtime level. /// + /// + /// The runtime level. + /// public RuntimeLevel RuntimeLevel { get; } } } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs new file mode 100644 index 0000000000..be4c6ccfd4 --- /dev/null +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppedNotification.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Core.Notifications +{ + /// + /// Notification that occurs when Umbraco has completely shutdown. + /// + /// + public class UmbracoApplicationStoppedNotification : INotification + { } +} diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index db86a1e614..6d5234bbcc 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,4 +1,9 @@ namespace Umbraco.Cms.Core.Notifications { - public class UmbracoApplicationStoppingNotification : INotification { } + /// + /// Notification that occurs when Umbraco is shutting down (after all s are terminated). + /// + /// + public class UmbracoApplicationStoppingNotification : INotification + { } } diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 2b169f3603..6fc1db3208 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -21,6 +21,7 @@ namespace Umbraco.Cms.Core.Packaging /// /// Manages the storage of installed/created package definitions /// + [Obsolete("Packages have now been moved to the database instead of local files, please use CreatedPackageSchemaRepository instead")] public class PackagesRepository : ICreatedPackagesRepository { private readonly IContentService _contentService; @@ -92,7 +93,7 @@ namespace Umbraco.Cms.Core.Packaging _tempFolderPath = tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; _packagesFolderPath = packagesFolderPath ?? Constants.SystemDirectories.Packages; - _mediaFolderPath = mediaFolderPath ?? globalSettings.Value.UmbracoMediaPath + "/created-packages"; + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); _parser = new PackageDefinitionXmlParser(); _mediaService = mediaService; @@ -744,5 +745,13 @@ namespace Umbraco.Cms.Core.Packaging var packagesXml = XDocument.Load(packagesFile); return packagesXml; } + + public void DeleteLocalRepositoryFiles() + { + var packagesFile = _hostingEnvironment.MapPathContentRoot(CreatedPackagesFile); + File.Delete(packagesFile); + var packagesFolder = _hostingEnvironment.MapPathContentRoot(_packagesFolderPath); + Directory.Delete(packagesFolder); + } } } diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs index 680eee5ba2..de5b8c04ae 100644 --- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs +++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs @@ -55,6 +55,7 @@ namespace Umbraco.Cms.Core public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node"; public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission"; public const string ExternalLogin = TableNamePrefix + "ExternalLogin"; + public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin"; public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken"; public const string Macro = /*TableNamePrefix*/ "cms" + "Macro"; @@ -81,6 +82,8 @@ namespace Umbraco.Cms.Core public const string UserLogin = TableNamePrefix + "UserLogin"; public const string LogViewerQuery = TableNamePrefix + "LogViewerQuery"; + + public const string CreatedPackageSchema = TableNamePrefix + "CreatedPackageSchema"; } } } diff --git a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs b/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs index 7c08189d74..bd95776dea 100644 --- a/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs +++ b/src/Umbraco.Core/Persistence/Constants-DbProviderNames.cs @@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core { public static class DbProviderNames { - public const string SqlServer = "System.Data.SqlClient"; + public const string SqlServer = "Microsoft.Data.SqlClient"; public const string SqlCe = "System.Data.SqlServerCe.4.0"; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs index a685ab67f1..7d9594a3c6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs @@ -1,15 +1,19 @@ +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Security; namespace Umbraco.Cms.Core.Persistence.Repositories { + public interface IExternalLoginRepository : IReadWriteQueryRepository, IQueryRepository { + /// /// Replaces all external login providers for the user /// /// /// + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] void Save(int userId, IEnumerable logins); /// @@ -17,8 +21,9 @@ namespace Umbraco.Cms.Core.Persistence.Repositories /// /// /// + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] void Save(int userId, IEnumerable tokens); - + [Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")] void DeleteUserLogins(int memberId); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs new file mode 100644 index 0000000000..0a4b9e76cf --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginWithKeyRepository.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + + /// + /// Repository for external logins with Guid as key, so it can be shared for members and users + /// + public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository, IQueryRepository + { + /// + /// Replaces all external login providers for the user/member key + /// + void Save(Guid userOrMemberKey, IEnumerable logins); + + /// + /// Replaces all external login provider tokens for the providers specified for the user/member key + /// + void Save(Guid userOrMemberKey, IEnumerable tokens); + + /// + /// Deletes all external logins for the specified the user/member key + /// + void DeleteUserLogins(Guid userOrMemberKey); + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs new file mode 100644 index 0000000000..63622f8e82 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ITwoFactorLoginRepository.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + public interface ITwoFactorLoginRepository: IReadRepository, IWriteRepository + { + Task DeleteUserLoginsAsync(Guid userOrMemberKey); + Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName); + + Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey); + } + +} diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 07607be2b0..6d3e40067e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -197,6 +197,7 @@ namespace Umbraco.Cms.Core.PropertyEditors default: throw new ArgumentOutOfRangeException(); } + return value.TryConvertTo(valueType); } @@ -232,6 +233,7 @@ namespace Umbraco.Cms.Core.PropertyEditors StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); return null; } + return result.Result; } diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 2a7bc97650..cc86fff004 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -156,7 +156,7 @@ namespace Umbraco.Cms.Core.Routing : 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 || culture is null) + if (domainUri is not null || culture == defaultCulture || string.IsNullOrEmpty(culture)) { var url = AssembleUrl(domainUri, path, current, mode).ToString(); return UrlInfo.Url(url, culture); diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index a9564712c3..6c45e4d969 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -25,7 +25,7 @@ namespace Umbraco.Cms.Core.Runtime // ensure we have some essential directories // every other component can then initialize safely _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data)); - _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath)); + _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MvcViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews)); _ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials)); diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index ec4e56df1b..08d11db5cd 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -154,11 +154,11 @@ namespace Umbraco.Cms.Core.Runtime // the handler is not installed so that would be the hosting environment if (_signaled) { - _logger.LogInformation("Cannot acquire (signaled)."); + _logger.LogInformation("Cannot acquire MainDom (signaled)."); return false; } - _logger.LogInformation("Acquiring."); + _logger.LogInformation("Acquiring MainDom."); // Get the lock var acquired = false; @@ -168,12 +168,12 @@ namespace Umbraco.Cms.Core.Runtime } catch (Exception ex) { - _logger.LogError(ex, "Error while acquiring"); + _logger.LogError(ex, "Error while acquiring MainDom"); } if (!acquired) { - _logger.LogInformation("Cannot acquire (timeout)."); + _logger.LogInformation("Cannot acquire MainDom (timeout)."); // In previous versions we'd let a TimeoutException be thrown // and the appdomain would not start. We have the opportunity to allow it to @@ -209,7 +209,7 @@ namespace Umbraco.Cms.Core.Runtime _logger.LogWarning(ex, ex.Message); } - _logger.LogInformation("Acquired."); + _logger.LogInformation("Acquired MainDom."); return true; } diff --git a/src/Umbraco.Core/Security/IIdentityUserLogin.cs b/src/Umbraco.Core/Security/IIdentityUserLogin.cs index 67ca739509..4e18771a17 100644 --- a/src/Umbraco.Core/Security/IIdentityUserLogin.cs +++ b/src/Umbraco.Core/Security/IIdentityUserLogin.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security string ProviderKey { get; set; } /// - /// Gets or sets user Id for the user who owns this login + /// Gets or sets user or member key (Guid) for the user/member who owns this login /// string UserId { get; set; } // TODO: This should be able to be used by both users and members diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs index 0117a5cf8f..291464f16f 100644 --- a/src/Umbraco.Core/Services/ExternalLoginService.cs +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -1,44 +1,77 @@ +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services { - public class ExternalLoginService : RepositoryService, IExternalLoginService + public class ExternalLoginService : RepositoryService, IExternalLoginService, IExternalLoginWithKeyService { - private readonly IExternalLoginRepository _externalLoginRepository; + private readonly IExternalLoginWithKeyRepository _externalLoginRepository; public ExternalLoginService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, - IExternalLoginRepository externalLoginRepository) + IExternalLoginWithKeyRepository externalLoginRepository) : base(provider, loggerFactory, eventMessagesFactory) { _externalLoginRepository = externalLoginRepository; } + [Obsolete("Use ctor injecting IExternalLoginWithKeyRepository")] + public ExternalLoginService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, + IExternalLoginRepository externalLoginRepository) + : this(provider, loggerFactory, eventMessagesFactory, StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// + [Obsolete("Use overload that takes a user/member key (Guid).")] public IEnumerable GetExternalLogins(int userId) + => GetExternalLogins(userId.ToGuid()); + + /// + [Obsolete("Use overload that takes a user/member key (Guid).")] + public IEnumerable GetExternalLoginTokens(int userId) => + GetExternalLoginTokens(userId.ToGuid()); + + /// + [Obsolete("Use overload that takes a user/member key (Guid).")] + public void Save(int userId, IEnumerable logins) + => Save(userId.ToGuid(), logins); + + /// + [Obsolete("Use overload that takes a user/member key (Guid).")] + public void Save(int userId, IEnumerable tokens) + => Save(userId.ToGuid(), tokens); + + /// + [Obsolete("Use overload that takes a user/member key (Guid).")] + public void DeleteUserLogins(int userId) + => DeleteUserLogins(userId.ToGuid()); + + /// + public IEnumerable GetExternalLogins(Guid userOrMemberKey) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - // TODO: This is temp until we update the external service to support guids for both users and members - var asString = userId.ToString(CultureInfo.InvariantCulture); - return _externalLoginRepository.Get(Query().Where(x => x.UserId == asString)) + return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey)) .ToList(); } } - public IEnumerable GetExternalLoginTokens(int userId) + /// + public IEnumerable GetExternalLoginTokens(Guid userOrMemberKey) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - // TODO: This is temp until we update the external service to support guids for both users and members - var asString = userId.ToString(CultureInfo.InvariantCulture); - return _externalLoginRepository.Get(Query().Where(x => x.UserId == asString)) + return _externalLoginRepository.Get(Query().Where(x => x.Key == userOrMemberKey)) .ToList(); } } @@ -55,30 +88,31 @@ namespace Umbraco.Cms.Core.Services } /// - public void Save(int userId, IEnumerable logins) + public void Save(Guid userOrMemberKey, IEnumerable logins) { using (var scope = ScopeProvider.CreateScope()) { - _externalLoginRepository.Save(userId, logins); - scope.Complete(); - } - } - - public void Save(int userId, IEnumerable tokens) - { - using (var scope = ScopeProvider.CreateScope()) - { - _externalLoginRepository.Save(userId, tokens); + _externalLoginRepository.Save(userOrMemberKey, logins); scope.Complete(); } } /// - public void DeleteUserLogins(int userId) + public void Save(Guid userOrMemberKey, IEnumerable tokens) { using (var scope = ScopeProvider.CreateScope()) { - _externalLoginRepository.DeleteUserLogins(userId); + _externalLoginRepository.Save(userOrMemberKey, tokens); + scope.Complete(); + } + } + + /// + public void DeleteUserLogins(Guid userOrMemberKey) + { + using (var scope = ScopeProvider.CreateScope()) + { + _externalLoginRepository.DeleteUserLogins(userOrMemberKey); scope.Complete(); } } diff --git a/src/Umbraco.Core/Services/IConflictingRouteService.cs b/src/Umbraco.Core/Services/IConflictingRouteService.cs new file mode 100644 index 0000000000..04d81d7f88 --- /dev/null +++ b/src/Umbraco.Core/Services/IConflictingRouteService.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services +{ + public interface IConflictingRouteService + { + public bool HasConflictingRoutes(out string controllerName); + } +} diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs index 787631d500..75f8069f0c 100644 --- a/src/Umbraco.Core/Services/IExternalLoginService.cs +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Security; @@ -6,6 +7,7 @@ namespace Umbraco.Cms.Core.Services /// /// Used to store the external login info /// + [Obsolete("Use IExternalLoginServiceWithKey. This will be removed in Umbraco 10")] public interface IExternalLoginService : IService { /// diff --git a/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs new file mode 100644 index 0000000000..bc31f54f8b --- /dev/null +++ b/src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Core.Services +{ + public interface IExternalLoginWithKeyService : IService + { + /// + /// Returns all user logins assigned + /// + IEnumerable GetExternalLogins(Guid userOrMemberKey); + + /// + /// Returns all user login tokens assigned + /// + IEnumerable GetExternalLoginTokens(Guid userOrMemberKey); + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an administrator has been editing/removing members + /// + IEnumerable Find(string loginProvider, string providerKey); + + /// + /// Saves the external logins associated with the user + /// + /// + /// The user or member key associated with the logins + /// + /// + /// + /// This will replace all external login provider information for the user + /// + void Save(Guid userOrMemberKey, IEnumerable logins); + + /// + /// Saves the external login tokens associated with the user + /// + /// + /// The user or member key associated with the logins + /// + /// + /// + /// This will replace all external login tokens for the user + /// + void Save(Guid userOrMemberKey,IEnumerable tokens); + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + void DeleteUserLogins(Guid userOrMemberKey); + } +} diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs new file mode 100644 index 0000000000..dd11f864fb --- /dev/null +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface ITwoFactorLoginService : IService + { + /// + /// 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); + + Task GetSetupInfoAsync(Guid userOrMemberKey, string providerName); + + IEnumerable GetAllProviderNames(); + Task DisableAsync(Guid userOrMemberKey, string providerName); + + bool ValidateTwoFactorSetup(string providerName, string secret, string code); + Task SaveAsync(TwoFactorLogin twoFactorLogin); + Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); + } + +} diff --git a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs index cf5e71a568..b0f0a9b003 100644 --- a/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs +++ b/src/Umbraco.Core/Strings/DefaultShortStringHelperConfig.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Configuration.UmbracoSettings; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Strings @@ -60,7 +61,9 @@ namespace Umbraco.Cms.Core.Strings /// The short string helper. public DefaultShortStringHelperConfig WithDefault(RequestHandlerSettings requestHandlerSettings) { - UrlReplaceCharacters = requestHandlerSettings.CharCollection + IEnumerable charCollection = requestHandlerSettings.GetCharReplacements(); + + UrlReplaceCharacters = charCollection .Where(x => string.IsNullOrEmpty(x.Char) == false) .ToDictionary(x => x.Char, x => x.Replacement); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9a1da38222..8867a6c9fb 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -15,23 +15,24 @@ - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs index 8454bbdf31..ccb515182e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.FileSystems.cs @@ -49,7 +49,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection ILogger logger = factory.GetRequiredService>(); GlobalSettings globalSettings = factory.GetRequiredService>().Value; - var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPath); + var rootPath = hostingEnvironment.MapPathWebRoot(globalSettings.UmbracoMediaPhysicalRootPath); var rootUrl = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); return new PhysicalFileSystem(ioHelper, hostingEnvironment, logger, rootPath, rootUrl); }); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index e157f929b3..511c09304d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,5 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Extensions; @@ -30,7 +32,10 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddMultipleUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(factory => factory.GetRequiredService()); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index ff538bb9f6..cf2dde2bea 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -8,13 +8,19 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -38,17 +44,32 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(factory => new ExternalLoginService( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService() + )); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(factory => factory.GetRequiredService()); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddTransient(SourcesFactory); - builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(factory => CreatePackageRepository(factory, "createdPackages.config")); + builder.Services.AddUnique(); builder.Services.AddSingleton(); builder.Services.AddUnique(); return builder; } - /// - /// Creates an instance of PackagesRepository for either the ICreatedPackagesRepository or the IInstalledPackagesRepository - /// private static PackagesRepository CreatePackageRepository(IServiceProvider factory, string packageRepoFileName) => new PackagesRepository( factory.GetRequiredService(), diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 0ad2271d7e..1d228ebf98 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -44,7 +44,7 @@ namespace Umbraco.Cms.Infrastructure.Install hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), - hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) }; _packagesPermissionsDirs = new[] @@ -70,7 +70,7 @@ namespace Umbraco.Cms.Infrastructure.Install EnsureFiles(_permissionFiles, out errors); report[FilePermissionTest.FileWriting] = errors.ToList(); - EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), out errors); + EnsureCanCreateSubDirectory(_hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPhysicalRootPath), out errors); report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); return report.Sum(x => x.Value.Count()) == 0; diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs index b3db06fd5b..52c86f9ccf 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs @@ -60,6 +60,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install typeof(CacheInstructionDto), typeof(ExternalLoginDto), typeof(ExternalLoginTokenDto), + typeof(TwoFactorLoginDto), typeof(RedirectUrlDto), typeof(LockDto), typeof(UserGroupDto), @@ -78,7 +79,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install typeof(ContentScheduleDto), typeof(LogViewerQueryDto), typeof(ContentVersionCleanupPolicyDto), - typeof(UserGroup2NodeDto) + typeof(UserGroup2NodeDto), + typeof(CreatedPackageSchemaDto) }; private readonly IUmbracoDatabase _database; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 257fee9967..2080034554 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade @@ -267,6 +268,14 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // TO 9.2.0 To("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}"); + To("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}"); + + + + // TO 9.3.0 + To("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}"); + To("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}"); + To("{0828F206-DCF7-4F73-ABBB-6792275532EB}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs new file mode 100644 index 0000000000..3bc62ab42e --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_2_0/AddDefaultForNotificationsToggle.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0 +{ + public class AddDefaultForNotificationsToggle : MigrationBase + { + public AddDefaultForNotificationsToggle(IMigrationContext context) + : base(context) + { } + + protected override void Migrate() + { + var updateSQL = Sql($"UPDATE {Constants.DatabaseSchema.Tables.UserGroup} SET userGroupDefaultPermissions = userGroupDefaultPermissions + 'N' WHERE userGroupAlias IN ('admin', 'writer', 'editor')"); + Execute.Sql(updateSQL.SQL).Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs new file mode 100644 index 0000000000..c5e569282a --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/AddTwoFactorLoginTable.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +{ + public class AddTwoFactorLoginTable : MigrationBase + { + public AddTwoFactorLoginTable(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (tables.InvariantContains(TwoFactorLoginDto.TableName)) + { + return; + } + + Create.Table().Do(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs new file mode 100644 index 0000000000..3d003eb31d --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/MovePackageXMLToDb.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +{ + public class MovePackageXMLToDb : MigrationBase + { + private readonly PackagesRepository _packagesRepository; + private readonly PackageDefinitionXmlParser _xmlParser; + + /// + /// Initializes a new instance of the class. + /// + public MovePackageXMLToDb(IMigrationContext context, PackagesRepository packagesRepository) + : base(context) + { + _packagesRepository = packagesRepository; + _xmlParser = new PackageDefinitionXmlParser(); + } + + private void CreateDatabaseTable() + { + // Add CreatedPackage table in database if it doesn't exist + IEnumerable tables = SqlSyntax.GetTablesInSchema(Context.Database); + if (!tables.InvariantContains(CreatedPackageSchemaDto.TableName)) + { + Create.Table().Do(); + } + } + + private void MigrateCreatedPackageFilesToDb() + { + // Load data from file + IEnumerable packages = _packagesRepository.GetAll(); + var createdPackageDtos = new List(); + foreach (PackageDefinition package in packages) + { + // Create dto from xmlDocument + var dto = new CreatedPackageSchemaDto() + { + Name = package.Name, + Value = _xmlParser.ToXml(package).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() + }; + createdPackageDtos.Add(dto); + } + + _packagesRepository.DeleteLocalRepositoryFiles(); + if (createdPackageDtos.Any()) + { + // Insert dto into CreatedPackage table + Database.InsertBulk(createdPackageDtos); + } + } + + /// + protected override void Migrate() + { + CreateDatabaseTable(); + MigrateCreatedPackageFilesToDb(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs new file mode 100644 index 0000000000..4c7104e762 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_3_0/UpdateExternalLoginToUseKeyInsteadOfId.cs @@ -0,0 +1,77 @@ +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0 +{ + public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase + { + public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context) + { + } + + protected override void Migrate() + { + if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey")) + { + var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider"; + var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId"; + + if (IndexExists(indexNameToRecreate)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do(); + } + + if (IndexExists(indexNameToDelete)) + { + // drop it since the previous migration index was wrong, and we + // need to modify a column that belons to it + Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do(); + } + + //special trick to add the column without constraints and return the sql to add them later + AddColumn("userOrMemberKey", out var sqls); + + + if (DatabaseType.IsSqlCe()) + { + var userIds = Database.Fetch(Sql().Select("userId").From()); + + foreach (int userId in userIds) + { + Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = '{userId.ToGuid()}' WHERE userId = {userId}").Do(); + } + } + else + { + //populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid. + Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do(); + + } + + //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls) Execute.Sql(sql).Do(); + + //now remove these old columns + Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do(); + + // create index with the correct definition + Create + .Index(indexNameToRecreate) + .OnTable(ExternalLoginDto.TableName) + .OnColumn("loginProvider").Ascending() + .OnColumn("userOrMemberKey").Ascending() + .WithOptions() + .Unique() + .WithOptions() + .NonClustered() + .Do(); + } + } + + + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs b/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs index 61db41a20a..42c3ff1865 100644 --- a/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs +++ b/src/Umbraco.Infrastructure/Persistence/BulkDataReader.cs @@ -1,15 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Diagnostics; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.Data.SqlClient; namespace Umbraco.Cms.Infrastructure.Persistence { diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs new file mode 100644 index 0000000000..37e6fd8d8d --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/CreatedPackageSchemaDto.cs @@ -0,0 +1,38 @@ +using System; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +{ + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("id")] + public class CreatedPackageSchemaDto + { + public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.CreatedPackageSchema; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("name")] + [Length(255)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "name", Name = "IX_" + TableName + "_Name")] + public string Name { get; set; } + + [Column("value")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Value { get; set; } + + [Column("updateDate")] + [Constraint(Default = SystemMethods.CurrentDateTime)] + public DateTime UpdateDate { get; set; } + + [Column("packageId")] + [NullSetting(NullSetting = NullSettings.NotNull)] + public Guid PackageId { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs index 69bf1b837e..0af1ff83c5 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ExternalLoginDto.cs @@ -16,13 +16,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [PrimaryKeyColumn] public int Id { get; set; } - // TODO: This is completely missing a FK!!? ... IIRC that is because we want to change this to a GUID - // to support both members and users for external logins and that will not have any referential integrity - // This should be part of the members task for enabling external logins. + [Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")] + [ResultColumn("userId")] + public int? UserId { get; set; } - [Column("userId")] + [Column("userOrMemberKey")] [Index(IndexTypes.NonClustered)] - public int UserId { get; set; } + public Guid UserOrMemberKey { get; set; } /// /// Used to store the name of the provider (i.e. Facebook, Google) @@ -30,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Column("loginProvider")] [Length(400)] [NullSetting(NullSetting = NullSettings.NotNull)] - [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userId", Name = "IX_" + TableName + "_LoginProvider")] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")] public string LoginProvider { get; set; } /// diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs new file mode 100644 index 0000000000..1202fe2a19 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/TwoFactorLoginDto.cs @@ -0,0 +1,33 @@ +using System; +using NPoco; +using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos +{ + [TableName(TableName)] + [ExplicitColumns] + [PrimaryKey("Id")] + internal class TwoFactorLoginDto + { + public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TwoFactorLogin; + + [Column("id")] + [PrimaryKeyColumn] + public int Id { get; set; } + + [Column("userOrMemberKey")] + [Index(IndexTypes.NonClustered)] + public Guid UserOrMemberKey { get; set; } + + [Column("providerName")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + [Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", Name = "IX_" + TableName + "_ProviderName")] + public string ProviderName { get; set; } + + [Column("secret")] + [Length(400)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string Secret { get; set; } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs index 82bbb4a40a..1c74dcb8bd 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/ExternalLoginFactory.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Factories { @@ -9,7 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories { public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto) { - var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserId.ToString(CultureInfo.InvariantCulture), dto.CreateDate); + var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); @@ -18,7 +19,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto) { - var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId.ToString(CultureInfo.InvariantCulture), dto.CreateDate) + + //If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state. + //At this point we have to manually set the key, to ensure external logins can be used to upgrade + var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString(); + + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate) { UserData = dto.UserData }; @@ -36,19 +42,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories CreateDate = entity.CreateDate, LoginProvider = entity.LoginProvider, ProviderKey = entity.ProviderKey, - UserId = int.Parse(entity.UserId, CultureInfo.InvariantCulture), // TODO: This is temp until we change the ext logins to use GUIDs + UserOrMemberKey = entity.Key, UserData = entity.UserData }; return dto; } - public static ExternalLoginDto BuildDto(int userId, IExternalLogin entity, int? id = null) + public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null) { var dto = new ExternalLoginDto { Id = id ?? default, - UserId = userId, + UserOrMemberKey = userOrMemberKey, LoginProvider = entity.LoginProvider, ProviderKey = entity.ProviderKey, UserData = entity.UserData, diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs index 004ec1f9b2..8cd01e706c 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/NetworkConnectivityErrorDetectionStrategy.cs @@ -1,5 +1,5 @@ using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies { diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 37968c4376..faf6442333 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -1,5 +1,5 @@ using System; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies { @@ -104,7 +104,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies // Resource ID: %d. The %s limit for the database is %d and has been reached. case 10928: // SQL Error Code: 10929 - // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. // However, the server is currently too busy to support requests greater than %d for this database. case 10929: // SQL Error Code: 10053 @@ -112,14 +112,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies // An established connection was aborted by the software in your host machine. case 10053: // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. + // A transport-level error has occurred when sending the request to the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 10054: // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed // because connected host has failed to respond.)"} case 10060: // SQL Error Code: 40197 @@ -129,21 +129,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling.Strategies // The service has encountered an error processing your request. Please try again. case 40540: // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer // support, and provide them the session tracing ID of ZZZZZ. case 40613: // SQL Error Code: 40143 // The service has encountered an error processing your request. Please try again. case 40143: // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 233: // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) case 64: // DBNETLIB Error Code: 20 // The instance of SQL Server you attempted to connect to does not support encryption. diff --git a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs index 9155937fe0..96d42a9481 100644 --- a/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs +++ b/src/Umbraco.Infrastructure/Persistence/FaultHandling/ThrottlingCondition.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.Data.SqlClient; using System.Globalization; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Data.SqlClient; namespace Umbraco.Cms.Infrastructure.Persistence.FaultHandling { diff --git a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs index c51344a342..fe616b56f6 100644 --- a/src/Umbraco.Infrastructure/Persistence/LocalDb.cs +++ b/src/Umbraco.Infrastructure/Persistence/LocalDb.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.Data.SqlClient; namespace Umbraco.Cms.Infrastructure.Persistence { diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs index 2d47746baa..85db7bf553 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginMapper.cs @@ -18,7 +18,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers DefineMap(nameof(IdentityUserLogin.CreateDate), nameof(ExternalLoginDto.CreateDate)); DefineMap(nameof(IdentityUserLogin.LoginProvider), nameof(ExternalLoginDto.LoginProvider)); DefineMap(nameof(IdentityUserLogin.ProviderKey), nameof(ExternalLoginDto.ProviderKey)); - DefineMap(nameof(IdentityUserLogin.UserId), nameof(ExternalLoginDto.UserId)); + DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); + DefineMap(nameof(IdentityUserLogin.UserData), nameof(ExternalLoginDto.UserData)); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs index 4d03031ffd..ca8360c626 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/ExternalLoginTokenMapper.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers DefineMap(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name)); DefineMap(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value)); // separate table - DefineMap(nameof(IdentityUserToken.UserId), nameof(ExternalLoginDto.UserId)); + DefineMap(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey)); } } } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs index f07867cccc..c53076ff18 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions-Bulk.cs @@ -2,9 +2,10 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; -using System.Data.SqlClient; using System.Linq; +using Microsoft.Data.SqlClient; using NPoco; +using NPoco.SqlServer; using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 813eea58ef..0159245bfd 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Text.RegularExpressions; +using Microsoft.Data.SqlClient; using NPoco; using StackExchange.Profiling.Data; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs new file mode 100644 index 0000000000..be1a31c2c9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/CreatedPackageSchemaRepository.cs @@ -0,0 +1,758 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml.Linq; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; +using File = System.IO.File; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + /// + public class CreatedPackageSchemaRepository : ICreatedPackagesRepository + { + private readonly PackageDefinitionXmlParser _xmlParser; + private readonly IUmbracoDatabase _umbracoDatabase; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly FileSystems _fileSystems; + private readonly IEntityXmlSerializer _serializer; + private readonly IDataTypeService _dataTypeService; + private readonly ILocalizationService _localizationService; + private readonly IFileService _fileService; + private readonly IMediaService _mediaService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IContentService _contentService; + private readonly MediaFileManager _mediaFileManager; + private readonly IMacroService _macroService; + private readonly IContentTypeService _contentTypeService; + private readonly string _tempFolderPath; + private readonly string _mediaFolderPath; + + /// + /// Initializes a new instance of the class. + /// + public CreatedPackageSchemaRepository( + IUmbracoDatabase umbracoDatabase, + IHostingEnvironment hostingEnvironment, + IOptions globalSettings, + FileSystems fileSystems, + IEntityXmlSerializer serializer, + IDataTypeService dataTypeService, + ILocalizationService localizationService, + IFileService fileService, + IMediaService mediaService, + IMediaTypeService mediaTypeService, + IContentService contentService, + MediaFileManager mediaFileManager, + IMacroService macroService, + IContentTypeService contentTypeService, + string mediaFolderPath = null, + string tempFolderPath = null) + { + _umbracoDatabase = umbracoDatabase; + _hostingEnvironment = hostingEnvironment; + _fileSystems = fileSystems; + _serializer = serializer; + _dataTypeService = dataTypeService; + _localizationService = localizationService; + _fileService = fileService; + _mediaService = mediaService; + _mediaTypeService = mediaTypeService; + _contentService = contentService; + _mediaFileManager = mediaFileManager; + _macroService = macroService; + _contentTypeService = contentTypeService; + _xmlParser = new PackageDefinitionXmlParser(); + _mediaFolderPath = mediaFolderPath ?? Path.Combine(globalSettings.Value.UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.CreatedPackages); + _tempFolderPath = + tempFolderPath ?? Constants.SystemDirectories.TempData.EnsureEndsWith('/') + "PackageFiles"; + } + + public IEnumerable GetAll() + { + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Select() + .From() + .OrderBy(x => x.Id); + + var packageDefinitions = new List(); + + List xmlSchemas = _umbracoDatabase.Fetch(query); + foreach (CreatedPackageSchemaDto packageSchema in xmlSchemas) + { + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + packageDefinitions.Add(packageDefinition); + } + + return packageDefinitions; + } + + public PackageDefinition GetById(int id) + { + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Select() + .From() + .Where(x => x.Id == id); + List schemaDtos = _umbracoDatabase.Fetch(query); + + if (schemaDtos.IsCollectionEmpty()) + { + return null; + } + + var packageSchema = schemaDtos.First(); + var packageDefinition = _xmlParser.ToPackageDefinition(XElement.Parse(packageSchema.Value)); + packageDefinition.Id = packageSchema.Id; + packageDefinition.Name = packageSchema.Name; + packageDefinition.PackageId = packageSchema.PackageId; + return packageDefinition; + } + + public void Delete(int id) + { + // Delete package snapshot + var packageDef = GetById(id); + if (File.Exists(packageDef.PackagePath)) + { + File.Delete(packageDef.PackagePath); + } + + Sql query = new Sql(_umbracoDatabase.SqlContext) + .Delete() + .Where(x => x.Id == id); + + _umbracoDatabase.Delete(query); + } + + public bool SavePackage(PackageDefinition definition) + { + if (definition == null) + { + throw new NullReferenceException("PackageDefinition cannot be null when saving"); + } + + if (definition.Name == null || string.IsNullOrEmpty(definition.Name) || definition.PackagePath == null) + { + return false; + } + + // Ensure it's valid + ValidatePackage(definition); + + + if (definition.Id == default) + { + // Create dto from definition + var dto = new CreatedPackageSchemaDto() + { + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + UpdateDate = DateTime.Now, + PackageId = Guid.NewGuid() + }; + + // Set the ids, we have to save in database first to get the Id + definition.PackageId = dto.PackageId; + var result = _umbracoDatabase.Insert(dto); + var decimalResult = result.SafeCast(); + definition.Id = decimal.ToInt32(decimalResult); + } + + // Save snapshot locally, we do this to the updated packagePath + ExportPackage(definition); + // Create dto from definition + var updatedDto = new CreatedPackageSchemaDto() + { + Name = definition.Name, + Value = _xmlParser.ToXml(definition).ToString(), + Id = definition.Id, + PackageId = definition.PackageId, + UpdateDate = DateTime.Now + }; + _umbracoDatabase.Update(updatedDto); + + return true; + } + + public string ExportPackage(PackageDefinition definition) + { + + // Ensure it's valid + ValidatePackage(definition); + + // Create a folder for building this package + var temporaryPath = + _hostingEnvironment.MapPathContentRoot(_tempFolderPath.EnsureEndsWith('/') + Guid.NewGuid()); + if (Directory.Exists(temporaryPath) == false) + { + Directory.CreateDirectory(temporaryPath); + } + + try + { + // Init package file + XDocument compiledPackageXml = CreateCompiledPackageXml(out XElement root); + + // Info section + root.Add(GetPackageInfoXml(definition)); + + PackageDocumentsAndTags(definition, root); + PackageDocumentTypes(definition, root); + PackageMediaTypes(definition, root); + PackageTemplates(definition, root); + PackageStylesheets(definition, root); + PackageStaticFiles(definition.Scripts, root, "Scripts", "Script", _fileSystems.ScriptsFileSystem); + PackageStaticFiles(definition.PartialViews, root, "PartialViews", "View", + _fileSystems.PartialViewsFileSystem); + PackageMacros(definition, root); + PackageDictionaryItems(definition, root); + PackageLanguages(definition, root); + PackageDataTypes(definition, root); + Dictionary mediaFiles = PackageMedia(definition, root); + + string fileName; + string tempPackagePath; + if (mediaFiles.Count > 0) + { + fileName = "package.zip"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + using (var archive = new ZipArchive(fileStream, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry packageXmlEntry = archive.CreateEntry("package.xml"); + using (Stream entryStream = packageXmlEntry.Open()) + { + compiledPackageXml.Save(entryStream); + } + + foreach (KeyValuePair mediaFile in mediaFiles) + { + var entryPath = $"media{mediaFile.Key.EnsureStartsWith('/')}"; + ZipArchiveEntry mediaEntry = archive.CreateEntry(entryPath); + using (Stream entryStream = mediaEntry.Open()) + using (mediaFile.Value) + { + mediaFile.Value.Seek(0, SeekOrigin.Begin); + mediaFile.Value.CopyTo(entryStream); + } + } + } + } + else + { + fileName = "package.xml"; + tempPackagePath = Path.Combine(temporaryPath, fileName); + + using (FileStream fileStream = File.OpenWrite(tempPackagePath)) + { + compiledPackageXml.Save(fileStream); + } + } + + var directoryName = + _hostingEnvironment.MapPathWebRoot( + Path.Combine(_mediaFolderPath, definition.Name.Replace(' ', '_'))); + + if (Directory.Exists(directoryName) == false) + { + Directory.CreateDirectory(directoryName); + } + + var finalPackagePath = Path.Combine(directoryName, fileName); + + if (File.Exists(finalPackagePath)) + { + File.Delete(finalPackagePath); + } + + if (File.Exists(finalPackagePath.Replace("zip", "xml"))) + { + File.Delete(finalPackagePath.Replace("zip", "xml")); + } + + File.Move(tempPackagePath, finalPackagePath); + + definition.PackagePath = finalPackagePath; + + return finalPackagePath; + } + finally + { + // Clean up + Directory.Delete(temporaryPath, true); + } + } + + private XDocument CreateCompiledPackageXml(out XElement root) + { + root = new XElement("umbPackage"); + var compiledPackageXml = new XDocument(root); + return compiledPackageXml; + } + + private void ValidatePackage(PackageDefinition definition) + { + // Ensure it's valid + var context = new ValidationContext(definition, serviceProvider: null, items: null); + var results = new List(); + var isValid = Validator.TryValidateObject(definition, context, results); + if (!isValid) + { + throw new InvalidOperationException("Validation failed, there is invalid data on the model: " + + string.Join(", ", results.Select(x => x.ErrorMessage))); + } + } + + private void PackageDataTypes(PackageDefinition definition, XContainer root) + { + var dataTypes = new XElement("DataTypes"); + foreach (var dtId in definition.DataTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDataType dataType = _dataTypeService.GetDataType(outInt); + if (dataType == null) + { + continue; + } + + dataTypes.Add(_serializer.Serialize(dataType)); + } + + root.Add(dataTypes); + } + + private void PackageLanguages(PackageDefinition definition, XContainer root) + { + var languages = new XElement("Languages"); + foreach (var langId in definition.Languages) + { + if (!int.TryParse(langId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ILanguage lang = _localizationService.GetLanguageById(outInt); + if (lang == null) + { + continue; + } + + languages.Add(_serializer.Serialize(lang)); + } + + root.Add(languages); + } + + private void PackageDictionaryItems(PackageDefinition definition, XContainer root) + { + var rootDictionaryItems = new XElement("DictionaryItems"); + var items = new Dictionary(); + + foreach (var dictionaryId in definition.DictionaryItems) + { + if (!int.TryParse(dictionaryId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IDictionaryItem di = _localizationService.GetDictionaryItemById(outInt); + + if (di == null) + { + continue; + } + + items[di.Key] = (di, _serializer.Serialize(di, false)); + } + + // organize them in hierarchy ... + var itemCount = items.Count; + var processed = new Dictionary(); + while (processed.Count < itemCount) + { + foreach (Guid key in items.Keys.ToList()) + { + (IDictionaryItem dictionaryItem, XElement serializedDictionaryValue) = items[key]; + + if (!dictionaryItem.ParentId.HasValue) + { + // if it has no parent, its definitely just at the root + AppendDictionaryElement(rootDictionaryItems, items, processed, key, serializedDictionaryValue); + } + else + { + if (processed.ContainsKey(dictionaryItem.ParentId.Value)) + { + // we've processed this parent element already so we can just append this xml child to it + AppendDictionaryElement(processed[dictionaryItem.ParentId.Value], items, processed, key, + serializedDictionaryValue); + } + else if (items.ContainsKey(dictionaryItem.ParentId.Value)) + { + // we know the parent exists in the dictionary but + // we haven't processed it yet so we'll leave it for the next loop + continue; + } + else + { + // in this case, the parent of this item doesn't exist in our collection, we have no + // choice but to add it to the root. + AppendDictionaryElement(rootDictionaryItems, items, processed, key, + serializedDictionaryValue); + } + } + } + } + + root.Add(rootDictionaryItems); + + static void AppendDictionaryElement(XElement rootDictionaryItems, + Dictionary items, + Dictionary processed, Guid key, XElement serializedDictionaryValue) + { + // track it + processed.Add(key, serializedDictionaryValue); + + // append it + rootDictionaryItems.Add(serializedDictionaryValue); + + // remove it so its not re-processed + items.Remove(key); + } + } + + private void PackageMacros(PackageDefinition definition, XContainer root) + { + var packagedMacros = new List(); + var macros = new XElement("Macros"); + foreach (var macroId in definition.Macros) + { + if (!int.TryParse(macroId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int outInt)) + { + continue; + } + + XElement macroXml = GetMacroXml(outInt, out IMacro macro); + if (macroXml == null) + { + continue; + } + + macros.Add(macroXml); + packagedMacros.Add(macro); + } + + root.Add(macros); + + // Get the partial views for macros and package those (exclude views outside of the default directory, e.g. App_Plugins\*\Views) + IEnumerable views = packagedMacros + .Where(x => x.MacroSource.StartsWith(Constants.SystemDirectories.MacroPartials)) + .Select(x => + x.MacroSource.Substring(Constants.SystemDirectories.MacroPartials.Length).Replace('/', '\\')); + PackageStaticFiles(views, root, "MacroPartialViews", "View", _fileSystems.MacroPartialsFileSystem); + } + + private void PackageStylesheets(PackageDefinition definition, XContainer root) + { + var stylesheetsXml = new XElement("Stylesheets"); + foreach (var stylesheet in definition.Stylesheets) + { + if (stylesheet.IsNullOrWhiteSpace()) + { + continue; + } + + XElement xml = GetStylesheetXml(stylesheet, true); + if (xml != null) + { + stylesheetsXml.Add(xml); + } + } + + root.Add(stylesheetsXml); + } + + private void PackageStaticFiles( + IEnumerable filePaths, + XContainer root, + string containerName, + string elementName, + IFileSystem fileSystem) + { + var scriptsXml = new XElement(containerName); + foreach (var file in filePaths) + { + if (file.IsNullOrWhiteSpace()) + { + continue; + } + + if (!fileSystem.FileExists(file)) + { + throw new InvalidOperationException("No file found with path " + file); + } + + using (Stream stream = fileSystem.OpenFile(file)) + using (var reader = new StreamReader(stream)) + { + var fileContents = reader.ReadToEnd(); + scriptsXml.Add( + new XElement( + elementName, + new XAttribute("path", file), + new XCData(fileContents))); + } + } + + root.Add(scriptsXml); + } + + private void PackageTemplates(PackageDefinition definition, XContainer root) + { + var templatesXml = new XElement("Templates"); + foreach (var templateId in definition.Templates) + { + if (!int.TryParse(templateId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + ITemplate template = _fileService.GetTemplate(outInt); + if (template == null) + { + continue; + } + + templatesXml.Add(_serializer.Serialize(template)); + } + + root.Add(templatesXml); + } + + private void PackageDocumentTypes(PackageDefinition definition, XContainer root) + { + var contentTypes = new HashSet(); + var docTypesXml = new XElement("DocumentTypes"); + foreach (var dtId in definition.DocumentTypes) + { + if (!int.TryParse(dtId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IContentType contentType = _contentTypeService.Get(outInt); + if (contentType == null) + { + continue; + } + + AddDocumentType(contentType, contentTypes); + } + + foreach (IContentType contentType in contentTypes) + { + docTypesXml.Add(_serializer.Serialize(contentType)); + } + + root.Add(docTypesXml); + } + + private void PackageMediaTypes(PackageDefinition definition, XContainer root) + { + var mediaTypes = new HashSet(); + var mediaTypesXml = new XElement("MediaTypes"); + foreach (var mediaTypeId in definition.MediaTypes) + { + if (!int.TryParse(mediaTypeId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var outInt)) + { + continue; + } + + IMediaType mediaType = _mediaTypeService.Get(outInt); + if (mediaType == null) + { + continue; + } + + AddMediaType(mediaType, mediaTypes); + } + + foreach (IMediaType mediaType in mediaTypes) + { + mediaTypesXml.Add(_serializer.Serialize(mediaType)); + } + + root.Add(mediaTypesXml); + } + + private void PackageDocumentsAndTags(PackageDefinition definition, XContainer root) + { + // Documents and tags + if (string.IsNullOrEmpty(definition.ContentNodeId) == false && int.TryParse(definition.ContentNodeId, + NumberStyles.Integer, CultureInfo.InvariantCulture, out var contentNodeId)) + { + if (contentNodeId > 0) + { + // load content from umbraco. + IContent content = _contentService.GetById(contentNodeId); + if (content != null) + { + var contentXml = definition.ContentLoadChildNodes + ? content.ToDeepXml(_serializer) + : content.ToXml(_serializer); + + // Create the Documents/DocumentSet node + + root.Add( + new XElement( + "Documents", + new XElement( + "DocumentSet", + new XAttribute("importMode", "root"), + contentXml))); + } + } + } + } + + private Dictionary PackageMedia(PackageDefinition definition, XElement root) + { + var mediaStreams = new Dictionary(); + + // callback that occurs on each serialized media item + void OnSerializedMedia(IMedia media, XElement xmlMedia) + { + // get the media file path and store that separately in the XML. + // the media file path is different from the URL and is specifically + // extracted using the property editor for this media file and the current media file system. + Stream mediaStream = _mediaFileManager.GetFile(media, out var mediaFilePath); + if (mediaStream != null) + { + xmlMedia.Add(new XAttribute("mediaFilePath", mediaFilePath)); + + // add the stream to our outgoing stream + mediaStreams.Add(mediaFilePath, mediaStream); + } + } + + IEnumerable medias = _mediaService.GetByIds(definition.MediaUdis); + + var mediaXml = new XElement( + "MediaItems", + medias.Select(media => + { + XElement serializedMedia = _serializer.Serialize( + media, + definition.MediaLoadChildNodes, + OnSerializedMedia); + + return new XElement("MediaSet", serializedMedia); + })); + + root.Add(mediaXml); + + return mediaStreams; + } + + /// + /// Gets a macros xml node + /// + private XElement GetMacroXml(int macroId, out IMacro macro) + { + macro = _macroService.GetById(macroId); + if (macro == null) + { + return null; + } + + XElement xml = _serializer.Serialize(macro); + return xml; + } + + /// + /// Converts a umbraco stylesheet to a package xml node + /// + /// The path of the stylesheet. + /// if set to true [include properties]. + private XElement GetStylesheetXml(string path, bool includeProperties) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(path)); + } + + IStylesheet stylesheet = _fileService.GetStylesheet(path); + if (stylesheet == null) + { + return null; + } + + return _serializer.Serialize(stylesheet, includeProperties); + } + + private void AddDocumentType(IContentType dt, HashSet dtl) + { + if (dt.ParentId > 0) + { + IContentType parent = _contentTypeService.Get(dt.ParentId); + if (parent != null) + { + AddDocumentType(parent, dtl); + } + } + + if (!dtl.Contains(dt)) + { + dtl.Add(dt); + } + } + + private void AddMediaType(IMediaType mediaType, HashSet mediaTypes) + { + if (mediaType.ParentId > 0) + { + IMediaType parent = _mediaTypeService.Get(mediaType.ParentId); + if (parent != null) + { + AddMediaType(parent, mediaTypes); + } + } + + if (!mediaTypes.Contains(mediaType)) + { + mediaTypes.Add(mediaType); + } + } + + private static XElement GetPackageInfoXml(PackageDefinition definition) + { + var info = new XElement("info"); + + // Package info + var package = new XElement("package"); + package.Add(new XElement("name", definition.Name)); + info.Add(package); + return info; + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs index 60fa5e02e7..955cbf5d5d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ExternalLoginRepository.cs @@ -6,7 +6,6 @@ using NPoco; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Models.Entities; -using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Security; @@ -18,22 +17,34 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement { - // TODO: We should update this to support both users and members. It means we would remove referential integrity from users - // and the user/member key would be a GUID (we also need to add a GUID to users) - internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository + internal class ExternalLoginRepository : EntityRepositoryBase, IExternalLoginRepository, IExternalLoginWithKeyRepository { public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger) : base(scopeAccessor, cache, logger) { } - public void DeleteUserLogins(int memberId) => Database.Delete("WHERE userId=@userId", new { userId = memberId }); + /// + [Obsolete("Use method that takes guid as param")] + public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid()); - public void Save(int userId, IEnumerable logins) + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable logins) => Save(userId.ToGuid(), logins); + + /// + [Obsolete("Use method that takes guid as param")] + public void Save(int userId, IEnumerable tokens) => Save(userId.ToGuid(), tokens); + + /// + public void DeleteUserLogins(Guid userOrMemberKey) => Database.Delete("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey }); + + /// + public void Save(Guid userOrMemberKey, IEnumerable logins) { var sql = Sql() .Select() .From() - .Where(x => x.UserId == userId) + .Where(x => x.UserOrMemberKey == userOrMemberKey) .ForUpdate(); // deduplicate the logins @@ -71,10 +82,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement foreach (var u in toUpdate) { - Database.Update(ExternalLoginFactory.BuildDto(userId, u.Value, u.Key)); + Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key)); } - Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userId, i))); + Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i))); } protected override IIdentityUserLogin PerformGet(int id) @@ -217,11 +228,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return Database.ExecuteScalar(sql); } - public void Save(int userId, IEnumerable tokens) + /// + public void Save(Guid userOrMemberKey, IEnumerable tokens) { // get the existing logins (provider + id) var existingUserLogins = Database - .Fetch(GetBaseQuery(false).Where(x => x.UserId == userId)) + .Fetch(GetBaseQuery(false).Where(x => x.UserOrMemberKey == userOrMemberKey)) .ToDictionary(x => x.LoginProvider, x => x.Id); // deduplicate the tokens @@ -231,7 +243,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement Sql sql = GetBaseTokenQuery(true) .WhereIn(x => x.LoginProvider, providers) - .Where(x => x.UserId == userId); + .Where(x => x.UserOrMemberKey == userOrMemberKey); var toUpdate = new Dictionary(); var toDelete = new List(); @@ -289,7 +301,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement .On(x => x.ExternalLoginId, x => x.Id) : Sql() .Select() - .AndSelect(x => x.LoginProvider, x => x.UserId) + .AndSelect(x => x.LoginProvider, x => x.UserOrMemberKey) .From() .InnerJoin() .On(x => x.ExternalLoginId, x => x.Id); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs new file mode 100644 index 0000000000..18063edf16 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Querying; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + internal class TwoFactorLoginRepository : EntityRepositoryBase, ITwoFactorLoginRepository + { + public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, + ILogger logger) + : base(scopeAccessor, cache, logger) + { + } + + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = SqlContext.Sql(); + + sql = isCount + ? sql.SelectCount() + : sql.Select(); + + sql.From(); + + return sql; + } + + protected override string GetBaseWhereClause() => + Core.Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id"; + + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); + + protected override ITwoFactorLogin PerformGet(int id) + { + var sql = GetBaseQuery(false).Where(x => x.Id == id); + var dto = Database.Fetch(sql).FirstOrDefault(); + return dto == null ? null : Map(dto); + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + var sql = GetBaseQuery(false).WhereIn(x => x.Id, ids); + var dtos = Database.Fetch(sql); + return dtos.WhereNotNull().Select(Map); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + return Database.Fetch(sql).Select(Map); + } + + protected override void PersistNewItem(ITwoFactorLogin entity) + { + var dto = Map(entity); + Database.Insert(dto); + } + + protected override void PersistUpdatedItem(ITwoFactorLogin entity) + { + var dto = Map(entity); + Database.Update(dto); + } + + private static TwoFactorLoginDto Map(ITwoFactorLogin entity) + { + if (entity == null) return null; + + return new TwoFactorLoginDto + { + Id = entity.Id, + UserOrMemberKey = entity.UserOrMemberKey, + ProviderName = entity.ProviderName, + Secret = entity.Secret, + }; + } + + private static ITwoFactorLogin Map(TwoFactorLoginDto dto) + { + if (dto == null) return null; + + return new TwoFactorLogin + { + Id = dto.Id, + UserOrMemberKey = dto.UserOrMemberKey, + ProviderName = dto.ProviderName, + Secret = dto.Secret, + }; + } + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) + { + return await DeleteUserLoginsAsync(userOrMemberKey, null); + } + + public async Task DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName) + { + var sql = Sql() + .Delete() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + + if (providerName is not null) + { + sql = sql.Where(x => x.ProviderName == providerName); + } + + var deletedRows = await Database.ExecuteAsync(sql); + + return deletedRows > 0; + } + + public async Task> GetByUserOrMemberKeyAsync(Guid userOrMemberKey) + { + var sql = Sql() + .Select() + .From() + .Where(x => x.UserOrMemberKey == userOrMemberKey); + var dtos = await Database.FetchAsync(sql); + return dtos.WhereNotNull().Select(Map); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs index d67c97f2c4..ee2689b9e3 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Linq; +using Microsoft.Data.SqlClient; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs index e7f5934e78..63aab47047 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDatabaseCreator.cs @@ -1,6 +1,6 @@ using System; -using System.Data.SqlClient; using System.IO; +using Microsoft.Data.SqlClient; using Umbraco.Cms.Core; namespace Umbraco.Cms.Infrastructure.Persistence diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 210b3f2d6b..2db603ad1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Linq; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs index fd8bdc0269..6093c06a97 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs @@ -292,7 +292,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence _pocoMappers.AddRange(_dbProviderFactoryCreator.ProviderSpecificMappers(_providerName)); - var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver); + var factory = new FluentPocoDataFactory(GetPocoDataFactoryResolver, _pocoMappers); _pocoDataFactory = factory; var config = new FluentConfig(xmappers => factory); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index b4b46f2f7c..7a2d626fcd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -238,7 +238,7 @@ namespace Umbraco.Cms.Core.PropertyEditors MapBlockItemData(blockEditorData.BlockValue.SettingsData); // return json - return JsonConvert.SerializeObject(blockEditorData.BlockValue); + return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index cfa1c4b3cb..7248a7f5b0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -52,7 +52,7 @@ namespace Umbraco.Cms.Core.PropertyEditors UpdateBlockListRecursively(blockListValue, createGuid); - return JsonConvert.SerializeObject(blockListValue.BlockValue); + return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); } private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index 36f11f5ce8..0c9cd40995 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Serialization; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs index e4039b6cee..ac7d5a4ef4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -41,11 +43,13 @@ namespace Umbraco.Cms.Core.PropertyEditors foreach (var cultureVal in propVals) { // Remove keys from published value & any nested properties - var updatedPublishedVal = FormatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + var publishedValue = cultureVal.PublishedValue is JToken jsonPublishedValue ? jsonPublishedValue.ToString(Formatting.None) : cultureVal.PublishedValue?.ToString(); + var updatedPublishedVal = FormatPropertyValue(publishedValue, onlyMissingKeys).NullOrWhiteSpaceAsNull(); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested properties - var updatedEditedVal = FormatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + var editedValue = cultureVal.EditedValue is JToken jsonEditedValue ? jsonEditedValue.ToString(Formatting.None) : cultureVal.EditedValue?.ToString(); + var updatedEditedVal = FormatPropertyValue(editedValue, onlyMissingKeys).NullOrWhiteSpaceAsNull(); cultureVal.EditedValue = updatedEditedVal; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index e3a6987110..52a1e50fc4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -139,7 +139,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } // Convert back to raw JSON for persisting - return JsonConvert.SerializeObject(grid); + return JsonConvert.SerializeObject(grid, Formatting.None); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index 8afdb42419..be3bc3b707 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -213,7 +213,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(src); var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath); jo["src"] = _mediaFileManager.FileSystem.GetUrl(copyPath); - notification.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment); + notification.Copy.SetValue(property.Alias, jo.ToString(Formatting.None), propertyValue.Culture, propertyValue.Segment); isUpdated = true; } } @@ -274,17 +274,11 @@ namespace Umbraco.Cms.Core.PropertyEditors // it can happen when an image is uploaded via the folder browser, in which case // 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... - - var dt = _dataTypeService.GetDataType(property.PropertyType.DataTypeId); - var config = dt?.ConfigurationAs(); src = svalue; - var json = new + property.SetValue(JsonConvert.SerializeObject(new { - src = svalue, - crops = config == null ? Array.Empty() : config.Crops - }; - - property.SetValue(JsonConvert.SerializeObject(json), pvalue.Culture, pvalue.Segment); + 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 4fa41bc7d3..b2f6852cee 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -206,6 +206,10 @@ namespace Umbraco.Cms.Core.PropertyEditors { src = val, crops = crops + },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 45aa507a54..2cfe5dd56e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -12,6 +12,7 @@ using Newtonsoft.Json; using System; using System.Linq; using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors @@ -53,23 +54,55 @@ namespace Umbraco.Cms.Core.PropertyEditors internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference { private readonly IJsonSerializer _jsonSerializer; + private readonly IDataTypeService _dataTypeService; public MediaPicker3PropertyValueEditor( ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataEditorAttribute attribute) + DataEditorAttribute attribute, + IDataTypeService dataTypeService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _jsonSerializer = jsonSerializer; + _dataTypeService = dataTypeService; } public override object ToEditor(IProperty property, string culture = null, string segment = null) { var value = property.GetValue(culture, segment); - return Deserialize(_jsonSerializer, value); + var dtos = Deserialize(_jsonSerializer, value).ToList(); + + var dataType = _dataTypeService.GetDataType(property.PropertyType.DataTypeId); + if (dataType?.Configuration != null) + { + var configuration = dataType.ConfigurationAs(); + + foreach (var dto in dtos) + { + dto.ApplyConfiguration(configuration); + } + } + + return dtos; + } + + public override object FromEditor(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value is JArray dtos) + { + // Clean up redundant/default data + foreach (var dto in dtos.Values()) + { + MediaWithCropsDto.Prune(dto); + } + + return dtos.ToString(Formatting.None); + } + + return base.FromEditor(editorValue, currentValue); } /// @@ -142,6 +175,52 @@ 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. + /// + /// The configuration. + public void ApplyConfiguration(MediaPicker3Configuration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + Crops = crops; + + if (configuration?.EnableLocalFocalPoint == false) + { + FocalPoint = null; + } + } + + /// + /// 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. + /// + public static void Prune(JObject value) => ImageCropperValue.Prune(value); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs index c1d8aa33f8..f6d8a598b0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -141,6 +141,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings { + Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore }; @@ -150,13 +151,17 @@ namespace Umbraco.Cms.Core.PropertyEditors if (string.IsNullOrEmpty(value)) { - return string.Empty; + return null; } try { + var links = JsonConvert.DeserializeObject>(value); + if (links.Count == 0) + return null; + return JsonConvert.SerializeObject( - from link in JsonConvert.DeserializeObject>(value) + from link in links select new MultiUrlPickerValueEditor.LinkDto { Name = link.Name, @@ -164,8 +169,8 @@ namespace Umbraco.Cms.Core.PropertyEditors Target = link.Target, Udi = link.Udi, Url = link.Udi == null ? link.Url : null, // only save the URL for external links - }, LinkDisplayJsonSerializerSettings - ); + }, + LinkDisplayJsonSerializerSettings); } catch (Exception ex) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs index c3a5bae383..97cb677d4c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -80,7 +80,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { var asArray = editorValue.Value as JArray; - if (asArray == null) + if (asArray == null || asArray.HasValues == false) { return null; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs index c0e544b50c..47f8c9a169 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs @@ -59,14 +59,19 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { var json = editorValue.Value as JArray; - if (json == null) + if (json == null || json.HasValues == false) { return null; } var values = json.Select(item => item.Value()).ToArray(); - return JsonConvert.SerializeObject(values); + if (values.Length == 0) + { + return null; + } + + return JsonConvert.SerializeObject(values, Formatting.None); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 80ed34e6e1..835431820c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(propertyValue); if (rows.Count == 0) - return string.Empty; + return null; foreach (var row in rows.ToList()) { @@ -136,7 +136,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } } - return JsonConvert.SerializeObject(rows).ToXmlString(); + return JsonConvert.SerializeObject(rows, Formatting.None).ToXmlString(); } #endregion @@ -231,7 +231,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(editorValue.Value); if (rows.Count == 0) - return string.Empty; + return null; foreach (var row in rows.ToList()) { @@ -256,7 +256,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } // return json - return JsonConvert.SerializeObject(rows); + return JsonConvert.SerializeObject(rows, Formatting.None); } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs index 1bc5dd2f4b..aa825bb0f8 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Extensions; @@ -32,7 +33,7 @@ namespace Umbraco.Cms.Core.PropertyEditors UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); - return complexEditorValue.ToString(); + return complexEditorValue.ToString(Formatting.None); } private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) @@ -65,7 +66,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var parsed = JToken.Parse(propVal); UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); // set the value to the updated one - prop.Value = parsed.ToString(); + prop.Value = parsed.ToString(Formatting.None); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 50b6d7a881..1f05da3bde 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -157,7 +157,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); - return parsed; + return parsed.NullOrWhiteSpaceAsNull(); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 4683851936..42f6424bfa 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -73,7 +73,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (editorValue.Value is JArray json) { - return json.Select(x => x.Value()); + return json.HasValues ? json.Select(x => x.Value()) : null; } if (string.IsNullOrWhiteSpace(value) == false) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs index c0efaac4ae..97f1b8398c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; @@ -122,13 +123,19 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// /// public bool HasFocalPoint() - => FocalPoint != null && (FocalPoint.Left != 0.5m || FocalPoint.Top != 0.5m); + => FocalPoint is ImageCropperFocalPoint focalPoint && (focalPoint.Left != 0.5m || focalPoint.Top != 0.5m); + + /// + /// Determines whether the value has crops. + /// + public bool HasCrops() + => Crops is IEnumerable crops && crops.Any(); /// /// Determines whether the value has a specified crop. /// public bool HasCrop(string alias) - => Crops != null && Crops.Any(x => x.Alias == alias); + => Crops is IEnumerable crops && crops.Any(x => x.Alias == alias); /// /// Determines whether the value has a source image. @@ -167,6 +174,51 @@ 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.TryGetValue("crops", out var crops)) + { + if (crops.HasValues) + { + foreach (var crop in crops.Values().ToList()) + { + if (crop.TryGetValue("coordinates", out var coordinates) == false || coordinates.HasValues == false) + { + // Remove crop without coordinates + crop.Remove(); + continue; + } + + // Width/height are already stored in the crop configuration + crop.Remove("width"); + crop.Remove("height"); + } + } + + if (crops.HasValues == false) + { + // Remove empty crops + value.Remove("crops"); + } + } + + if (value.TryGetValue("focalPoint", out var focalPoint) && + (focalPoint.HasValues == false || (focalPoint.Value("top") == 0.5m && focalPoint.Value("left") == 0.5m))) + { + // Remove empty/default focal point + value.Remove("focalPoint"); + } + } + #region IEquatable /// diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 175bceb9e0..5dbe78c2f5 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -2,6 +2,8 @@ using System; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; @@ -16,12 +18,13 @@ using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using ComponentCollection = Umbraco.Cms.Core.Composing.ComponentCollection; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.Runtime { + /// public class CoreRuntime : IRuntime { - private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; private readonly ComponentCollection _components; private readonly IApplicationShutdownRegistry _applicationShutdownRegistry; @@ -32,14 +35,16 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly IHostingEnvironment _hostingEnvironment; private readonly IUmbracoVersion _umbracoVersion; private readonly IServiceProvider _serviceProvider; + private readonly IHostApplicationLifetime _hostApplicationLifetime; + private readonly ILogger _logger; private CancellationToken _cancellationToken; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CoreRuntime( - ILoggerFactory loggerFactory, IRuntimeState state, + ILoggerFactory loggerFactory, ComponentCollection components, IApplicationShutdownRegistry applicationShutdownRegistry, IProfilingLogger profilingLogger, @@ -48,9 +53,11 @@ namespace Umbraco.Cms.Infrastructure.Runtime IEventAggregator eventAggregator, IHostingEnvironment hostingEnvironment, IUmbracoVersion umbracoVersion, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, + IHostApplicationLifetime hostApplicationLifetime) { State = state; + _loggerFactory = loggerFactory; _components = components; _applicationShutdownRegistry = applicationShutdownRegistry; @@ -61,6 +68,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime _hostingEnvironment = hostingEnvironment; _umbracoVersion = umbracoVersion; _serviceProvider = serviceProvider; + _hostApplicationLifetime = hostApplicationLifetime; _logger = _loggerFactory.CreateLogger(); } @@ -76,23 +84,49 @@ namespace Umbraco.Cms.Infrastructure.Runtime IUmbracoDatabaseFactory databaseFactory, IEventAggregator eventAggregator, IHostingEnvironment hostingEnvironment, - IUmbracoVersion umbracoVersion - ):this( - loggerFactory, - state, - components, - applicationShutdownRegistry, - profilingLogger, - mainDom, - databaseFactory, - eventAggregator, - hostingEnvironment, - umbracoVersion, - null - ) - { + IUmbracoVersion umbracoVersion, + IServiceProvider serviceProvider) + : this( + state, + loggerFactory, + components, + applicationShutdownRegistry, + profilingLogger, + mainDom, + databaseFactory, + eventAggregator, + hostingEnvironment, + umbracoVersion, + serviceProvider, + serviceProvider?.GetRequiredService()) + { } - } + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete] + public CoreRuntime( + ILoggerFactory loggerFactory, + IRuntimeState state, + ComponentCollection components, + IApplicationShutdownRegistry applicationShutdownRegistry, + IProfilingLogger profilingLogger, + IMainDom mainDom, + IUmbracoDatabaseFactory databaseFactory, + IEventAggregator eventAggregator, + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion) + : this( + loggerFactory, + state, + components, + applicationShutdownRegistry, + profilingLogger, + mainDom, + databaseFactory, + eventAggregator, + hostingEnvironment, + umbracoVersion, + null) + { } /// /// Gets the state of the Umbraco runtime. @@ -103,13 +137,17 @@ namespace Umbraco.Cms.Infrastructure.Runtime 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) { + // Store token, so we can re-use this during restart _cancellationToken = cancellationToken; + StaticApplicationLogging.Initialize(_loggerFactory); StaticServiceProvider.Instance = _serviceProvider; @@ -130,6 +168,13 @@ namespace Umbraco.Cms.Infrastructure.Runtime _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())); + } + // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(); @@ -137,7 +182,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime await _eventAggregator.PublishAsync(new UmbracoApplicationMainDomAcquiredNotification(), cancellationToken); // notify for unattended install - await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification()); + await _eventAggregator.PublishAsync(new RuntimeUnattendedInstallNotification(), cancellationToken); DetermineRuntimeLevel(); if (!State.UmbracoCanBoot()) @@ -153,7 +198,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime // 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); + await _eventAggregator.PublishAsync(unattendedUpgradeNotification, cancellationToken); switch (unattendedUpgradeNotification.UnattendedUpgradeResult) { case RuntimeUnattendedUpgradeNotification.UpgradeResult.HasErrors: @@ -161,6 +206,7 @@ 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 return; case RuntimeUnattendedUpgradeNotification.UpgradeResult.CoreUpgradeComplete: diff --git a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs index 8eab08bfee..73b6692e3a 100644 --- a/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/Runtime/RuntimeState.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -12,6 +13,7 @@ using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Runtime @@ -30,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime private readonly ILogger _logger; private readonly PendingPackageMigrations _packageMigrationState; private readonly Dictionary _startupState = new Dictionary(); + private readonly IConflictingRouteService _conflictingRouteService; /// /// The initial @@ -51,6 +54,25 @@ namespace Umbraco.Cms.Infrastructure.Runtime IUmbracoDatabaseFactory databaseFactory, ILogger logger, PendingPackageMigrations packageMigrationState) + : this( + globalSettings, + unattendedSettings, + umbracoVersion, + databaseFactory, + logger, + packageMigrationState, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + public RuntimeState( + IOptions globalSettings, + IOptions unattendedSettings, + IUmbracoVersion umbracoVersion, + IUmbracoDatabaseFactory databaseFactory, + ILogger logger, + PendingPackageMigrations packageMigrationState, + IConflictingRouteService conflictingRouteService) { _globalSettings = globalSettings; _unattendedSettings = unattendedSettings; @@ -58,9 +80,9 @@ namespace Umbraco.Cms.Infrastructure.Runtime _databaseFactory = databaseFactory; _logger = logger; _packageMigrationState = packageMigrationState; + _conflictingRouteService = conflictingRouteService; } - /// public Version Version => _umbracoVersion.Version; @@ -101,6 +123,16 @@ namespace Umbraco.Cms.Infrastructure.Runtime return; } + // Check if we have multiple controllers with the same name. + if (_conflictingRouteService.HasConflictingRoutes(out string controllerName)) + { + Level = RuntimeLevel.BootFailed; + Reason = RuntimeLevelReason.BootFailedOnException; + BootFailedException = new BootFailedException($"Conflicting routes, you cannot have multiple controllers with the same name: {controllerName}"); + + return; + } + // Check the database state, whether we can connect or if it's in an upgrade or empty state, etc... switch (GetUmbracoDatabaseState(_databaseFactory)) diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 8d1c74b619..133c0d857a 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -1,15 +1,14 @@ using System; using System.Data; -using System.Data.SqlClient; using System.Diagnostics; using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NPoco; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Runtime; diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index ebd12719e1..df4d704781 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -159,5 +159,24 @@ namespace Umbraco.Cms.Core.Security } private static string UserIdToString(int userId) => string.Intern(userId.ToString(CultureInfo.InvariantCulture)); + + public Guid Key => UserIdToInt(Id).ToGuid(); + + + private static int UserIdToInt(string userId) + { + if(int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) + { + return result; + } + + if(Guid.TryParse(userId, out var key)) + { + // Reverse the IntExtensions.ToGuid + return BitConverter.ToInt32(key.ToByteArray(), 0); + } + + throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture"); + } } } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 3645115aa5..df05b1e6b5 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; @@ -16,6 +17,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security @@ -29,7 +31,7 @@ namespace Umbraco.Cms.Core.Security private readonly IScopeProvider _scopeProvider; private readonly IUserService _userService; private readonly IEntityService _entityService; - private readonly IExternalLoginService _externalLoginService; + private readonly IExternalLoginWithKeyService _externalLoginService; private readonly GlobalSettings _globalSettings; private readonly IUmbracoMapper _mapper; private readonly AppCaches _appCaches; @@ -37,11 +39,12 @@ namespace Umbraco.Cms.Core.Security /// /// Initializes a new instance of the class. /// + [ActivatorUtilitiesConstructor] public BackOfficeUserStore( IScopeProvider scopeProvider, IUserService userService, IEntityService entityService, - IExternalLoginService externalLoginService, + IExternalLoginWithKeyService externalLoginService, IOptionsSnapshot globalSettings, IUmbracoMapper mapper, BackOfficeErrorDescriber describer, @@ -59,6 +62,29 @@ namespace Umbraco.Cms.Core.Security _externalLoginService = externalLoginService; } + [Obsolete("Use ctor injecting IExternalLoginWithKeyService ")] + public BackOfficeUserStore( + IScopeProvider scopeProvider, + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IOptions globalSettings, + IUmbracoMapper mapper, + BackOfficeErrorDescriber describer, + AppCaches appCaches) + : this( + scopeProvider, + userService, + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + globalSettings, + mapper, + describer, + appCaches) + { + + } + /// public override Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { @@ -104,7 +130,7 @@ namespace Umbraco.Cms.Core.Security if (isLoginsPropertyDirty) { _externalLoginService.Save( - userEntity.Id, + userEntity.Key, user.Logins.Select(x => new ExternalLogin( x.LoginProvider, x.ProviderKey, @@ -114,7 +140,7 @@ namespace Umbraco.Cms.Core.Security if (isTokensPropertyDirty) { _externalLoginService.Save( - userEntity.Id, + userEntity.Key, user.LoginTokens.Select(x => new ExternalLoginToken( x.LoginProvider, x.Name, @@ -156,7 +182,7 @@ namespace Umbraco.Cms.Core.Security if (isLoginsPropertyDirty) { _externalLoginService.Save( - found.Id, + found.Key, user.Logins.Select(x => new ExternalLogin( x.LoginProvider, x.ProviderKey, @@ -166,7 +192,7 @@ namespace Umbraco.Cms.Core.Security if (isTokensPropertyDirty) { _externalLoginService.Save( - found.Id, + found.Key, user.LoginTokens.Select(x => new ExternalLoginToken( x.LoginProvider, x.Name, @@ -190,13 +216,14 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(user)); } - IUser found = _userService.GetUserById(UserIdToInt(user.Id)); + var userId = UserIdToInt(user.Id); + IUser found = _userService.GetUserById(userId); if (found != null) { _userService.Delete(found); } - _externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); + _externalLoginService.DeleteUserLogins(userId.ToGuid()); return Task.FromResult(IdentityResult.Success); } @@ -414,7 +441,7 @@ namespace Umbraco.Cms.Core.Security { if (user != null) { - var userId = UserIdToInt(user.Id); + var userId = UserIdToInt(user.Id).ToGuid(); user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetExternalLogins(userId))); user.SetTokensCallback(new Lazy>(() => _externalLoginService.GetExternalLoginTokens(userId))); } diff --git a/src/Umbraco.Infrastructure/Security/DeleteExternalLoginsOnMemberDeletedHandler.cs b/src/Umbraco.Infrastructure/Security/DeleteExternalLoginsOnMemberDeletedHandler.cs new file mode 100644 index 0000000000..b2b829d8e1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/DeleteExternalLoginsOnMemberDeletedHandler.cs @@ -0,0 +1,30 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Security +{ + /// + /// Deletes the external logins for the deleted members. This cannot be handled by the database as there is not foreign keys. + /// + public class DeleteExternalLoginsOnMemberDeletedHandler : INotificationHandler + { + private readonly IExternalLoginWithKeyService _externalLoginWithKeyService; + + /// + /// Initializes a new instance of the class. + /// + public DeleteExternalLoginsOnMemberDeletedHandler(IExternalLoginWithKeyService externalLoginWithKeyService) + => _externalLoginWithKeyService = externalLoginWithKeyService; + + /// + public void Handle(MemberDeletedNotification notification) + { + foreach (IMember member in notification.DeletedEntities) + { + _externalLoginWithKeyService.DeleteUserLogins(member.Key); + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Security/DeleteTwoFactorLoginsOnMemberDeletedHandler.cs b/src/Umbraco.Infrastructure/Security/DeleteTwoFactorLoginsOnMemberDeletedHandler.cs new file mode 100644 index 0000000000..7fe4a7c506 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/DeleteTwoFactorLoginsOnMemberDeletedHandler.cs @@ -0,0 +1,33 @@ +using System.Threading; +using System.Threading.Tasks; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Security +{ + /// + /// Deletes the two factor for the deleted members. This cannot be handled by the database as there is not foreign keys. + /// + public class DeleteTwoFactorLoginsOnMemberDeletedHandler : INotificationAsyncHandler + { + private readonly ITwoFactorLoginService _twoFactorLoginService; + + /// + /// Initializes a new instance of the class. + /// + public DeleteTwoFactorLoginsOnMemberDeletedHandler(ITwoFactorLoginService twoFactorLoginService) + => _twoFactorLoginService = twoFactorLoginService; + + /// + public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken) + { + foreach (IMember member in notification.DeletedEntities) + { + await _twoFactorLoginService.DeleteUserLoginsAsync(member.Key); + } + } + + } +} diff --git a/src/Umbraco.Infrastructure/Security/ITwoFactorProvider.cs b/src/Umbraco.Infrastructure/Security/ITwoFactorProvider.cs new file mode 100644 index 0000000000..f0da6c314a --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/ITwoFactorProvider.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; + +namespace Umbraco.Cms.Core.Security +{ + public interface ITwoFactorProvider + { + string ProviderName { get; } + + Task GetSetupDataAsync(Guid userOrMemberKey, string secret); + + bool ValidateTwoFactorPIN(string secret, string token); + + /// + /// + /// + /// Called to confirm the setup of two factor on the user. + bool ValidateTwoFactorSetup(string secret, string token); + } + + +} diff --git a/src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs b/src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs new file mode 100644 index 0000000000..c0df423638 --- /dev/null +++ b/src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs @@ -0,0 +1,63 @@ +using System; +using System.Reflection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.Security +{ + + public class MemberIdentityBuilder : IdentityBuilder + { + /// + /// Initializes a new instance of the class. + /// + public MemberIdentityBuilder(IServiceCollection services) + : base(typeof(MemberIdentityUser), services) + => InitializeServices(services); + + /// + /// Initializes a new instance of the class. + /// + public MemberIdentityBuilder(Type role, IServiceCollection services) + : base(typeof(MemberIdentityUser), role, services) + => InitializeServices(services); + + private void InitializeServices(IServiceCollection services) + { + + } + + // override to add itself, by default identity only wants a single IdentityErrorDescriber + public override IdentityBuilder AddErrorDescriber() + { + if (!typeof(MembersErrorDescriber).IsAssignableFrom(typeof(TDescriber))) + { + throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(MembersErrorDescriber)}"); + } + + Services.AddScoped(); + return this; + } + + /// + /// Adds a token provider for the . + /// + /// The name of the provider to add. + /// The type of the to add. + /// The current instance. + public override IdentityBuilder AddTokenProvider(string providerName, Type provider) + { + if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo())) + { + throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}"); + } + + Services.Configure(options => options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider)); + Services.AddTransient(provider); + return this; + } + } +} diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index d757cfb088..4fba880e81 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Security @@ -26,6 +28,8 @@ namespace Umbraco.Cms.Core.Security private readonly IUmbracoMapper _mapper; private readonly IScopeProvider _scopeProvider; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IExternalLoginWithKeyService _externalLoginService; + private readonly ITwoFactorLoginService _twoFactorLoginService; /// /// Initializes a new instance of the class for the members identity store @@ -34,18 +38,52 @@ namespace Umbraco.Cms.Core.Security /// The mapper for properties /// The scope provider /// The error describer + /// The published snapshot accessor + /// The external login service + /// The two factor login service + [ActivatorUtilitiesConstructor] public MemberUserStore( IMemberService memberService, IUmbracoMapper mapper, IScopeProvider scopeProvider, IdentityErrorDescriber describer, - IPublishedSnapshotAccessor publishedSnapshotAccessor) + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IExternalLoginWithKeyService externalLoginService, + ITwoFactorLoginService twoFactorLoginService + ) : base(describer) { _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider)); _publishedSnapshotAccessor = publishedSnapshotAccessor; + _externalLoginService = externalLoginService; + _twoFactorLoginService = twoFactorLoginService; + } + + [Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")] + public MemberUserStore( + IMemberService memberService, + IUmbracoMapper mapper, + IScopeProvider scopeProvider, + IdentityErrorDescriber describer, + IPublishedSnapshotAccessor publishedSnapshotAccessor, + IExternalLoginService externalLoginService) + : this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")] + public MemberUserStore( + IMemberService memberService, + IUmbracoMapper mapper, + IScopeProvider scopeProvider, + IdentityErrorDescriber describer, + IPublishedSnapshotAccessor publishedSnapshotAccessor) + : this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) + { + } /// @@ -83,18 +121,29 @@ namespace Umbraco.Cms.Core.Security user.Id = UserIdToString(memberEntity.Id); user.Key = memberEntity.Key; - // [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. - // var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins)); - // TODO: confirm re externallogins implementation - //if (isLoginsPropertyDirty) - //{ - // _externalLoginService.Save( - // user.Id, - // user.Logins.Select(x => new ExternalLogin( - // x.LoginProvider, - // x.ProviderKey, - // x.UserData))); - //} + // we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it. + var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins)); + var isTokensPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.LoginTokens)); + + if (isLoginsPropertyDirty) + { + _externalLoginService.Save( + memberEntity.Key, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + x.UserData))); + } + + if (isTokensPropertyDirty) + { + _externalLoginService.Save( + memberEntity.Key, + user.LoginTokens.Select(x => new ExternalLoginToken( + x.LoginProvider, + x.Name, + x.Value))); + } return Task.FromResult(IdentityResult.Success); @@ -142,17 +191,15 @@ namespace Umbraco.Cms.Core.Security _memberService.SetLastLogin(found.Username, DateTime.Now); } - // TODO: when to implement external login service? - - //if (isLoginsPropertyDirty) - //{ - // _externalLoginService.Save( - // found.Id, - // user.Logins.Select(x => new ExternalLogin( - // x.LoginProvider, - // x.ProviderKey, - // x.UserData))); - //} + if (isLoginsPropertyDirty) + { + _externalLoginService.Save( + found.Key, + user.Logins.Select(x => new ExternalLogin( + x.LoginProvider, + x.ProviderKey, + x.UserData))); + } } return Task.FromResult(IdentityResult.Success); @@ -181,8 +228,7 @@ namespace Umbraco.Cms.Core.Security _memberService.Delete(found); } - // TODO: when to implement external login service? - //_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id)); + _externalLoginService.DeleteUserLogins(user.Key); return Task.FromResult(IdentityResult.Success); } @@ -203,7 +249,8 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(userId)); } - IMember user = _memberService.GetById(UserIdToInt(userId)); + + IMember user = Guid.TryParse(userId, out var key) ? _memberService.GetByKey(key) : _memberService.GetById(UserIdToInt(userId)); if (user == null) { return Task.FromResult((MemberIdentityUser)null); @@ -375,10 +422,7 @@ namespace Umbraco.Cms.Core.Security throw new ArgumentNullException(nameof(providerKey)); } - var logins = new List(); - - // TODO: external login needed - //_externalLoginService.Find(loginProvider, providerKey).ToList(); + var logins = _externalLoginService.Find(loginProvider, providerKey).ToList(); if (logins.Count == 0) { return Task.FromResult((IdentityUserLogin)null); @@ -492,8 +536,8 @@ namespace Umbraco.Cms.Core.Security { if (user != null) { - //TODO: implement - //user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetAll(UserIdToInt(user.Id)))); + user.SetLoginsCallback(new Lazy>(() => _externalLoginService.GetExternalLogins(user.Key))); + user.SetTokensCallback(new Lazy>(() => _externalLoginService.GetExternalLoginTokens(user.Key))); } return user; @@ -639,5 +683,34 @@ namespace Umbraco.Cms.Core.Security LoginOnly, FullSave } + + /// + /// Overridden to support Umbraco's own data storage requirements + /// + /// + /// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change + /// tracking ORMs like EFCore. + /// + /// + public override Task GetTokenAsync(MemberIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ThrowIfDisposed(); + + if (user == null) + { + throw new ArgumentNullException(nameof(user)); + } + IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name)); + + return Task.FromResult(token?.Value); + } + + /// + public override async Task GetTwoFactorEnabledAsync(MemberIdentityUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key); + } } } diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 0fee166013..111a05d816 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -263,5 +263,13 @@ namespace Umbraco.Cms.Core.Security return await VerifyPasswordAsync(userPasswordStore, user, password) == PasswordVerificationResult.Success; } + + /// + public virtual async Task> GetValidTwoFactorProvidersAsync(TUser user) + { + var results = await base.GetValidTwoFactorProvidersAsync(user); + + return results; + } } } diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs index 6a2325c316..aaaaed55e7 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs @@ -34,6 +34,12 @@ namespace Umbraco.Cms.Core.Security return result; } + if(Guid.TryParse(userId, out var key)) + { + // Reverse the IntExtensions.ToGuid + return BitConverter.ToInt32(key.ToByteArray(), 0); + } + throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture"); } diff --git a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs index e8e32fe7da..ab978c903e 100644 --- a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs @@ -12,6 +12,8 @@ namespace Umbraco.Cms.Infrastructure.Serialization { JsonSerializerSettings.Converters.Add(new FuzzyBooleanConverter()); JsonSerializerSettings.ContractResolver = new ConfigurationCustomContractResolver(); + JsonSerializerSettings.Formatting = Formatting.None; + JsonSerializerSettings.NullValueHandling = NullValueHandling.Ignore; } private class ConfigurationCustomContractResolver : DefaultContractResolver diff --git a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs index a728630680..5c5377c0a1 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Infrastructure.Serialization Converters = new List() { new StringEnumConverter() - } + }, + Formatting = Formatting.None }; public string Serialize(object input) { diff --git a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs index e4c17ab918..2e7416b2d2 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Serialization } // Load JObject from stream JObject jObject = JObject.Load(reader); - return jObject.ToString(); + return jObject.ToString(Formatting.None); } public override bool CanConvert(Type objectType) diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs new file mode 100644 index 0000000000..713a73c1df --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +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 IDictionary _twoFactorSetupGenerators; + + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + IScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions) + { + _twoFactorLoginRepository = twoFactorLoginRepository; + _scopeProvider = scopeProvider; + _identityOptions = identityOptions; + _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); + } + + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)) + .Select(x => x.ProviderName).ToArray(); + + return providersOnUser.Where(x => _identityOptions.Value.Tokens.ProviderMap.ContainsKey(x)); + } + + + 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; + } + + 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 + if (!string.IsNullOrEmpty(secret)) + { + return default; + } + + secret = GenerateSecret(); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + 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)); + + } + + public bool ValidateTwoFactorSetup(string providerName, string secret, string code) + { + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + return generator.ValidateTwoFactorSetup(secret, code); + } + + public Task SaveAsync(TwoFactorLogin twoFactorLogin) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + _twoFactorLoginRepository.Save(twoFactorLogin); + + return Task.CompletedTask; + } + + + /// + /// Generates a new random unique secret. + /// + /// The random secret + protected virtual string GenerateSecret() => Guid.NewGuid().ToString(); + } +} diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index 806fe0853d..0a50a7862c 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -36,7 +36,7 @@ - + @@ -49,7 +49,6 @@ - diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs index 81c9710ad6..e2f8f57bbe 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotServiceEventHandler.cs @@ -1,12 +1,9 @@ using System; -using System.Collections.Generic; using System.Linq; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; using Umbraco.Extensions; @@ -17,7 +14,6 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// Subscribes to Umbraco events to ensure nucache remains consistent with the source data /// public class PublishedSnapshotServiceEventHandler : - IDisposable, INotificationHandler, INotificationHandler, INotificationHandler, @@ -28,47 +24,18 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache INotificationHandler, INotificationHandler { - private readonly IRuntimeState _runtime; - private bool _disposedValue; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly INuCacheContentService _publishedContentService; - private readonly IContentService _contentService; - private readonly IMediaService _mediaService; /// /// Initializes a new instance of the class. /// public PublishedSnapshotServiceEventHandler( - IRuntimeState runtime, IPublishedSnapshotService publishedSnapshotService, - INuCacheContentService publishedContentService, - IContentService contentService, - IMediaService mediaService) + INuCacheContentService publishedContentService) { - _runtime = runtime; _publishedSnapshotService = publishedSnapshotService; _publishedContentService = publishedContentService; - _contentService = contentService; - _mediaService = mediaService; - } - - /// - /// Binds to the Umbraco events - /// - /// Returns true if binding occurred - public bool Initialize() - { - // however, the cache is NOT available until we are configured, because loading - // content (and content types) from database cannot be consistent (see notes in "Handle - // Notifications" region), so - // - notifications will be ignored - // - trying to obtain a published snapshot from the service will throw - if (_runtime.Level != RuntimeLevel.Run) - { - return false; - } - - return true; } // note: if the service is not ready, ie _isReady is false, then we still handle repository events, @@ -136,20 +103,5 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _publishedSnapshotService.Rebuild(contentTypeIds: Array.Empty()); } } - - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - _disposedValue = true; - } - } - - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index cc8ff205b7..0282ac0612 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -29,6 +30,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.Cms.Web.Common.Models; using Umbraco.Extensions; @@ -71,9 +73,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly LinkGenerator _linkGenerator; private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebRoutingSettings _webRoutingSettings; // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here - + [ActivatorUtilitiesConstructor] public AuthenticationController( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IBackOfficeUserManager backOfficeUserManager, @@ -91,7 +95,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IHostingEnvironment hostingEnvironment, LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalAuthenticationOptions, - IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) + IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, + IHttpContextAccessor httpContextAccessor, + IOptions webRoutingSettings) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _userManager = backOfficeUserManager; @@ -110,7 +116,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _linkGenerator = linkGenerator; _externalAuthenticationOptions = externalAuthenticationOptions; _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; - + _httpContextAccessor = httpContextAccessor; + _webRoutingSettings = webRoutingSettings.Value; } /// @@ -629,8 +636,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers r = code }); - // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _hostingEnvironment.ApplicationMainUrl; + // Construct full URL using configured application URL (which will fall back to current request) + Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request.GetApplicationUri(_webRoutingSettings); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs index 0c6f798901..8255dcd977 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; @@ -8,9 +9,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dashboards; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; @@ -66,6 +69,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers //we have just one instance of HttpClient shared for the entire application private static readonly HttpClient HttpClient = new HttpClient(); + // TODO(V10) : change return type to Task> and consider removing baseUrl as parameter //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side [ValidateAngularAntiForgeryToken] public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.com/") @@ -76,6 +80,16 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); var isAdmin = user.IsAdmin(); + if (!IsAllowedUrl(baseUrl)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {baseUrl}"); + HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; + + // Hacking the response - can't set the HttpContext.Response.Body, so instead returning the error as JSON + var errorJson = JsonConvert.SerializeObject(new { Error = "Dashboard source not permitted" }); + return JObject.Parse(errorJson); + } + var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}", baseUrl, _dashboardSettings.ContentDashboardPath, @@ -116,8 +130,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return result; } + // TODO(V10) : consider removing baseUrl as parameter public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") { + if (!IsAllowedUrl(baseUrl)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {baseUrl}"); + return BadRequest("Dashboard source not permitted"); + } + var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); var key = "umbraco-dynamic-dashboard-css-" + section; @@ -152,12 +173,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } - return Content(result,"text/css", Encoding.UTF8); + return Content(result, "text/css", Encoding.UTF8); } public async Task GetRemoteXml(string site, string url) { + if (!IsAllowedUrl(url)) + { + _logger.LogError($"The following URL is not listed in the setting 'Umbraco:CMS:ContentDashboard:ContentDashboardUrlAllowlist' in configuration: {url}"); + return BadRequest("Dashboard source not permitted"); + } + // This is used in place of the old feedproxy.config // Which was used to grab data from our.umbraco.com, umbraco.com or umbraco.tv // for certain dashboards or the help drawer @@ -214,7 +241,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers } } - return Content(result,"text/xml", Encoding.UTF8); + return Content(result, "text/xml", Encoding.UTF8); } @@ -240,5 +267,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }) }).ToList(); } + + // Checks if the passed URL is part of the configured allowlist of addresses + private bool IsAllowedUrl(string url) + { + // No addresses specified indicates that any URL is allowed + if (_dashboardSettings.ContentDashboardUrlAllowlist is null || _dashboardSettings.ContentDashboardUrlAllowlist.Contains(url, StringComparer.OrdinalIgnoreCase)) + { + return true; + } + else + { + return false; + } + } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs index 813682c70e..6312b935f9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PackageController.cs @@ -69,7 +69,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (ModelState.IsValid == false) return ValidationProblem(ModelState); - //save it + // Save it if (!_packagingService.SaveCreatedPackage(model)) { return ValidationProblem( @@ -78,9 +78,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers : $"The package with id {model.Id} was not found"); } - _packagingService.ExportCreatedPackage(model); - - //the packagePath will be on the model + // The packagePath will be on the model return model; } @@ -112,11 +110,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationErrorResult.CreateNotificationValidationErrorResult( $"Package migration failed on package {packageName} with error: {ex.Message}. Check log for full details."); - } + } } [HttpGet] - public IActionResult DownloadCreatedPackage(int id) + public IActionResult DownloadCreatedPackage(int id) { var package = _packagingService.GetCreatedPackageById(id); if (package == null) diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index db23e3b04c..c6171e1add 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using MimeKit; @@ -42,6 +43,7 @@ using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; @@ -75,7 +77,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper; private readonly IPasswordChanger _passwordChanger; private readonly ILogger _logger; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebRoutingSettings _webRoutingSettings; + [ActivatorUtilitiesConstructor] public UsersController( MediaFileManager mediaFileManager, IOptionsSnapshot contentSettings, @@ -96,7 +101,9 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalLogins, UserEditorAuthorizationHelper userEditorAuthorizationHelper, - IPasswordChanger passwordChanger) + IPasswordChanger passwordChanger, + IHttpContextAccessor httpContextAccessor, + IOptions webRoutingSettings) { _mediaFileManager = mediaFileManager; _contentSettings = contentSettings.Value; @@ -119,6 +126,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _userEditorAuthorizationHelper = userEditorAuthorizationHelper; _passwordChanger = passwordChanger; _logger = _loggerFactory.CreateLogger(); + _httpContextAccessor = httpContextAccessor; + _webRoutingSettings = webRoutingSettings.Value; } /// @@ -421,20 +430,25 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// public async Task> PostInviteUser(UserInvite userSave) { - if (userSave == null) throw new ArgumentNullException("userSave"); + if (userSave == null) + { + throw new ArgumentNullException("userSave"); + } if (userSave.Message.IsNullOrWhiteSpace()) + { ModelState.AddModelError("Message", "Message cannot be empty"); + } IUser user; if (_securitySettings.UsernameIsEmail) { - //ensure it's the same + // ensure it's the same userSave.Username = userSave.Email; } else { - //first validate the username if we're showing it + // first validate the username if we're showing it var userResult = CheckUniqueUsername(userSave.Username, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); if (!(userResult.Result is null)) { @@ -443,6 +457,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers user = userResult.Value; } + user = CheckUniqueEmail(userSave.Email, u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue); if (ModelState.IsValid == false) @@ -455,7 +470,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem("No Email server is configured"); } - //Perform authorization here to see if the current user can actually save this user with the info being requested + // Perform authorization here to see if the current user can actually save this user with the info being requested var canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser, user, null, null, userSave.UserGroups); if (canSaveUser == false) { @@ -464,8 +479,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers if (user == null) { - //we want to create the user with the UserManager, this ensures the 'empty' (special) password - //format is applied without us having to duplicate that logic + // we want to create the user with the UserManager, this ensures the 'empty' (special) password + // format is applied without us having to duplicate that logic var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email, _globalSettings.DefaultUILanguage); identityUser.Name = userSave.Name; @@ -475,21 +490,21 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return ValidationProblem(created.Errors.ToErrorMessage()); } - //now re-look the user back up + // now re-look the user back up user = _userService.GetByEmail(userSave.Email); } - //map the save info over onto the user + // map the save info over onto the user user = _umbracoMapper.Map(userSave, user); - //ensure the invited date is set + // ensure the invited date is set user.InvitedDate = DateTime.Now; - //Save the updated user (which will process the user groups too) + // Save the updated user (which will process the user groups too) _userService.Save(user); var display = _umbracoMapper.Map(user); - //send the email + // send the email await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Name, _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Email, user, userSave.Message); display.AddSuccessNotification(_localizedTextService.Localize("speechBubbles","resendInviteHeader"), _localizedTextService.Localize("speechBubbles","resendInviteSuccess", new[] { user.Name })); @@ -544,14 +559,14 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers }); // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _hostingEnvironment.ApplicationMainUrl; + Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request.GetApplicationUri(_webRoutingSettings); var inviteUri = new Uri(applicationUri, action); var emailSubject = _localizedTextService.Localize("user","inviteEmailCopySubject", - //Ensure the culture of the found user is used for the email! + // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings)); var emailBody = _localizedTextService.Localize("user","inviteEmailCopyFormat", - //Ensure the culture of the found user is used for the email! + // Ensure the culture of the found user is used for the email! UmbracoUserExtensions.GetUserCulture(to.Language, _localizedTextService, _globalSettings), new[] { userDisplay.Name, from, message, inviteUri.ToString(), senderEmail }); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index 4e95236b5f..e9cc213598 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -2,9 +2,15 @@ using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Net; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Security; @@ -27,7 +33,16 @@ namespace Umbraco.Extensions builder.BuildUmbracoBackOfficeIdentity() .AddDefaultTokenProviders() - .AddUserStore() + .AddUserStore, BackOfficeUserStore>(factory => new BackOfficeUserStore( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService>(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService() + )) .AddUserManager() .AddSignInManager() .AddClaimsPrincipalFactory() diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 705b47d806..2c801e963b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -114,6 +114,7 @@ namespace Umbraco.Extensions }); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddSingleton(); return builder; diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs index 80a9d920a1..d62edcc1f9 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeAuthenticationBuilder.cs @@ -62,6 +62,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security { public void PostConfigure(string name, TOptions options) { + if (!name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix)) + { + return; + } + options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType; } } diff --git a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs index 504c74d90e..1f5a7fad33 100644 --- a/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ExternalSignInAutoLinkOptions.cs @@ -3,6 +3,7 @@ using System.Runtime.Serialization; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.Security; using SecurityConstants = Umbraco.Cms.Core.Constants.Security; namespace Umbraco.Cms.Web.BackOffice.Security @@ -13,11 +14,12 @@ namespace Umbraco.Cms.Web.BackOffice.Security public class ExternalSignInAutoLinkOptions { /// - /// Creates a new instance + /// Initializes a new instance of the class. /// /// /// If null, the default will be the 'editor' group /// + /// public ExternalSignInAutoLinkOptions( bool autoLinkExternalAccount = false, string[] defaultUserGroups = null, @@ -30,12 +32,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security _defaultCulture = defaultCulture; } - /// - /// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user - /// will not see and cannot perform manual linking or unlinking of the external provider. - /// - public bool AllowManualLinking { get; } - /// /// A callback executed during account auto-linking and before the user is persisted /// @@ -50,10 +46,16 @@ namespace Umbraco.Cms.Web.BackOffice.Security public Func OnExternalLogin { get; set; } /// - /// Flag indicating if logging in with the external provider should auto-link/create a local user + /// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a local user /// public bool AutoLinkExternalAccount { get; } + /// + /// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user + /// will not see and cannot perform manual linking or unlinking of the external provider. + /// + public bool AllowManualLinking { get; protected set; } + /// /// The default user groups to assign to the created local user linked /// @@ -64,7 +66,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security /// /// The default Culture to use for auto-linking users /// - // TODO: Should we use IDefaultCultureAccessor here intead? + // TODO: Should we use IDefaultCultureAccessor here instead? public string GetUserAutoLinkCulture(GlobalSettings globalSettings) => _defaultCulture ?? globalSettings.DefaultUILanguage; } } diff --git a/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs new file mode 100644 index 0000000000..2951ace9e1 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Services/ConflictingRouteService.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Controllers; + +namespace Umbraco.Cms.Web.BackOffice.Services +{ + public class ConflictingRouteService : IConflictingRouteService + { + private readonly TypeLoader _typeLoader; + + /// + /// Initializes a new instance of the class. + /// + public ConflictingRouteService(TypeLoader typeLoader) => _typeLoader = typeLoader; + + /// + public bool HasConflictingRoutes(out string controllerName) + { + var controllers = _typeLoader.GetTypes().ToList(); + foreach (Type controller in controllers) + { + if (controllers.Count(x => x.Name == controller.Name) > 1) + { + controllerName = controller.Name; + return true; + } + } + + controllerName = string.Empty; + return false; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 3dd303d8f7..a41f396faf 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -282,7 +282,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (_emailSender.CanSendRequiredEmail()) { - AddActionNode(item, menu, true, opensDialog: true); + menu.Items.Add(new MenuItem("notify", LocalizedTextService) + { + Icon = "megaphone", + SeparatorBefore = true, + OpensDialog = true + }); } if((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false) diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs index ecc5b78a51..27eb5b7f6e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTypeTreeController.cs @@ -118,7 +118,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees // root actions menu.Items.Add(LocalizedTextService, opensDialog: true); - menu.Items.Add(new MenuItem("importDocumentType", LocalizedTextService) + menu.Items.Add(new MenuItem("importdocumenttype", LocalizedTextService) { Icon = "page-up", SeparatorBefore = true, diff --git a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs index d8d8afe13a..08f6d7b400 100644 --- a/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs +++ b/src/Umbraco.Web.BackOffice/Trees/TreeCollectionBuilder.cs @@ -48,6 +48,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees var attribute = controllerType.GetCustomAttribute(false); if (attribute == null) return; + + bool isCoreTree = controllerType.HasCustomAttribute(false); + + // Use section as tree group if core tree, so it isn't grouped by empty key and thus end up in "Third Party" tree group if adding custom tree nodes in other groups, e.g. "Settings" tree group. + attribute.TreeGroup = attribute.TreeGroup ?? (isCoreTree ? attribute.SectionAlias : attribute.TreeGroup); + var tree = new Tree(attribute.SortOrder, attribute.SectionAlias, attribute.TreeGroup, attribute.TreeAlias, attribute.TreeTitle, attribute.TreeUse, controllerType, attribute.IsSingleNodeTree); _trees.Add(tree); } diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index 9d30551071..58f66a6fb8 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -1,10 +1,16 @@ using System; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Web.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.ApplicationBuilder { @@ -22,12 +28,14 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder { AppBuilder = appBuilder ?? throw new ArgumentNullException(nameof(appBuilder)); ApplicationServices = appBuilder.ApplicationServices; - RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); + RuntimeState = appBuilder.ApplicationServices.GetRequiredService(); _umbracoPipelineStartupOptions = ApplicationServices.GetRequiredService>(); } public IServiceProvider ApplicationServices { get; } + public IRuntimeState RuntimeState { get; } + public IApplicationBuilder AppBuilder { get; } /// @@ -78,18 +86,32 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder } /// - /// Registers the default required middleware to run Umbraco + /// Registers the default required middleware to run Umbraco. /// - /// public void RegisterDefaultRequiredMiddleware() { UseUmbracoCoreMiddleware(); AppBuilder.UseStatusCodePages(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. AppBuilder.UseImageSharp(); + + // Get media file provider and request path/URL + var mediaFileManager = AppBuilder.ApplicationServices.GetRequiredService(); + if (mediaFileManager.FileSystem.TryCreateFileProvider(out IFileProvider mediaFileProvider)) + { + GlobalSettings globalSettings = AppBuilder.ApplicationServices.GetRequiredService>().Value; + IHostingEnvironment hostingEnvironment = AppBuilder.ApplicationServices.GetService(); + string mediaRequestPath = hostingEnvironment.ToAbsolute(globalSettings.UmbracoMediaPath); + + // Configure custom file provider for media + IWebHostEnvironment webHostEnvironment = AppBuilder.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(mediaRequestPath, mediaFileProvider)); + } + AppBuilder.UseStaticFiles(); + AppBuilder.UseUmbracoPluginsStaticFiles(); // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index 84354e4988..4d74dd1767 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -53,6 +53,26 @@ namespace Umbraco.Cms.Web.Common.Controllers /// public virtual IActionResult Index() => CurrentTemplate(new ContentModel(CurrentPage)); + /// + /// Gets an action result based on the template name found in the route values and a model. + /// + /// The type of the model. + /// The model. + /// The action result. + /// + /// If the template found in the route values doesn't physically exist, Umbraco not found result is returned. + /// + protected override IActionResult CurrentTemplate(T model) + { + if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) + { + // no physical template file was found + return new PublishedContentNotFoundResult(UmbracoContext); + } + + return View(UmbracoRouteValues.TemplateName, model); + } + /// /// Before the controller executes we will handle redirects and not founds /// @@ -123,6 +143,6 @@ namespace Umbraco.Cms.Web.Common.Controllers { return new PublishedContentNotFoundResult(UmbracoContext); } - } + } } } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs index 0e6b6d0d0c..f9840df370 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoPageController.cs @@ -74,7 +74,7 @@ namespace Umbraco.Cms.Web.Common.Controllers /// The model. /// The action result. /// If the template found in the route values doesn't physically exist and exception is thrown - protected IActionResult CurrentTemplate(T model) + protected virtual IActionResult CurrentTemplate(T model) { if (EnsurePhsyicalViewExists(UmbracoRouteValues.TemplateName) == false) { @@ -90,6 +90,13 @@ namespace Umbraco.Cms.Web.Common.Controllers /// The view name. protected bool EnsurePhsyicalViewExists(string template) { + if (string.IsNullOrWhiteSpace(template)) + { + string docTypeAlias = UmbracoRouteValues.PublishedRequest.PublishedContent.ContentType.Alias; + _logger.LogWarning("No physical template file was found for document type with alias {Alias}", docTypeAlias); + return false; + } + ViewEngineResult result = _compositeViewEngine.FindView(ControllerContext, template, false); if (result.View != null) { @@ -99,6 +106,5 @@ namespace Umbraco.Cms.Web.Common.Controllers _logger.LogWarning("No physical template file was found for template {Template}", template); return false; } - } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs index 628345dcd6..f8897e522c 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/ImageSharpConfigurationOptions.cs @@ -7,7 +7,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection /// /// Configures the ImageSharp middleware options to use the registered configuration. /// - /// + /// public sealed class ImageSharpConfigurationOptions : IConfigureOptions { /// @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Web.Common.DependencyInjection public ImageSharpConfigurationOptions(Configuration configuration) => _configuration = configuration; /// - /// Invoked to configure a instance. + /// Invoked to configure an instance. /// /// The options instance to configure. public void Configure(ImageSharpMiddlewareOptions options) => options.Configuration = _configuration; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs index 4d621d348c..30331fd812 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.ImageSharp.cs @@ -74,6 +74,7 @@ namespace Umbraco.Extensions .Configure(options => options.CacheFolder = builder.BuilderHostingEnvironment.MapPathContentRoot(imagingSettings.Cache.CacheFolder)) .AddProcessor(); + // Configure middleware to use the registered/shared ImageSharp configuration builder.Services.AddTransient, ImageSharpConfigurationOptions>(); return builder.Services; diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs index c6856f8f19..98391d7590 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilder.MembersIdentity.cs @@ -1,7 +1,18 @@ +using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Security; using Umbraco.Cms.Web.Common.Security; namespace Umbraco.Extensions @@ -34,14 +45,26 @@ namespace Umbraco.Extensions services.AddIdentity() .AddDefaultTokenProviders() - .AddUserStore() + .AddUserStore, MemberUserStore>(factory => new MemberUserStore( + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService(), + factory.GetRequiredService() + )) .AddRoleStore() .AddRoleManager() .AddMemberManager() .AddSignInManager() + .AddSignInManager() .AddErrorDescriber() .AddUserConfirmation>(); + + builder.AddNotificationHandler(); + builder.AddNotificationAsyncHandler(); services.ConfigureOptions(); services.AddScoped(x => (IMemberUserStore)x.GetRequiredService>()); @@ -50,6 +73,8 @@ namespace Umbraco.Extensions services.ConfigureOptions(); services.ConfigureOptions(); + services.AddUnique(); + return builder; } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 247e364ba4..172a093c3c 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Data.Common; -using System.Data.SqlClient; using System.IO; using System.Linq; using System.Reflection; @@ -11,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -109,7 +109,6 @@ namespace Umbraco.Extensions var appCaches = AppCaches.Create(requestCache); services.ConfigureOptions(); - services.ConfigureOptions(); services.ConfigureOptions(); IProfiler profiler = GetWebProfiler(config); diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 832b9dd92d..8f739ab542 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -1,18 +1,20 @@ using System; using System.IO; +using Dazinator.Extensions.FileProviders.PrependBasePath; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Serilog.Context; using StackExchange.Profiling; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging.Serilog.Enrichers; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.Plugins; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions { @@ -94,7 +96,8 @@ namespace Umbraco.Extensions throw new ArgumentNullException(nameof(app)); } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + return app; app.UseMiddleware(); @@ -109,25 +112,21 @@ namespace Umbraco.Extensions public static IApplicationBuilder UseUmbracoPluginsStaticFiles(this IApplicationBuilder app) { var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); var pluginFolder = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.AppPlugins); - - // Ensure the plugin folder exists - Directory.CreateDirectory(pluginFolder); - - var fileProvider = new UmbracoPluginPhysicalFileProvider( - pluginFolder, - umbracoPluginSettings); - - app.UseStaticFiles(new StaticFileOptions + if (Directory.Exists(pluginFolder)) { - FileProvider = fileProvider, - RequestPath = Constants.SystemDirectories.AppPlugins - }); + var umbracoPluginSettings = app.ApplicationServices.GetRequiredService>(); + + var pluginFileProvider = new UmbracoPluginPhysicalFileProvider( + pluginFolder, + umbracoPluginSettings); + + IWebHostEnvironment webHostEnvironment = app.ApplicationServices.GetService(); + webHostEnvironment.WebRootFileProvider = webHostEnvironment.WebRootFileProvider.ConcatComposite(new PrependBasePathFileProvider(Constants.SystemDirectories.AppPlugins, pluginFileProvider)); + } return app; } } - } diff --git a/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs new file mode 100644 index 0000000000..35a2882bdd --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/FileProviderExtensions.cs @@ -0,0 +1,19 @@ +using System.Linq; +using Microsoft.Extensions.FileProviders; + +namespace Umbraco.Extensions +{ + internal static class FileProviderExtensions + { + public static IFileProvider ConcatComposite(this IFileProvider fileProvider, params IFileProvider[] fileProviders) + { + var existingFileProviders = fileProvider switch + { + CompositeFileProvider compositeFileProvider => compositeFileProvider.FileProviders, + _ => new[] { fileProvider } + }; + + return new CompositeFileProvider(existingFileProviders.Concat(fileProviders)); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs index c7c2bb3115..2aeb2555eb 100644 --- a/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/HttpRequestExtensions.cs @@ -1,9 +1,12 @@ -using System.IO; +using System; +using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Routing; namespace Umbraco.Extensions @@ -107,5 +110,31 @@ namespace Umbraco.Extensions return result; } } + + /// + /// Gets the application URI, will use the one specified in settings if present + /// + public static Uri GetApplicationUri(this HttpRequest request, WebRoutingSettings routingSettings) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + if (routingSettings == null) + { + throw new ArgumentNullException(nameof(routingSettings)); + } + + if (string.IsNullOrEmpty(routingSettings.UmbracoApplicationUrl)) + { + var requestUri = new Uri(request.GetDisplayUrl()); + + // Create a new URI with the relative uri as /, this ensures that only the base path is returned. + return new Uri(requestUri, "/"); + } + + return new Uri(routingSettings.UmbracoApplicationUrl); + } } } diff --git a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs index 77b9f6c8dd..9b80f3e82a 100644 --- a/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/IdentityBuilderExtensions.cs @@ -1,7 +1,9 @@ +using System; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Infrastructure.Security; namespace Umbraco.Extensions { @@ -50,5 +52,22 @@ namespace Umbraco.Extensions identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager)); return identityBuilder; } + + + public static IdentityBuilder AddUserStore(this IdentityBuilder identityBuilder, Func implementationFactory) + where TStore : class, TInterface + { + identityBuilder.Services.AddScoped(typeof(TInterface), implementationFactory); + return identityBuilder; + } + + public static MemberIdentityBuilder AddTwoFactorProvider(this MemberIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider + { + identityBuilder.Services.AddSingleton(); + identityBuilder.Services.AddSingleton(); + identityBuilder.AddTokenProvider>(providerName); + + return identityBuilder; + } } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs index 64fde06ac8..e7c0246f40 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoApplicationBuilder.Identity.cs @@ -20,5 +20,33 @@ namespace Umbraco.Extensions builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); return builder; } + + public static IUmbracoBuilder SetBackOfficeUserStore(this IUmbracoBuilder builder) + where TUserStore : BackOfficeUserStore + { + Type customType = typeof(TUserStore); + builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(BackOfficeIdentityUser)), customType)); + return builder; + } + + public static IUmbracoBuilder SetMemberManager(this IUmbracoBuilder builder) + where TUserManager : UserManager, IMemberManager + { + + Type customType = typeof(TUserManager); + Type userManagerType = typeof(UserManager); + builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IMemberManager), customType)); + builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType)); + builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType)); + return builder; + } + + public static IUmbracoBuilder SetMemberUserStore(this IUmbracoBuilder builder) + where TUserStore : MemberUserStore + { + Type customType = typeof(TUserStore); + builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(MemberIdentityUser)), customType)); + return builder; + } } } diff --git a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs index 36adacc2d2..8e62ca09cf 100644 --- a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Serialization; @@ -16,6 +18,7 @@ namespace Umbraco.Extensions public const string TokenUmbracoVersion = "UmbracoVersion"; public const string TokenExternalSignInError = "ExternalSignInError"; public const string TokenPasswordResetCode = "PasswordResetCode"; + public const string TokenTwoFactorRequired = "TwoFactorRequired"; public static bool FromTempData(this ViewDataDictionary viewData, ITempDataDictionary tempData, string token) { @@ -135,5 +138,16 @@ namespace Umbraco.Extensions { viewData[TokenPasswordResetCode] = value; } + + public static void SetTwoFactorProviderNames(this ViewDataDictionary viewData, IEnumerable providerNames) + { + viewData[TokenTwoFactorRequired] = providerNames; + } + + public static bool TryGetTwoFactorProviderNames(this ViewDataDictionary viewData, out IEnumerable providerNames) + { + providerNames = viewData[TokenTwoFactorRequired] as IEnumerable; + return providerNames is not null; + } } } diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 2b83d1e33a..9df64c64ad 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -20,7 +19,6 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.Common.Profiler; using Umbraco.Extensions; @@ -45,7 +43,6 @@ namespace Umbraco.Cms.Web.Common.Middleware private readonly IUmbracoContextFactory _umbracoContextFactory; private readonly IRequestCache _requestCache; - private readonly PublishedSnapshotServiceEventHandler _publishedSnapshotServiceEventHandler; private readonly IEventAggregator _eventAggregator; private readonly IHostingEnvironment _hostingEnvironment; private readonly UmbracoRequestPaths _umbracoRequestPaths; @@ -56,10 +53,6 @@ namespace Umbraco.Cms.Web.Common.Middleware private SmidgeOptions _smidgeOptions; private readonly WebProfiler _profiler; - private static bool s_cacheInitialized; - private static bool s_cacheInitializedFlag = false; - private static object s_cacheInitializedLock = new object(); - #pragma warning disable IDE0044 // Add readonly modifier private static bool s_firstBackOfficeRequest; private static bool s_firstBackOfficeReqestFlag; @@ -73,7 +66,6 @@ namespace Umbraco.Cms.Web.Common.Middleware ILogger logger, IUmbracoContextFactory umbracoContextFactory, IRequestCache requestCache, - PublishedSnapshotServiceEventHandler publishedSnapshotServiceEventHandler, IEventAggregator eventAggregator, IProfiler profiler, IHostingEnvironment hostingEnvironment, @@ -87,7 +79,6 @@ namespace Umbraco.Cms.Web.Common.Middleware _logger = logger; _umbracoContextFactory = umbracoContextFactory; _requestCache = requestCache; - _publishedSnapshotServiceEventHandler = publishedSnapshotServiceEventHandler; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; _umbracoRequestPaths = umbracoRequestPaths; @@ -117,8 +108,6 @@ namespace Umbraco.Cms.Web.Common.Middleware // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await _profiler?.UmbracoApplicationBeginRequest(context, _runtimeState.Level); - EnsureContentCacheInitialized(); - _variationContextAccessor.VariationContext ??= new VariationContext(_defaultCultureAccessor.DefaultCulture); UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); @@ -230,18 +219,5 @@ namespace Umbraco.Cms.Web.Common.Middleware IHttpScopeReference httpScopeReference = request.HttpContext.RequestServices.GetRequiredService(); httpScopeReference.Register(); } - - /// - /// Initializes the content cache one time - /// - private void EnsureContentCacheInitialized() => LazyInitializer.EnsureInitialized( - ref s_cacheInitialized, - ref s_cacheInitializedFlag, - ref s_cacheInitializedLock, - () => - { - _publishedSnapshotServiceEventHandler.Initialize(); - return true; - }); } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs index 4141669c1c..1c1460432f 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureIISServerOptions.cs @@ -1,18 +1,24 @@ +using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Web.Common.Security { + [Obsolete("This class is obsolete, as this does not configure your Maximum request length, see https://our.umbraco.com/documentation/Reference/V9-Config/MaximumUploadSizeSettings/ for information about configuring maximum request length")] public class ConfigureIISServerOptions : IConfigureOptions { private readonly IOptions _runtimeSettings; - public ConfigureIISServerOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; + public ConfigureIISServerOptions(IOptions runtimeSettings) => + _runtimeSettings = runtimeSettings; + public void Configure(IISServerOptions options) { // convert from KB to bytes - options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. + options.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue + ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 + : uint.MaxValue; // ~4GB is the max supported value for IIS and IIS express. } } } diff --git a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs index c11e0d8814..59ec330700 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureKestrelServerOptions.cs @@ -11,8 +11,8 @@ namespace Umbraco.Cms.Web.Common.Security public ConfigureKestrelServerOptions(IOptions runtimeSettings) => _runtimeSettings = runtimeSettings; public void Configure(KestrelServerOptions options) { - // convert from KB to bytes - options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : long.MaxValue; + // convert from KB to bytes, 52428800 bytes (50 MB) is the same as in the IIS settings + options.Limits.MaxRequestBodySize = _runtimeSettings.Value.MaxRequestLength.HasValue ? _runtimeSettings.Value.MaxRequestLength.Value * 1024 : 52428800; } } } diff --git a/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs new file mode 100644 index 0000000000..b3d6813c2f --- /dev/null +++ b/src/Umbraco.Web.Common/Security/IMemberExternalLoginProviders.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Umbraco.Cms.Web.Common.Security +{ + + /// + /// Service to return instances + /// + public interface IMemberExternalLoginProviders + { + /// + /// Get the for the specified scheme + /// + /// + /// + Task GetAsync(string authenticationType); + + /// + /// Get all registered + /// + /// + Task> GetMemberProvidersAsync(); + } + +} diff --git a/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs b/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs index cc6b0e88b9..4ba5caca9b 100644 --- a/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/IMemberSignInManager.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Umbraco.Cms.Core.Security; + namespace Umbraco.Cms.Web.Common.Security { public interface IMemberSignInManager diff --git a/src/Umbraco.Web.Common/Security/IMemberSignInManagerExternalLogins.cs b/src/Umbraco.Web.Common/Security/IMemberSignInManagerExternalLogins.cs new file mode 100644 index 0000000000..eb6a66a000 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/IMemberSignInManagerExternalLogins.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + [Obsolete("This interface will be merged with IMemberSignInManager in Umbraco 10")] + public interface IMemberSignInManagerExternalLogins : IMemberSignInManager + { + AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null); + Task GetExternalLoginInfoAsync(string expectedXsrf = null); + Task UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin); + Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false); + Task GetTwoFactorAuthenticationUserAsync(); + Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient); + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs new file mode 100644 index 0000000000..9681d47413 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProvider.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.Extensions.Options; + +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// An external login (OAuth) provider for the members + /// + public class MemberExternalLoginProvider : IEquatable + { + public MemberExternalLoginProvider( + string authenticationType, + IOptionsMonitor properties) + { + if (properties is null) + { + throw new ArgumentNullException(nameof(properties)); + } + + AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType)); + Options = properties.Get(authenticationType); + } + + /// + /// The authentication "Scheme" + /// + public string AuthenticationType { get; } + + public MemberExternalLoginProviderOptions Options { get; } + + public override bool Equals(object obj) => Equals(obj as MemberExternalLoginProvider); + public bool Equals(MemberExternalLoginProvider other) => other != null && AuthenticationType == other.AuthenticationType; + public override int GetHashCode() => HashCode.Combine(AuthenticationType); + } + +} diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs new file mode 100644 index 0000000000..ea93a522da --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderOptions.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// Options used to configure member external login providers + /// + public class MemberExternalLoginProviderOptions + { + public MemberExternalLoginProviderOptions( + MemberExternalSignInAutoLinkOptions autoLinkOptions = null, + bool autoRedirectLoginToExternalProvider = false, + string customBackOfficeView = null) + { + AutoLinkOptions = autoLinkOptions ?? new MemberExternalSignInAutoLinkOptions(); + } + + public MemberExternalLoginProviderOptions() + { + } + + /// + /// Options used to control how users can be auto-linked/created/updated based on the external login provider + /// + public MemberExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new MemberExternalSignInAutoLinkOptions(); + + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs new file mode 100644 index 0000000000..600405b638 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviderScheme.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.AspNetCore.Authentication; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class MemberExternalLoginProviderScheme + { + public MemberExternalLoginProviderScheme( + MemberExternalLoginProvider externalLoginProvider, + AuthenticationScheme authenticationScheme) + { + ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider)); + AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme)); + } + + public MemberExternalLoginProvider ExternalLoginProvider { get; } + public AuthenticationScheme AuthenticationScheme { get; } + } + +} diff --git a/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs new file mode 100644 index 0000000000..28102c434f --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberExternalLoginProviders.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Security +{ + + /// + public class MemberExternalLoginProviders : IMemberExternalLoginProviders + { + private readonly Dictionary _externalLogins; + private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; + + public MemberExternalLoginProviders( + IEnumerable externalLogins, + IAuthenticationSchemeProvider authenticationSchemeProvider) + { + _externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType); + _authenticationSchemeProvider = authenticationSchemeProvider; + } + + /// + public async Task GetAsync(string authenticationType) + { + var schemaName = + authenticationType.EnsureStartsWith(Core.Constants.Security.MemberExternalAuthenticationTypePrefix); + + if (!_externalLogins.TryGetValue(schemaName, out MemberExternalLoginProvider provider)) + { + return null; + } + + // get the associated scheme + AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType); + + if (associatedScheme == null) + { + throw new InvalidOperationException("No authentication scheme registered for " + provider.AuthenticationType); + } + + return new MemberExternalLoginProviderScheme(provider, associatedScheme); + } + + /// + public async Task> GetMemberProvidersAsync() + { + var providersWithSchemes = new List(); + foreach (MemberExternalLoginProvider login in _externalLogins.Values) + { + // get the associated scheme + AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType); + + providersWithSchemes.Add(new MemberExternalLoginProviderScheme(login, associatedScheme)); + } + + return providersWithSchemes; + } + + } + +} diff --git a/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs new file mode 100644 index 0000000000..42dcf6d56f --- /dev/null +++ b/src/Umbraco.Web.Common/Security/MemberExternalSignInAutoLinkOptions.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Identity; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Security; +using SecurityConstants = Umbraco.Cms.Core.Constants.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + /// + /// Options used to configure auto-linking external OAuth providers + /// + public class MemberExternalSignInAutoLinkOptions + { + private readonly string _defaultCulture; + + /// + /// Initializes a new instance of the class. + /// + public MemberExternalSignInAutoLinkOptions( + bool autoLinkExternalAccount = false, + bool defaultIsApproved = true, + string defaultMemberTypeAlias = Core.Constants.Conventions.MemberTypes.DefaultAlias, + string defaultCulture = null, + IEnumerable defaultMemberGroups = null) + { + AutoLinkExternalAccount = autoLinkExternalAccount; + DefaultIsApproved = defaultIsApproved; + DefaultMemberTypeAlias = defaultMemberTypeAlias; + _defaultCulture = defaultCulture; + DefaultMemberGroups = defaultMemberGroups ?? Array.Empty(); + } + + /// + /// A callback executed during account auto-linking and before the user is persisted + /// + [IgnoreDataMember] + public Action OnAutoLinking { get; set; } + + /// + /// A callback executed during every time a user authenticates using an external login. + /// returns a boolean indicating if sign in should continue or not. + /// + [IgnoreDataMember] + public Func OnExternalLogin { get; set; } + + /// + /// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a + /// local user + /// + public bool AutoLinkExternalAccount { get; } + + /// + /// Gets the member type alias that auto linked members are created as + /// + public string DefaultMemberTypeAlias { get; } + + /// + /// Gets the IsApproved value for auto linked members. + /// + public bool DefaultIsApproved { get; } + + /// + /// Gets the default member groups to add the user in. + /// + public IEnumerable DefaultMemberGroups { get; } + + /// + /// The default Culture to use for auto-linking users + /// + // TODO: Should we use IDefaultCultureAccessor here instead? + public string GetUserAutoLinkCulture(GlobalSettings globalSettings) => + _defaultCulture ?? globalSettings.DefaultUILanguage; + } +} diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index 52bfd6a58b..08fd280b0e 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -45,6 +45,9 @@ namespace Umbraco.Cms.Web.Common.Security _httpContextAccessor = httpContextAccessor; } + /// + public override bool SupportsUserTwoFactor => true; + /// public async Task IsMemberAuthorizedAsync(IEnumerable allowTypes = null, IEnumerable allowGroups = null, IEnumerable allowMembers = null) { diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs index 40cc17667d..e8bf1c2eb3 100644 --- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs @@ -1,12 +1,20 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Principal; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Security { @@ -14,8 +22,28 @@ namespace Umbraco.Cms.Web.Common.Security /// /// The sign in manager for members /// - public class MemberSignInManager : UmbracoSignInManager, IMemberSignInManager + public class MemberSignInManager : UmbracoSignInManager, IMemberSignInManagerExternalLogins { + private readonly IMemberExternalLoginProviders _memberExternalLoginProviders; + private readonly IEventAggregator _eventAggregator; + + public MemberSignInManager( + UserManager memberManager, + IHttpContextAccessor contextAccessor, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation, + IMemberExternalLoginProviders memberExternalLoginProviders, + IEventAggregator eventAggregator) : + base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + _memberExternalLoginProviders = memberExternalLoginProviders; + _eventAggregator = eventAggregator; + } + + [Obsolete("Use ctor with all params")] public MemberSignInManager( UserManager memberManager, IHttpContextAccessor contextAccessor, @@ -24,7 +52,9 @@ namespace Umbraco.Cms.Web.Common.Security ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : - base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + this(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } // use default scheme for members @@ -40,40 +70,312 @@ namespace Umbraco.Cms.Web.Common.Security protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme; /// - public override Task GetTwoFactorAuthenticationUserAsync() - => throw new NotImplementedException("Two factor is not yet implemented for members"); + public override async Task GetExternalLoginInfoAsync(string expectedXsrf = null) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422 + // to replace the auth scheme - /// - public override Task TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient) - => throw new NotImplementedException("Two factor is not yet implemented for members"); + var auth = await Context.AuthenticateAsync(ExternalAuthenticationType); + var items = auth?.Properties?.Items; + if (auth?.Principal == null || items == null) + { + Logger.LogDebug(auth?.Failure ?? new NullReferenceException("Context.AuthenticateAsync(ExternalAuthenticationType) is null"), + "The external login authentication failed. No user Principal or authentication items was resolved."); + return null; + } - /// - public override Task IsTwoFactorClientRememberedAsync(MemberIdentityUser user) - => throw new NotImplementedException("Two factor is not yet implemented for members"); + if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey)) + { + throw new InvalidOperationException($"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult."); + } - /// - public override Task RememberTwoFactorClientAsync(MemberIdentityUser user) - => throw new NotImplementedException("Two factor is not yet implemented for members"); + if (expectedXsrf != null) + { + if (!items.ContainsKey(UmbracoSignInMgrXsrfKey)) + { + return null; + } + var userId = items[UmbracoSignInMgrXsrfKey]; + if (userId != expectedXsrf) + { + return null; + } + } - /// - public override Task ForgetTwoFactorClientAsync() - => throw new NotImplementedException("Two factor is not yet implemented for members"); + var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier); + if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider) + { + return null; + } - /// - public override Task TwoFactorRecoveryCodeSignInAsync(string recoveryCode) - => throw new NotImplementedException("Two factor is not yet implemented for members"); + var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider; + return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName) + { + AuthenticationTokens = auth.Properties.GetTokens(), + AuthenticationProperties = auth.Properties + }; + } - /// - public override Task GetExternalLoginInfoAsync(string expectedXsrf = null) - => throw new NotImplementedException("External login is not yet implemented for members"); + /// + /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking + /// + public async Task ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false) + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to be able to deal with auto-linking and reduce duplicate lookups + + var autoLinkOptions = (await _memberExternalLoginProviders.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider?.Options?.AutoLinkOptions; + var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (user == null) + { + // user doesn't exist so see if we can auto link + return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions); + } + + if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null) + { + var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo); + if (shouldSignIn == false) + { + LogFailedExternalLogin(loginInfo, user); + return ExternalLoginSignInResult.NotAllowed; + } + } + + var error = await PreSignInCheck(user); + if (error != null) + { + return error; + } + return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor); + } + + + /// + /// Used for auto linking/creating user accounts for external logins + /// + /// + /// + /// + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, MemberExternalSignInAutoLinkOptions autoLinkOptions) + { + // If there are no autolink options then the attempt is failed (user does not exist) + if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount) + { + return SignInResult.Failed; + } + + var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email); + + //we are allowing auto-linking/creating of local accounts + if (email.IsNullOrWhiteSpace()) + { + return AutoLinkSignInResult.FailedNoEmail; + } + else + { + //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address + var autoLinkUser = await UserManager.FindByEmailAsync(email); + if (autoLinkUser != null) + { + try + { + //call the callback if one is assigned + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + return AutoLinkSignInResult.FailedException(ex.Message); + } + + var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); + if (shouldLinkUser) + { + return await LinkUser(autoLinkUser, loginInfo); + } + else + { + LogFailedExternalLogin(loginInfo, autoLinkUser); + return ExternalLoginSignInResult.NotAllowed; + } + } + else + { + var name = loginInfo.Principal?.Identity?.Name; + if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null"); + + autoLinkUser = MemberIdentityUser.CreateNew(email, email, autoLinkOptions.DefaultMemberTypeAlias, autoLinkOptions.DefaultIsApproved, name); + + foreach (var userGroup in autoLinkOptions.DefaultMemberGroups) + { + autoLinkUser.AddRole(userGroup); + } + + //call the callback if one is assigned + try + { + autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo); + } + catch (Exception ex) + { + Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider); + return AutoLinkSignInResult.FailedException(ex.Message); + } + + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (!userCreationResult.Succeeded) + { + return AutoLinkSignInResult.FailedCreatingUser(userCreationResult.Errors.Select(x => x.Description).ToList()); + } + else + { + var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo); + if (shouldLinkUser) + { + return await LinkUser(autoLinkUser, loginInfo); + } + else + { + LogFailedExternalLogin(loginInfo, autoLinkUser); + return ExternalLoginSignInResult.NotAllowed; + } + } + } + } + } + + // TODO in v10 we can share this with backoffice by moving the backoffice into common. + public class ExternalLoginSignInResult : SignInResult + { + public static ExternalLoginSignInResult NotAllowed { get; } = new ExternalLoginSignInResult() + { + Succeeded = false + }; + } + // TODO in v10 we can share this with backoffice by moving the backoffice into common. + public class AutoLinkSignInResult : SignInResult + { + public static AutoLinkSignInResult FailedNotLinked { get; } = new AutoLinkSignInResult() + { + Succeeded = false + }; + + public static AutoLinkSignInResult FailedNoEmail { get; } = new AutoLinkSignInResult() + { + Succeeded = false + }; + + public static AutoLinkSignInResult FailedException(string error) => new AutoLinkSignInResult(new[] { error }) + { + Succeeded = false + }; + + public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors) + { + Succeeded = false + }; + + public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection errors) => new AutoLinkSignInResult(errors) + { + Succeeded = false + }; + + public AutoLinkSignInResult(IReadOnlyCollection errors) + { + Errors = errors ?? throw new ArgumentNullException(nameof(errors)); + } + + public AutoLinkSignInResult() + { + } + + public IReadOnlyCollection Errors { get; } = Array.Empty(); + } /// public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null) - => throw new NotImplementedException("External login is not yet implemented for members"); + { + // borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs + // to be able to use our own XsrfKey/LoginProviderKey because the default is private :/ + + var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; + properties.Items[UmbracoSignInMgrLoginProviderKey] = provider; + if (userId != null) + { + properties.Items[UmbracoSignInMgrXsrfKey] = userId; + } + return properties; + } /// public override Task> GetExternalAuthenticationSchemesAsync() - => throw new NotImplementedException("External login is not yet implemented for members"); + { + // That can be done by either checking the scheme (maybe) or comparing it to what we have registered in the collection of BackOfficeExternalLoginProvider + return base.GetExternalAuthenticationSchemesAsync(); + } + private async Task LinkUser(MemberIdentityUser autoLinkUser, ExternalLoginInfo loginInfo) + { + var existingLogins = await UserManager.GetLoginsAsync(autoLinkUser); + var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey); + + // if it already exists (perhaps it was added in the AutoLink callbak) then we just continue + if (exists != null) + { + //sign in + return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider); + } + + var linkResult = await UserManager.AddLoginAsync(autoLinkUser, loginInfo); + if (linkResult.Succeeded) + { + //we're good! sign in + return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider); + } + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded) + { + var errors = linkResult.Errors.Select(x => x.Description).ToList(); + return AutoLinkSignInResult.FailedLinkingUser(errors); + } + else + { + //DOH! ... this isn't good, combine all errors to be shown + var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList(); + return AutoLinkSignInResult.FailedLinkingUser(errors); + } + } + + private void LogFailedExternalLogin(ExternalLoginInfo loginInfo, MemberIdentityUser user) => + Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id); + + protected override async Task SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent, + string loginProvider = null, bool bypassTwoFactor = false) + { + var result = await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); + + if (result.RequiresTwoFactor) + { + NotifyRequiresTwoFactor(user); + } + + return result; + } + + protected void NotifyRequiresTwoFactor(MemberIdentityUser user) => Notify(user, + (currentUser) => new MemberTwoFactorRequestedNotification(currentUser.Key) + ); + + private T Notify(MemberIdentityUser currentUser, Func createNotification) where T : INotification + { + + var notification = createNotification(currentUser); + _eventAggregator.Publish(notification); + return notification; + } } } diff --git a/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs new file mode 100644 index 0000000000..32b3226440 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/TwoFactorValidationProvider.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; + +using Microsoft.Extensions.Logging; + +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 class TwoFactorMemberValidationProvider : TwoFactorValidationProvider + where TTwoFactorSetupGenerator : ITwoFactorProvider + { + public TwoFactorMemberValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions options, ILogger> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator) + { + } + + } + + public class TwoFactorValidationProvider + : DataProtectorTokenProvider + where TUmbracoIdentityUser : UmbracoIdentityUser + where TTwoFactorSetupGenerator : ITwoFactorProvider + { + private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly TTwoFactorSetupGenerator _generator; + + protected TwoFactorValidationProvider( + + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger, + ITwoFactorLoginService twoFactorLoginService, + TTwoFactorSetupGenerator generator) + : base(dataProtectionProvider, options, logger) + { + _twoFactorLoginService = twoFactorLoginService; + _generator = generator; + } + + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, + TUmbracoIdentityUser user) => Task.FromResult(_generator is not null); + + public override async Task ValidateAsync(string purpose, string token, + UserManager manager, TUmbracoIdentityUser user) + { + var secret = + await _twoFactorLoginService.GetSecretForUserAndProviderAsync(GetUserKey(user), _generator.ProviderName); + + if (secret is null) + { + return false; + } + + var validToken = _generator.ValidateTwoFactorPIN(secret, token); + + + return validToken; + } + + protected Guid GetUserKey(TUmbracoIdentityUser user) + { + + switch (user) + { + case MemberIdentityUser memberIdentityUser: + return memberIdentityUser.Key; + case BackOfficeIdentityUser backOfficeIdentityUser: + return backOfficeIdentityUser.Key; + default: + throw new NotSupportedException( + "Current we only support MemberIdentityUser and BackOfficeIdentityUser"); + } + + } + + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 0c01f07da8..b023b5ecdc 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js index e8a40e9d70..55390bb520 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/dependencies.js @@ -134,7 +134,7 @@ function dependencies() { "./node_modules/angular-messages/angular-messages.min.js.map" ], "base": "./node_modules/angular-messages" - }, + }, { "name": "angular-mocks", "src": ["./node_modules/angular-mocks/angular-mocks.js"], @@ -285,11 +285,11 @@ function dependencies() { // add streams for node modules nodeModules.forEach(module => { var task = gulp.src(module.src, { base: module.base, allowEmpty: true }); - + _.forEach(config.roots, function(root){ task = task.pipe(gulp.dest(root + config.targets.lib + "/" + module.name)) }); - + stream.add(task); }); @@ -299,12 +299,12 @@ function dependencies() { _.forEach(config.roots, function(root){ libTask = libTask.pipe(gulp.dest(root + config.targets.lib)) }); - + stream.add(libTask); //Copies all static assets into /root / assets folder //css, fonts and image files - + var assetsTask = gulp.src(config.sources.globs.assets, { allowEmpty: true }); assetsTask = assetsTask.pipe(imagemin([ imagemin.gifsicle({interlaced: true}), @@ -321,8 +321,8 @@ function dependencies() { _.forEach(config.roots, function(root){ assetsTask = assetsTask.pipe(gulp.dest(root + config.targets.assets)); }); - - + + stream.add(assetsTask); // Copies all the less files related to the preview into their folder @@ -342,13 +342,13 @@ function dependencies() { configTask = configTask.pipe(gulp.dest(root + config.targets.views + "/propertyeditors/grid/config")); }); stream.add(configTask); - + var dashboardTask = gulp.src("src/views/dashboard/default/*.jpg", { allowEmpty: true }); _.forEach(config.roots, function(root){ dashboardTask = dashboardTask .pipe(gulp.dest(root + config.targets.views + "/dashboard/default")); }); stream.add(dashboardTask); - + return stream; }; diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index ffc5f061cc..81fa1d79cd 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -31,6 +31,8 @@ const coreBuild = parallel(dependencies, js, less, views); // *********************************************************** exports.build = series(coreBuild, testUnit); +exports.buildDev = series(setDevelopmentMode, coreBuild); + exports.coreBuild = coreBuild; exports.dev = series(setDevelopmentMode, coreBuild, runUnitTestServer, watchTask); exports.watch = series(watchTask); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index a9b706a677..0145421fa9 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -8256,12 +8256,6 @@ "object-visit": "^1.0.0" } }, - "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", - "dev": true - }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 43d7a3cecd..ef8135487a 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -7,6 +7,7 @@ "e2e": "gulp testE2e", "build": "gulp build", "build:skip-tests": "gulp coreBuild", + "build:dev": "gulp buildDev", "dev": "gulp dev", "fastdev": "gulp fastdev", "watch": "gulp watch" @@ -87,7 +88,6 @@ "karma-spec-reporter": "0.0.32", "less": "3.10.3", "lodash": "4.17.21", - "marked": "^0.7.0", "merge-stream": "2.0.0", "run-sequence": "2.2.1" } diff --git a/src/Umbraco.Web.UI.Client/src/assets/fonts/web.config b/src/Umbraco.Web.UI.Client/src/assets/fonts/web.config deleted file mode 100644 index 42051b6de2..0000000000 --- a/src/Umbraco.Web.UI.Client/src/assets/fonts/web.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index 5492fee1a0..6656f370d0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -117,12 +117,11 @@ vm.files = _.map(files, function (file) { var f = { fileName: file, + fileSrc: file, isImage: mediaHelper.detectIfImageByExtension(file), extension: getExtension(file) }; - f.fileSrc = getThumbnail(f); - return f; }); @@ -190,21 +189,6 @@ } } - function getThumbnail(file) { - - if (file.extension === 'svg') { - return file.fileName; - } - - if (!file.isImage) { - return null; - } - - var thumbnailUrl = mediaHelper.getThumbnailFromPath(file.fileName); - - return thumbnailUrl; - } - function getExtension(fileName) { var extension = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length); return extension.toLowerCase(); @@ -238,7 +222,8 @@ isImage: isImage, extension: extension, fileName: files[i].name, - isClientSide: true + isClientSide: true, + fileData: files[i] }; // Save the file object to the files collection @@ -247,6 +232,7 @@ //special check for a comma in the name newVal += files[i].name.split(',').join('-') + ","; + // TODO: I would love to remove this part. But I'm affright it would be breaking if removed. Its not used by File upload anymore as each preview handles the client-side data on their own. if (isImage || extension === "svg") { var deferred = $q.defer(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index d94bb4e6be..ff05656a4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -152,12 +152,12 @@ function entityResource($q, $http, umbRequestHelper) { $http.post( umbRequestHelper.getApiUrl( "entityApiBaseUrl", - "GetUrlsByUdis", + "GetUrlsByIds", query), { - udis: udis + ids: ids }), - 'Failed to retrieve url map for udis ' + udis); + 'Failed to retrieve url map for ids ' + ids); }, getUrlByUdi: function (udi, culture) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js new file mode 100644 index 0000000000..b922e07c9c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js @@ -0,0 +1,94 @@ +/** +* @ngdoc service +* @name umbraco.services.mediaPreview +* @description A service providing views used for dealing with previewing files. +* +* ##usage +* The service allows for registering and retrieving the view for one or more file extensions. +* +* You can register your own custom view in this way: +* +*
+*    angular.module('umbraco').run(['mediaPreview', function (mediaPreview) {
+*        mediaPreview.registerPreview(['docx'], "app_plugins/My_PACKAGE/preview.html");
+*    }]);
+* 
+* +* Here is a example of a preview template. (base on the audio-preview). +* +*
+*   
+*    
+* 
+* +* Notice that there often is a need to differentiate based on the file-data origin. In the state of the file still begin located locally its often needed to create an Object-URL for the data to be useable in HTML. As well you might want to provide links for the uploaded file when it is uploaded to the server. See 'vm.clientSide' and 'vm.clientSideData'. +* +**/ +function mediaPreview() { + + const DEFAULT_FILE_PREVIEW = "views/components/media/umbfilepreview/umb-file-preview.html"; + + var _mediaPreviews = []; + + function init(service) { + service.registerPreview(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes.split(","), "views/components/media/umbimagepreview/umb-image-preview.html"); + service.registerPreview(["svg"], "views/components/media/umbimagepreview/umb-image-preview.html"); + service.registerPreview(["mp4", "mov", "webm", "ogv"], "views/components/media/umbvideopreview/umb-video-preview.html"); + service.registerPreview(["mp3", "weba", "oga", "opus"], "views/components/media/umbaudiopreview/umb-audio-preview.html"); + } + + var service = { + + /** + * @ngdoc method + * @name umbraco.services.mediaPreview#getMediaPreview + * @methodOf umbraco.services.mediaPreview + * + * @param {string} fileExtension A string with the file extension, example: "pdf" + * + * @description + * The registered view matching this file extensions will be returned. + * + */ + getMediaPreview: function (fileExtension) { + + fileExtension = fileExtension.toLowerCase(); + + var previewObject = _mediaPreviews.find((preview) => preview.fileExtensions.indexOf(fileExtension) !== -1); + + if(previewObject !== undefined) { + return previewObject.view; + } + + return DEFAULT_FILE_PREVIEW; + }, + + /** + * @ngdoc method + * @name umbraco.services.mediaPreview#registerPreview + * @methodOf umbraco.services.mediaPreview + * + * @param {array} fileExtensions An array of file extensions, example: ["pdf", "jpg"] + * @param {array} view A URL to the view to be used for these file extensions. + * + * @description + * The registered view will be used when file extensions match the given file. + * + */ + registerPreview: function (fileExtensions, view) { + _mediaPreviews.push({ + fileExtensions: fileExtensions.map(e => e.toLowerCase()), + view: view + }) + } + + }; + + init(service); + + return service; +} angular.module('umbraco.services').factory('mediaPreview', mediaPreview); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index ba4df33aa0..fa0543aeaf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -202,6 +202,11 @@ @import "components/contextdialogs/umb-dialog-datatype-delete.less"; @import "components/umbemailmarketing.less"; +@import "../views/components/media/umbmediapreview/umb-media-preview.less"; +@import "../views/components/media/umbaudiopreview/umb-audio-preview.less"; +@import "../views/components/media/umbfilepreview/umb-file-preview.less"; +@import "../views/components/media/umbimagepreview/umb-image-preview.less"; +@import "../views/components/media/umbvideopreview/umb-video-preview.less"; // Editors @import "../views/common/infiniteeditors/rollback/rollback.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less index 75d171dd87..5856b0bd04 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less @@ -38,4 +38,8 @@ border-color: @gray-1; } } + + .umb-property-file-upload--actions { + margin-top: 10px; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tags-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tags-editor.less index 213807e685..2d41cbe6f2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tags-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tags-editor.less @@ -1,4 +1,4 @@ -.umb-tags-editor { +.umb-tags-editor { border: @inputBorder solid 1px; padding: 5px; min-height: 54px; @@ -14,24 +14,26 @@ position: relative; user-select: all; + > .btn-icon { + color: @white; + padding: 0; + position: relative; + cursor: pointer; + padding-left: 2px; + font-size: 15px; + right: -5px; + bottom: -1px; + user-select: none; + } + .umb_confirm-action { - > .btn-icon { - color: @white; - padding: 0; - position: relative; - cursor: pointer; - padding-left: 2px; - font-size: 15px; - right: -5px; - bottom: -1px; - user-select: none; - } - - .umb_confirm-action__overlay.-left { - top: 8px; - left: auto; - right: 15px; + &__overlay { + &.-left { + top: 8px; + left: auto; + right: 15px; + } } } } 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 cf49af526b..2763a879ea 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -30,11 +30,14 @@ position: absolute; top: 22px; left: 25px; - width: 30px; height: 30px; z-index: 1; } +.login-overlay__logo > img { + max-height:100%; +} + .login-overlay .umb-modalcolumn { background: none; border: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 2805e7f79b..c3ad08b8f8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -828,11 +828,15 @@ .umb-fileupload { display: flex; flex-direction: column; + padding: 20px; + border: 1px solid @inputBorder; + box-sizing: border-box; + width: 100%; + .umb-property-editor--limit-width(); } .umb-fileupload .preview { border-radius: 5px; - border: 1px solid @gray-6; padding: 3px; background: @gray-9; float: left; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js index 6c8a038536..eea8e87034 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js @@ -38,6 +38,8 @@ angular.module("umbraco") entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) { vm.media = mediaEntity; vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.fileSrc = mediaHelper.resolveFileFromEntity(mediaEntity, false); + vm.fileExtension = mediaHelper.getFileExtension(vm.fileSrc); vm.loading = false; vm.hasDimensions = false; vm.isCroppable = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html index 938d719431..a56e3aeed6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -1,127 +1,148 @@ -
+
+ + + + - +
+
+ + This item is in the Recycle Bin +
- +
+
+ - - - -
- -
- This item is in the Recycle Bin -
- -
-
- - - - -
- -
- -
- - - - - -
- -
- - - - - - - -
- - - -
-
- -
-
+ +
+
+
+ + +
- +
+ + - + + - +
+ + + +
+
+
+
+
- - + + - - + + + - - - -
- + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less index 139d7bef4a..982ef7bc63 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less @@ -110,11 +110,23 @@ } .umb-media-entry-editor__imageholder { - display: flex; - align-items: center; - justify-content: center; + position: relative; height: calc(100% - 50px); + + display: block; +} + +.umb-media-entry-editor__previewholder { + + position: relative; + height: calc(100% - 50px); + + display: flex; + justify-content: center; + align-items: center; + + overflow-y: auto; } .umb-media-entry-editor__imageholder-actions { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index fa85785868..24acef995e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -69,7 +69,7 @@ ng-click="unlink($event, login.authType, login.linkedProviderKey)" ng-if="login.linkedProviderKey != undefined" value="{{login.authType}}"> - + Un-link your {{login.caption}} account diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.html new file mode 100644 index 0000000000..4b7bd1c2a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.less new file mode 100644 index 0000000000..5d69fa07f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umb-audio-preview.less @@ -0,0 +1,18 @@ +.umb-audio-preview { + display: flex; + justify-content: center; + align-items: center; + audio { + max-width: 100%; + } + audio::-webkit-media-controls-panel { + background-color: white; + } + audio::-webkit-media-controls { + padding: 6px; + } + audio::-webkit-media-controls-enclosure { + &:extend(.shadow-depth-1); + border-radius: 6px; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umbaudiopreview.controller.js b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umbaudiopreview.controller.js new file mode 100644 index 0000000000..985c8540a3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbaudiopreview/umbaudiopreview.controller.js @@ -0,0 +1,11 @@ +angular.module("umbraco") + .controller("umbAudioPreviewController", + function () { + + var vm = this; + + vm.getClientSideUrl = function(source) { + return URL.createObjectURL(source); + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.html new file mode 100644 index 0000000000..2c72e27c82 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.html @@ -0,0 +1,18 @@ +
+ + +
{{vm.name}}
+
+
+ +
{{vm.name}}
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.less new file mode 100644 index 0000000000..427ac38244 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbfilepreview/umb-file-preview.less @@ -0,0 +1,13 @@ +.umb-file-preview { + display: flex; + justify-content: center; + align-items: center; + + .umb-file-preview--file { + display: block; + box-sizing: border-box; + text-align: center; + max-width: 320px; + padding: 10px; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.html new file mode 100644 index 0000000000..989f8ef093 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.html @@ -0,0 +1,6 @@ +
+ {{vm.name}} + + {{vm.name}} + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less new file mode 100644 index 0000000000..13f934c251 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less @@ -0,0 +1,9 @@ +.umb-image-preview { + display: flex; + justify-content: center; + align-items: center; + + img { + width: 100%; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umbimagepreview.controller.js b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umbimagepreview.controller.js new file mode 100644 index 0000000000..36eb3958e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umbimagepreview.controller.js @@ -0,0 +1,18 @@ + + + + +angular.module("umbraco") + .controller("umbImagePreviewController", + function (mediaHelper) { + + var vm = this; + + vm.getThumbnail = function(source) { + return mediaHelper.getThumbnailFromPath(source) || source; + } + vm.getClientSideUrl = function(sourceData) { + return URL.createObjectURL(sourceData); + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umb-media-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umb-media-preview.less new file mode 100644 index 0000000000..78c41eeab6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umb-media-preview.less @@ -0,0 +1,14 @@ +umb-media-preview { + position: relative; +} +.umb-media-preview { + position: relative; + width: 100%; + height: 100%; + + min-height: 240px; + + display: flex; + justify-content: center; + align-items: center; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umbmediapreview.component.js b/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umbmediapreview.component.js new file mode 100644 index 0000000000..b7b5536b0a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbmediapreview/umbmediapreview.component.js @@ -0,0 +1,38 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .component("umbMediaPreview", { + template: "
", + controller: UmbMediaPreviewController, + controllerAs: "vm", + bindings: { + extension: "<", + source: "<", + name: "<", + clientSide: " { + vm.loading = true; + }) + $scope.$on("mediaPreviewLoadingComplete", () => { + vm.loading = false; + }) + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.html new file mode 100644 index 0000000000..26003ab4c1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.html @@ -0,0 +1,8 @@ +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.less new file mode 100644 index 0000000000..3dd4e2f589 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umb-video-preview.less @@ -0,0 +1,10 @@ +.umb-video-preview { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + video { + max-width: 100%; + max-height: 100%; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umbvideopreview.controller.js b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umbvideopreview.controller.js new file mode 100644 index 0000000000..9c8b32d8b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbvideopreview/umbvideopreview.controller.js @@ -0,0 +1,15 @@ + + + + +angular.module("umbraco") + .controller("umbVideoPreviewController", + function () { + + var vm = this; + + vm.getClientSideUrl = function(source) { + return URL.createObjectURL(source); + } + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html index 76e1e99314..e54cc3e898 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html @@ -30,7 +30,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js index 24b20367aa..1014b95227 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js @@ -74,6 +74,7 @@ vm.media = mediaEntity; checkErrorState(); vm.thumbnail = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.fileExtension = mediaHelper.getFileExtension(vm.media.metaData.MediaPath); vm.loading = false; }, function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html b/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html index e8f77d09a5..7277ff63c2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tags/umb-tags-editor.html @@ -13,6 +13,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index ce5d7292c9..a6df878997 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,58 +1,68 @@ 
+ + - - +
+ +

Click to upload

+ +
-
- -

Click to upload

- +
+
+
+
- -
-
- -
- -
-
-
- {{file.fileName}} - - {{file.fileName}} - -
-
-
- -
- - - -
{{file.fileName}}
-
-
- - -
{{file.fileName}}
-
-
-
-
- - - -
- -
-
-
-
+
+ + +
- - +
+
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/web.config b/src/Umbraco.Web.UI.Client/src/web.config deleted file mode 100644 index 6903c39608..0000000000 --- a/src/Umbraco.Web.UI.Client/src/web.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6887bd7f63..56511315ac 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -103,8 +103,6 @@ - - diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/EditProfile.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/EditProfile.cshtml index 09954b3f8d..1b1ebd7284 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/EditProfile.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/EditProfile.cshtml @@ -1,12 +1,14 @@ @inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage -@using Umbraco.Cms.Core -@using Umbraco.Cms.Core.Security +@using Umbraco.Cms.Core.Services +@using Umbraco.Cms.Web.Common.Security @using Umbraco.Cms.Web.Website.Controllers @using Umbraco.Cms.Web.Website.Models @using Umbraco.Extensions @inject MemberModelBuilderFactory memberModelBuilderFactory; - +@inject IMemberExternalLoginProviders memberExternalLoginProviders +@inject IExternalLoginWithKeyService externalLoginWithKeyService @{ + // Build a profile model to edit var profileModel = await memberModelBuilderFactory .CreateProfileModel() @@ -17,6 +19,13 @@ .BuildForCurrentMemberAsync(); var success = TempData["FormSuccess"] != null; + + var loginProviders = await memberExternalLoginProviders.GetMemberProvidersAsync(); + var externalSignInError = ViewData.GetExternalSignInProviderErrors(); + + var currentExternalLogin = profileModel is null + ? new Dictionary() + : externalLoginWithKeyService.GetExternalLogins(profileModel.Key).ToDictionary(x=>x.LoginProvider, x=>x.ProviderKey); } @@ -70,5 +79,50 @@ } + + if (loginProviders.Any()) + { +
+

Link external accounts

+ + if (externalSignInError?.AuthenticationType is null && externalSignInError?.Errors.Any() == true) + { + @Html.DisplayFor(x => externalSignInError.Errors); + } + + @foreach (var login in loginProviders) + { + if (currentExternalLogin.TryGetValue(login.ExternalLoginProvider.AuthenticationType, out var providerKey)) + { + @using (Html.BeginUmbracoForm(nameof(UmbExternalLoginController.Disassociate))) + { + + + + if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType) + { + @Html.DisplayFor(x => externalSignInError.Errors); + } + } + } + else + { + @using (Html.BeginUmbracoForm(nameof(UmbExternalLoginController.LinkLogin))) + { + + + if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType) + { + @Html.DisplayFor(x => externalSignInError.Errors); + } + } + } + + } + } } } diff --git a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Login.cshtml b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Login.cshtml index d3c389c78d..7ba7f2acca 100644 --- a/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Login.cshtml +++ b/src/Umbraco.Web.UI/umbraco/PartialViewMacros/Templates/Login.cshtml @@ -1,9 +1,12 @@ @inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage -@using Microsoft.AspNetCore.Http.Extensions -@using Umbraco.Cms.Web.Common.Models -@using Umbraco.Cms.Web.Website.Controllers -@using Umbraco.Extensions +@using Umbraco.Cms.Web.Common.Models +@using Umbraco.Cms.Web.Common.Security +@using Umbraco.Cms.Web.Website.Controllers +@using Umbraco.Cms.Core.Services +@using Umbraco.Extensions +@inject IMemberExternalLoginProviders memberExternalLoginProviders +@inject ITwoFactorLoginService twoFactorLoginService @{ var loginModel = new LoginModel(); // You can modify this to redirect to a different URL instead of the current one @@ -14,32 +17,91 @@ + +@if (ViewData.TryGetTwoFactorProviderNames(out var providerNames)) +{ + + foreach (var providerName in providerNames) + { +
+

Two factor with @providerName.

+
+ @using (Html.BeginUmbracoForm(nameof(UmbTwoFactorLoginController.Verify2FACode))) + { + + + + Input security code:
+ +
+
+ } +
+ } + +} +else +{ + + +} diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml index af701cd5e3..fbbf9a495a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cs.xml @@ -19,7 +19,7 @@ Vyprázdnit koš Aktivovat Exportovat typ dokumentu - Importovat typ dokumentu + Importovat typ dokumentu Importovat balíček Editovat na stránce Odhlásit @@ -60,7 +60,7 @@ Ostatní - Povolit přístup k přiřazování kultury a názvů hostitelů + Povolit přístup k přiřazování kultury a názvů hostitelů Povolit přístup k zobrazení protokolu historie uzlu Povolit přístup k zobrazení uzlu Povolit přístup ke změně typu dokumentu daného uzlu diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml index 0692d01e7a..94fde5ebfe 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/cy.xml @@ -5,7 +5,7 @@ https://www.method4.co.uk/ - Diwylliannau ac Enwau Gwesteia + Diwylliannau ac Enwau Gwesteia Trywydd Archwilio Dewis Nod Newid Math o Ddogfen @@ -21,7 +21,7 @@ Gwagu bin ailgylchu Galluogi Allforio Math o Ddogfen - Mewnforio Math o Ddogfen + Mewnforio Math o Ddogfen Mewnforio Pecyn Golygu mewn Cynfas Gadael diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 27901846a9..5b90b7511d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Tilføj domæne + Tilføj domæne Revisionsspor Gennemse elementer Skift Dokument Type @@ -21,7 +21,7 @@ Tøm papirkurv Aktivér Eksportér dokumenttype - Importér dokumenttype + Importér dokumenttype Importér pakke Redigér i Canvas Log af diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index 8f2ba350d0..0366fe6853 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Kulturen und Hostnamen + Kulturen und Hostnamen Protokoll Durchsuchen Dokumenttyp ändern @@ -19,7 +19,7 @@ Papierkorb leeren Aktivieren Dokumenttyp exportieren - Dokumenttyp importieren + Dokumenttyp importieren Paket importieren 'Canvas'-Modus starten Abmelden diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 468a0cc735..4fc52fc0a7 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture and Hostnames + Culture and Hostnames Audit Trail Browse Node Change Document Type @@ -20,7 +20,7 @@ Empty recycle bin Enable Export Document Type - Import Document Type + Import Document Type Import Package Edit in Canvas Exit @@ -2290,6 +2290,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index aef05ca973..2d575ba77f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture and Hostnames + Culture and Hostnames Audit Trail Browse Node Change Document Type @@ -21,7 +21,7 @@ Empty recycle bin Enable Export Document Type - Import Document Type + Import Document Type Import Package Edit in Canvas Exit @@ -2372,6 +2372,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont + %0%.]]> + The appSetting 'Umbraco:CMS:WebRouting:UmbracoApplicationUrl' is not set. X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml index df78683aca..ec8dfbc895 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/es.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Administrar dominios + Administrar dominios Historial Nodo de Exploración Cambiar tipo de documento @@ -18,7 +18,7 @@ Vaciar Papelera Activar Exportar Documento (tipo) - Importar Documento (tipo) + Importar Documento (tipo) Importar Paquete Editar en vivo Cerrar sesión diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml index 4681818c47..4f292c0889 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/fr.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Culture et noms d'hôte + Culture et noms d'hôte Informations d'audit Parcourir Changer le type de document @@ -19,7 +19,7 @@ Vider la corbeille Activer Exporter le type de document - Importer un type de document + Importer un type de document Importer un package Editer dans Canvas Déconnexion diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml index 9ee8bbf014..403ea645fc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/he.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - נהל שמות מתחם + נהל שמות מתחם מעקב ביקורות צפה בתוכן העתק @@ -15,7 +15,7 @@ נטרל רוקן סל מיחזור ייצא סוג קובץ - ייבא סוג מסמך + ייבא סוג מסמך ייבא חבילה ערוך במצב "קנבס" יציאה diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml index a0d89bff2d..357d63e84b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/it.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Gestisci hostnames + Gestisci hostnames Audit Trail Sfoglia Cambia tipo di documento @@ -21,7 +21,7 @@ Svuota il cestino Abilita Esporta il tipo di documento - Importa il tipo di documento + Importa il tipo di documento Importa il pacchetto Modifica in Area di Lavoro Uscita diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml index 4b98adad26..1107181cab 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - ドメインの割り当て + ドメインの割り当て 動作記録 ノードの参照 ドキュメントタイプの変更 @@ -16,7 +16,7 @@ 無効 ごみ箱を空にする ドキュメントタイプの書出 - ドキュメントタイプの読込 + ドキュメントタイプの読込 パッケージの読み込み ライブ編集 ログアウト diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml index 792dd6700c..68cff8a318 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ko.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 호스트명 관리 + 호스트명 관리 감사 추적 노드 탐색 복사 @@ -15,7 +15,7 @@ 비활성 휴지통 비우기 추출 문서 유형 - 등록 문서 유형 + 등록 문서 유형 패키지 등록 캔버스 내용 편집 종료 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml index 1c47969189..341adad4bf 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nb.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Angi domene + Angi domene Revisjoner Bla gjennom Skift dokumenttype @@ -16,7 +16,7 @@ Deaktiver Tøm papirkurv Eksporter dokumenttype - Importer dokumenttype + Importer dokumenttype Importer pakke Rediger i Canvas Logg av diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index f830c3368d..fb6167520b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Beheer domeinnamen + Beheer domeinnamen Documentgeschiedenis Node bekijken Documenttype wijzigen @@ -21,7 +21,7 @@ Prullenbak leegmaken Inschakelen Documenttype exporteren - Documenttype importeren + Documenttype importeren Package importeren Aanpassen in Canvas Afsluiten @@ -770,6 +770,7 @@ Een ogenblik geduld aub... Vorige Eigenschappen + Lees meer Opnieuw opbouwen E-mail om formulier resultaten te ontvangen Prullenbak @@ -1251,6 +1252,10 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Terugzetten naar Selecteer versie Bekijk + + Versies + Conceptversie + Gepubliceerde versie Bewerk script-bestand @@ -1670,6 +1675,12 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je weggooien? Tabblad toevoegen + Geschiedenis opschonen + Overschrijf de standaard geschiedenis opschonen instellingen. + Bewaar alle versies nieuwer dan dagen + Bewaar de laatste versie per dag voor dagen + Voorkom opschonen + Geschiedenis opschonen is globaal uitgeschakeld. Deze instellingen worden pas van kracht nadat ze zijn ingeschakeld. Taal toevoegen @@ -1794,7 +1805,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Rollen Ledentypes Documenttypes - RelatieTypen + Relatietypes Packages Packages Partial Views diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml index dfbc324df6..b993f19f1b 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pl.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Zarządzanie hostami + Zarządzanie hostami Historia zmian Przeglądaj węzeł Zmień typ dokumentu @@ -18,7 +18,7 @@ Opróżnij kosz Aktywuj Eksportuj typ dokumentu - Importuj typ dokumentu + Importuj typ dokumentu Importuj zbiór Edytuj na stronie Wyjście diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml index 542b03abc1..c69912099c 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/pt.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Gerenciar hostnames + Gerenciar hostnames Caminho de Auditoria Navegar o Nó Copiar @@ -15,7 +15,7 @@ Desabilitar Esvaziar Lixeira Exportar Tipo de Documento - Importar Tipo de Documento + Importar Tipo de Documento Importar Pacote Editar na Tela Sair diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 9c1d9e12fb..24910c2e6f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Языки и домены + Языки и домены История исправлений Просмотреть Изменить тип документа @@ -21,7 +21,7 @@ Включить Экспорт Экспортировать - Импортировать + Импортировать Импортировать пакет Править на месте Выйти diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index e0e2235ae9..0efdedfff3 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -8,7 +8,7 @@ Innehåll - Hantera domännamn + Hantera domännamn Hantera versioner Surfa på sidan Ändra dokumenttyp @@ -22,7 +22,7 @@ Avaktivera Töm papperskorgen Exportera dokumenttyp - Importera dokumenttyp + Importera dokumenttyp Importera paket Redigera i Canvas Logga ut diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml index 58c0f7f94b..f54fc31076 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/tr.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - Kültür ve Ana Bilgisayar Adları + Kültür ve Ana Bilgisayar Adları Denetim Yolu Düğüme Göz At Belge Türünü Değiştir @@ -20,7 +20,7 @@ Geri dönüşüm kutusunu boşalt Etkinleştir Belge Türünü Dışa Aktar - Belge Türünü İçe Aktar + Belge Türünü İçe Aktar Paketi İçe Aktar Kanvas'ta Düzenle Çıkış diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml index ba51488f9f..8e7ca24f32 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 管理主机名 + 管理主机名 跟踪审计 浏览节点 改变文档类型 @@ -16,7 +16,7 @@ 禁用 清空回收站 导出文档类型 - 导入文档类型 + 导入文档类型 导入扩展包 实时编辑模式 退出 diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml index 8d5cf16de2..0102971ae9 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/zh_tw.xml @@ -5,7 +5,7 @@ https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - 管理主機名稱 + 管理主機名稱 跟蹤審計 流覽節點 改變文檔類型 @@ -16,7 +16,7 @@ 禁用 清空回收站 匯出文檔類型 - 導入文檔類型 + 導入文檔類型 導入擴展包 即時編輯模式 退出 diff --git a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs new file mode 100644 index 0000000000..c43754e170 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs @@ -0,0 +1,284 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; +using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; + +namespace Umbraco.Cms.Web.Website.Controllers +{ + [UmbracoMemberAuthorize] + public class UmbExternalLoginController : SurfaceController + { + private readonly IMemberManager _memberManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly ILogger _logger; + private readonly IMemberSignInManagerExternalLogins _memberSignInManager; + + public UmbExternalLoginController( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManagerExternalLogins memberSignInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) + { + _logger = logger; + _memberSignInManager = memberSignInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + } + + /// + /// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet. + /// + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult ExternalLogin(string provider, string returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = Request.GetEncodedPathAndQuery(); + } + + var wrappedReturnUrl = + Url.SurfaceAction(nameof(ExternalLoginCallback), this.GetControllerName(), new { returnUrl }); + + AuthenticationProperties properties = + _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl); + + return Challenge(properties, provider); + } + + /// + /// Endpoint used my the login provider to call back to our solution. + /// + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var errors = new List(); + + ExternalLoginInfo loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync(); + if (loginInfo is null) + { + errors.Add("Invalid response from the login provider"); + } + else + { + SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false); + + if (result == SignInResult.Success) + { + // Update any authentication tokens if succeeded + await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); + + return RedirectToLocal(returnUrl); + } + + if (result == SignInResult.TwoFactorRequired) + { + MemberIdentityUser attemptedUser = + await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (attemptedUser == null) + { + return new ValidationErrorResult( + $"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}"); + } + + + var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); + ViewData.SetTwoFactorProviderNames(providerNames); + + return CurrentUmbracoPage(); + + } + + if (result == SignInResult.LockedOut) + { + errors.Add( + $"The local member {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out."); + } + else if (result == SignInResult.NotAllowed) + { + // This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails + // however since we don't enforce those rules (yet) this shouldn't happen. + errors.Add( + $"The member {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in."); + } + else if (result == SignInResult.Failed) + { + // Failed only occurs when the user does not exist + errors.Add("The requested provider (" + loginInfo.LoginProvider + + ") has not been linked to an account, the provider must be linked before it can be used."); + } + else if (result == MemberSignInManager.ExternalLoginSignInResult.NotAllowed) + { + // This occurs when the external provider has approved the login but custom logic in OnExternalLogin has denined it. + errors.Add( + $"The user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not been accepted and cannot sign in."); + } + else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNotLinked) + { + errors.Add("The requested provider (" + loginInfo.LoginProvider + + ") has not been linked to an account, the provider must be linked from the back office."); + } + else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNoEmail) + { + errors.Add( + $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked."); + } + else if (result is MemberSignInManager.AutoLinkSignInResult autoLinkSignInResult && + autoLinkSignInResult.Errors.Count > 0) + { + errors.AddRange(autoLinkSignInResult.Errors); + } + else if (!result.Succeeded) + { + // this shouldn't occur, the above should catch the correct error but we'll be safe just in case + errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred."); + } + } + + if (errors.Count > 0) + { + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo?.LoginProvider, + errors)); + + return CurrentUmbracoPage(); + } + + return RedirectToLocal(returnUrl); + } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (IdentityError error in result.Errors) + { + ModelState.AddModelError(prefix, error.Description); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LinkLogin(string provider, string returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = Request.GetEncodedPathAndQuery(); + } + + var wrappedReturnUrl = + Url.SurfaceAction(nameof(ExternalLinkLoginCallback), this.GetControllerName(), new { returnUrl }); + + // Configures the redirect URL and user identifier for the specified external login including xsrf data + AuthenticationProperties properties = + _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl, + _memberManager.GetUserId(User)); + + return Challenge(properties, provider); + } + + [HttpGet] + public async Task ExternalLinkLoginCallback(string returnUrl) + { + MemberIdentityUser user = await _memberManager.GetUserAsync(User); + string loginProvider = null; + var errors = new List(); + if (user == null) + { + // ... this should really not happen + errors.Add("Local user does not exist"); + } + else + { + ExternalLoginInfo info = + await _memberSignInManager.GetExternalLoginInfoAsync(await _memberManager.GetUserIdAsync(user)); + + if (info == null) + { + //Add error and redirect for it to be displayed + errors.Add( "An error occurred, could not get external login info"); + } + else + { + loginProvider = info.LoginProvider; + IdentityResult addLoginResult = await _memberManager.AddLoginAsync(user, info); + if (addLoginResult.Succeeded) + { + // Update any authentication tokens if succeeded + await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(info); + + return RedirectToLocal(returnUrl); + } + + //Add errors and redirect for it to be displayed + errors.AddRange(addLoginResult.Errors.Select(x => x.Description)); + } + } + + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginProvider, + errors)); + return CurrentUmbracoPage(); + } + + private IActionResult RedirectToLocal(string returnUrl) => + Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disassociate(string provider, string providerKey, string returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = Request.GetEncodedPathAndQuery(); + } + + MemberIdentityUser user = await _memberManager.FindByIdAsync(User.Identity.GetUserId()); + + IdentityResult result = await _memberManager.RemoveLoginAsync(user, provider, providerKey); + + if (result.Succeeded) + { + await _memberSignInManager.SignInAsync(user, false); + return RedirectToLocal(returnUrl); + } + + AddModelErrors(result); + return CurrentUmbracoPage(); + } + } +} diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index afeb41a252..9dbcd292e4 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -1,14 +1,20 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.Models; using Umbraco.Cms.Web.Common.Security; @@ -20,7 +26,29 @@ namespace Umbraco.Cms.Web.Website.Controllers public class UmbLoginController : SurfaceController { private readonly IMemberSignInManager _signInManager; + private readonly IMemberManager _memberManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + [ActivatorUtilitiesConstructor] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + _signInManager = signInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + } + + [Obsolete("Use ctor with all params")] public UmbLoginController( IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, @@ -29,9 +57,11 @@ namespace Umbraco.Cms.Web.Website.Controllers IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider, IMemberSignInManager signInManager) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + : this(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider, signInManager, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { - _signInManager = signInManager; + } [HttpPost] @@ -74,15 +104,28 @@ namespace Umbraco.Cms.Web.Website.Controllers if (result.RequiresTwoFactor) { - throw new NotImplementedException("Two factor support is not supported for Umbraco members yet"); + MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username); + if (attemptedUser == null) + { + return new ValidationErrorResult( + $"No local member found for username {model.Username}"); + } + + var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); + ViewData.SetTwoFactorProviderNames(providerNames); + } + else if (result.IsLockedOut) + { + ModelState.AddModelError("loginModel", "Member is locked out"); + } + else if (result.IsNotAllowed) + { + ModelState.AddModelError("loginModel", "Member is not allowed"); + } + else + { + ModelState.AddModelError("loginModel", "Invalid username or password"); } - - // TODO: We can check for these and respond differently if we think it's important - // result.IsLockedOut - // result.IsNotAllowed - - // Don't add a field level error, just model level. - ModelState.AddModelError("loginModel", "Invalid username or password"); return CurrentUmbracoPage(); } @@ -97,5 +140,7 @@ namespace Umbraco.Cms.Web.Website.Controllers model.RedirectUrl = redirectUrl.ToString(); } } + + } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs new file mode 100644 index 0000000000..ba86e63a36 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs @@ -0,0 +1,160 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Cms.Web.Common.Filters; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; +using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; + +namespace Umbraco.Cms.Web.Website.Controllers +{ + [UmbracoMemberAuthorize] + public class UmbTwoFactorLoginController : SurfaceController + { + private readonly IMemberManager _memberManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + private readonly ILogger _logger; + private readonly IMemberSignInManagerExternalLogins _memberSignInManager; + + public UmbTwoFactorLoginController( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManagerExternalLogins memberSignInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) + { + _logger = logger; + _memberSignInManager = memberSignInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + } + + /// + /// Used to retrieve the 2FA providers for code submission + /// + /// + [AllowAnonymous] + public async Task>> Get2FAProviders() + { + var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + _logger.LogWarning("Get2FAProviders :: No verified member found, returning 404"); + return NotFound(); + } + + var userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user); + return new ObjectResult(userFactors); + } + + [AllowAnonymous] + public async Task Verify2FACode(Verify2FACodeModel model, string returnUrl = null) + { + var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + _logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404"); + return NotFound(); + } + + if (ModelState.IsValid) + { + var result = await _memberSignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, model.RememberClient); + if (result.Succeeded) + { + return RedirectToLocal(returnUrl); + } + + if (result.IsLockedOut) + { + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is locked out"); + } + else if (result.IsNotAllowed) + { + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is not allowed"); + } + else + { + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Invalid code"); + } + } + + //We need to set this, to ensure we show the 2fa login page + var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key); + ViewData.SetTwoFactorProviderNames(providerNames); + return CurrentUmbracoPage(); + } + + [HttpPost] + public async Task ValidateAndSaveSetup(string providerName, string secret, string code, string returnUrl = null) + { + var member = await _memberManager.GetCurrentMemberAsync(); + + var isValid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code); + + if (isValid == false) + { + ModelState.AddModelError(nameof(code), "Invalid Code"); + + return CurrentUmbracoPage(); + } + + var twoFactorLogin = new TwoFactorLogin() + { + Confirmed = true, + Secret = secret, + UserOrMemberKey = member.Key, + ProviderName = providerName + }; + + await _twoFactorLoginService.SaveAsync(twoFactorLogin); + + return RedirectToLocal(returnUrl); + } + + [HttpPost] + public async Task Disable(string providerName, string returnUrl = null) + { + var member = await _memberManager.GetCurrentMemberAsync(); + + var success = await _twoFactorLoginService.DisableAsync(member.Key, providerName); + + if (!success) + { + return CurrentUmbracoPage(); + } + + return RedirectToLocal(returnUrl); + } + + private IActionResult RedirectToLocal(string returnUrl) => + Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); + } +} diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs new file mode 100644 index 0000000000..c208d96972 --- /dev/null +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs @@ -0,0 +1,23 @@ +using System; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Web.Website.Security; + + +namespace Umbraco.Extensions +{ + /// + /// Extension methods for for the Umbraco back office + /// + public static partial class UmbracoBuilderExtensions + { + /// + /// Adds support for external login providers in Umbraco + /// + public static IUmbracoBuilder AddMemberExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) + { + builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services)); + return umbracoBuilder; + } + + } +} diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 797a4b2202..5f8f1d9b69 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -18,7 +18,7 @@ namespace Umbraco.Extensions /// /// extensions for umbraco front-end website /// - public static class UmbracoBuilderExtensions + public static partial class UmbracoBuilderExtensions { /// /// Add services for the umbraco front-end website diff --git a/src/Umbraco.Web.Website/Models/ProfileModel.cs b/src/Umbraco.Web.Website/Models/ProfileModel.cs index a435d22c06..5fc7ed2df8 100644 --- a/src/Umbraco.Web.Website/Models/ProfileModel.cs +++ b/src/Umbraco.Web.Website/Models/ProfileModel.cs @@ -12,6 +12,10 @@ namespace Umbraco.Cms.Web.Website.Models /// public class ProfileModel : PostRedirectModel { + + [ReadOnly(true)] + public Guid Key { get; set; } + [Required] [EmailAddress] [Display(Name = "Email")] diff --git a/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs b/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs index 00d4cebd1e..4a94fb094c 100644 --- a/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs +++ b/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs @@ -68,7 +68,8 @@ namespace Umbraco.Cms.Web.Website.Models CreatedDate = member.CreatedDateUtc.ToLocalTime(), LastLoginDate = member.LastLoginDateUtc?.ToLocalTime(), LastPasswordChangedDate = member.LastPasswordChangeDateUtc?.ToLocalTime(), - RedirectUrl = _redirectUrl + RedirectUrl = _redirectUrl, + Key = member.Key }; IMemberType memberType = MemberTypeService.Get(member.MemberTypeAlias); @@ -83,7 +84,7 @@ namespace Umbraco.Cms.Web.Website.Models { // should never happen throw new InvalidOperationException($"Could not find a member with key: {member.Key}."); - } + } if (_lookupProperties) { diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 717a6b490a..9106c3ed09 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -130,7 +130,7 @@ namespace Umbraco.Cms.Web.Website.Routing IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext); - umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); + umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); // now we need to do some public access checks umbracoRouteValues = await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues); @@ -202,8 +202,8 @@ namespace Umbraco.Cms.Web.Website.Routing } // if it is a POST/GET then a value must be in the request - if (!httpContext.Request.Query.TryGetValue("ufprt", out StringValues encodedVal) - && (!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out encodedVal))) + if ((!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out StringValues encodedVal)) + && !httpContext.Request.Query.TryGetValue("ufprt", out encodedVal)) { return null; } diff --git a/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs new file mode 100644 index 0000000000..d58abfc871 --- /dev/null +++ b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Web.Common.Security; +using Umbraco.Extensions; +using Constants = Umbraco.Cms.Core.Constants; + +namespace Umbraco.Cms.Web.Website.Security +{ + /// + /// Custom used to associate external logins with umbraco external login options + /// + public class MemberAuthenticationBuilder : AuthenticationBuilder + { + private readonly Action _loginProviderOptions; + + public MemberAuthenticationBuilder( + IServiceCollection services, + Action loginProviderOptions = null) + : base(services) + => _loginProviderOptions = loginProviderOptions ?? (x => { }); + + public string SchemeForMembers(string scheme) + => scheme?.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix); + + /// + /// Overridden to track the final authenticationScheme being registered for the external login + /// + /// + /// + /// + /// + /// + /// + public override AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string displayName, Action configureOptions) + { + // Validate that the prefix is set + if (!authenticationScheme.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) + { + throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.BackOfficeExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}"); + } + + // add our login provider to the container along with a custom options configuration + Services.Configure(authenticationScheme, _loginProviderOptions); + base.Services.AddSingleton(services => + { + return new MemberExternalLoginProvider( + authenticationScheme, + services.GetRequiredService>()); + }); + Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureMemberScheme>()); + + return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + } + + // Ensures that the sign in scheme is always the Umbraco member external type + private class EnsureMemberScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + { + public void PostConfigure(string name, TOptions options) + { + if (!name.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) + { + return; + } + + options.SignInScheme = IdentityConstants.ExternalScheme; + } + } + } + +} diff --git a/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs b/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs new file mode 100644 index 0000000000..4f8eb407be --- /dev/null +++ b/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Web.Common.Security; + +namespace Umbraco.Cms.Web.Website.Security +{ + /// + /// Used to add back office login providers + /// + public class MemberExternalLoginsBuilder + { + public MemberExternalLoginsBuilder(IServiceCollection services) + { + _services = services; + } + + private readonly IServiceCollection _services; + + /// + /// Add a back office login provider with options + /// + /// + /// + /// + public MemberExternalLoginsBuilder AddMemberLogin( + Action build, + Action loginProviderOptions = null) + { + build(new MemberAuthenticationBuilder(_services, loginProviderOptions)); + return this; + } + } + +} diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs new file mode 100644 index 0000000000..bc14f43235 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Web.HealthCheck.Checks.Config; + +namespace Umbraco.Web.HealthCheck.Checks.Security +{ + [HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] + public class UmbracoApplicationUrlCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IRuntimeState _runtime; + private readonly IUmbracoSettingsSection _settings; + + private const string SetApplicationUrlAction = "setApplicationUrl"; + + public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IRuntimeState runtime, IUmbracoSettingsSection settings) + { + _textService = textService; + _runtime = runtime; + _settings = settings; + } + + /// + /// Executes the action and returns its status + /// + /// + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case SetApplicationUrlAction: + return SetUmbracoApplicationUrl(); + default: + throw new InvalidOperationException("UmbracoApplicationUrlCheck action requested is either not executable or does not exist"); + } + } + + public override IEnumerable GetStatus() + { + //return the statuses + return new[] { CheckUmbracoApplicationUrl() }; + } + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _settings.WebRouting.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var actions = new List(); + + if (url.IsNullOrWhiteSpace()) + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + + actions.Add(new HealthCheckAction(SetApplicationUrlAction, Id) + { + Name = _textService.Localize("healthcheck", "umbracoApplicationUrlConfigureButton"), + Description = _textService.Localize("healthcheck", "umbracoApplicationUrlConfigureDescription") + }); + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + } + + return new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + Actions = actions + }; + } + + private HealthCheckStatus SetUmbracoApplicationUrl() + { + var configFilePath = IOHelper.MapPath("~/config/umbracoSettings.config"); + const string xPath = "/settings/web.routing/@umbracoApplicationUrl"; + var configurationService = new ConfigurationService(configFilePath, xPath, _textService); + var urlValue = _runtime.ApplicationUrl.ToString(); + var updateConfigFile = configurationService.UpdateConfigFile(urlValue); + + if (updateConfigFile.Success) + { + return + new HealthCheckStatus(_textService.Localize("healthcheck", "umbracoApplicationUrlConfigureSuccess", new[] { urlValue })) + { + ResultType = StatusResultType.Success + }; + } + + return + new HealthCheckStatus(_textService.Localize("healthcheck", "umbracoApplicationUrlConfigureError", new[] { updateConfigFile.Result })) + { + ResultType = StatusResultType.Error + }; + } + } +} diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Languages/languages.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Languages/languages.ts index 5898335105..33d5de24cb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Languages/languages.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Languages/languages.ts @@ -13,6 +13,7 @@ context('Languages', () => { cy.umbracoEnsureLanguageCultureNotExists(culture); cy.umbracoSection('settings'); + cy.get('.umb-tree-root-link').contains('Settings') // Enter language tree and create new language cy.umbracoTreeItem('settings', ['Languages']).click(); cy.umbracoButtonByLabelKey('languages_addLanguage').click(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts new file mode 100644 index 0000000000..80250502e6 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Packages/packages.ts @@ -0,0 +1,161 @@ +/// +import { + ContentBuilder, + DocumentTypeBuilder + } from 'umbraco-cypress-testhelpers'; + +context('Packages', () => { + const packageName = "TestPackage"; + const rootDocTypeName = "Test document type"; + const nodeName = "1) Home"; + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + function CreatePackage(contentId){ + const newPackage = { + id: 0, + packageGuid: "00000000-0000-0000-0000-000000000000", + name: "TestPackage", + packagePath: "", + contentLoadChildNodes: false, + contentNodeId: contentId, + macros: [], + languages: [], + dictionaryItems: [], + templates: [], + partialViews: [], + documentTypes: [], + mediaTypes: [], + stylesheets: [], + scripts: [], + dataTypes: [], + mediaUdis: [], + mediaLoadChildNodes: false + } + const url = "https://localhost:44331/umbraco/backoffice/umbracoapi/package/PostSavePackage"; + cy.umbracoApiRequest(url, 'POST', newPackage); + } + + function CreateSimplePackage(){ + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + cy.saveContent(rootContentNode).then((generatedContent) => { + CreatePackage(generatedContent.Id); + }); + }); + } + + it('Creates a simple package', () => { + + cy.umbracoEnsurePackageNameNotExists(packageName); + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + + const rootDocType = new DocumentTypeBuilder() + .withName(rootDocTypeName) + .withAllowAsRoot(true) + .build(); + + cy.saveDocumentType(rootDocType).then((generatedRootDocType) => { + const rootDocTypeAlias = generatedRootDocType["alias"]; + + const rootContentNode = new ContentBuilder() + .withContentTypeAlias(rootDocTypeAlias) + .withAction("saveNew") + .addVariant() + .withName(nodeName) + .withSave(true) + .done() + .build(); + cy.saveContent(rootContentNode); + }); + + // Navigate to create package section + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('Create package').click(); + + // Fill out package creation form + cy.get('#headerName').should('be.visible'); + cy.wait(1000); + cy.get('#headerName').type(packageName); + cy.get('.controls > .umb-node-preview-add').click(); + cy.get('.umb-tree-item__label').first().click(); + cy.contains('Create').click(); + + // Navigate pack to packages and Assert the file is created + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains(packageName).should('be.visible'); + + // Cleanup + cy.umbracoEnsurePackageNameNotExists(packageName); + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + }); + + it('Delete package', () => { + + // Ensure cleanup before test + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + + CreateSimplePackage(); + + // Navigate to create package section + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('Delete').click(); + cy.contains('Yes, delete').click(); + + // Assert + cy.contains('TestPackage').should('not.exist'); + + // Cleanup + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + }); + + it('Download package', () => { + + // Ensure cleanup before test + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + + CreateSimplePackage(); + + // Navigate to package and download + cy.umbracoSection('packages'); + cy.contains('Created').click(); + cy.contains('TestPackage').click(); + cy.contains('Download').click(); + + // Assert + cy.verifyDownload('package.xml'); + + // Cleanup + cy.deleteAllContent(); + cy.umbracoEnsureDocumentTypeNameNotExists(rootDocTypeName); + cy.umbracoEnsurePackageNameNotExists(packageName); + }); +}); \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js b/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js index 51b79a1fef..30988b82cf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js @@ -12,6 +12,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) const del = require('del') +const { isFileExist } = require('cy-verify-downloads'); /** * @type {Cypress.PluginConfig} @@ -25,6 +26,7 @@ module.exports = (on, config) => { config.baseUrl = baseUrl; } + on('task', { isFileExist }) on('after:spec', (spec, results) => { if(results.stats.failures === 0 && results.video) { // `del()` returns a promise, so it's important to return it to ensure @@ -33,5 +35,11 @@ module.exports = (on, config) => { } }) + +on('task', { + isFileExist +}); + + return config; -} +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js b/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js index 4a2a6d31a2..5056c05036 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js @@ -27,6 +27,7 @@ import {Command} from 'umbraco-cypress-testhelpers'; import {Chainable} from './chainable'; import { JsonHelper } from 'umbraco-cypress-testhelpers'; +require('cy-verify-downloads').addCustomCommand(); new Chainable(); new Command().registerCypressCommands(); diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index a4636d990a..7ada1d9fb7 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -453,6 +453,12 @@ "which": "^2.0.1" } }, + "cy-verify-downloads": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/cy-verify-downloads/-/cy-verify-downloads-0.0.5.tgz", + "integrity": "sha512-aRK7VvKG5rmDJK4hjZ27KM2oOOz0cMO7z/j4zX8qCc4ffXZS1XRJkofUY0w5u6MCB/wUsNMs03VuvkeR2tNPoQ==", + "dev": true + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -1622,9 +1628,9 @@ "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==" }, "umbraco-cypress-testhelpers": { - "version": "1.0.0-beta-62", - "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-62.tgz", - "integrity": "sha512-fVjXBdotb2TZrhaWq/HtD1cy+7scHcldJL+HRHeyYtwvUp368lUiAMrx0y4TOZTwBOJ898yyl8yTEPASfAX2pQ==", + "version": "1.0.0-beta-63", + "resolved": "https://registry.npmjs.org/umbraco-cypress-testhelpers/-/umbraco-cypress-testhelpers-1.0.0-beta-63.tgz", + "integrity": "sha512-X+DHWktfB+WBb7YrxvpneVfS1PATx2zPYMdkeZTmtoQEeyGxXA9fW6P712/AUbyGAhRhH+46t4cAINdWJxItug==", "dev": true, "requires": { "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index a44877e703..a95a71020f 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -10,10 +10,11 @@ "devDependencies": { "cross-env": "^7.0.2", "cypress": "8.4.1", + "cy-verify-downloads": "0.0.5", "del": "^6.0.0", "ncp": "^2.0.0", "prompt": "^1.2.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-62" + "umbraco-cypress-testhelpers": "^1.0.0-beta-63" }, "dependencies": { "typescript": "^3.9.2" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json index 6cb05bfcc7..96178bfc54 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json +++ b/tests/Umbraco.Tests.AcceptanceTest/tsconfig.json @@ -14,7 +14,8 @@ "target": "es5", "types": [ - "cypress" + "cypress", + "cy-verify-downloads" ], "lib": [ "es5", diff --git a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs index a69b6b8b76..bd0c426fec 100644 --- a/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs +++ b/tests/Umbraco.Tests.Benchmarks/SqlTemplatesBenchmark.cs @@ -33,8 +33,8 @@ namespace Umbraco.Tests.Benchmarks public SqlTemplatesBenchmark() { - var mappers = new NPoco.MapperCollection( ); - var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); + var mappers = new NPoco.MapperCollection(); + var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init(), mappers); SqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); SqlTemplates = new SqlTemplates(SqlContext); diff --git a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs index 188c515bf0..1eec4f5ae7 100644 --- a/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs +++ b/tests/Umbraco.Tests.Common/TestHelpers/TestDatabase.cs @@ -86,6 +86,8 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers { } + (List, List, List, List) IDatabaseQuery.FetchMultiple(Sql sql) => throw new NotImplementedException(); + public int OneTimeCommandTimeout { get; set; } public MapperCollection Mappers { get; set; } @@ -182,14 +184,17 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public Task ExecuteAsync(string sql, params object[] args) => throw new NotImplementedException(); public Task ExecuteAsync(Sql sql) => throw new NotImplementedException(); + public Task InsertAsync(string tableName, string primaryKeyName, object poco) => throw new NotImplementedException(); public object Insert(string tableName, string primaryKeyName, bool autoIncrement, T poco) => throw new NotImplementedException(); public object Insert(string tableName, string primaryKeyName, T poco) => throw new NotImplementedException(); public object Insert(T poco) => throw new NotImplementedException(); + public void InsertBulk(IEnumerable pocos, InsertBulkOptions? options = null) => throw new NotImplementedException(); public Task InsertAsync(T poco) => throw new NotImplementedException(); + public Task InsertBulkAsync(IEnumerable pocos, InsertBulkOptions options = null) => throw new NotImplementedException(); public Task InsertBatchAsync(IEnumerable pocos, BatchOptions options = null) => throw new NotImplementedException(); @@ -206,6 +211,9 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public IAsyncUpdateQueryProvider UpdateManyAsync() => throw new NotImplementedException(); public IAsyncDeleteQueryProvider DeleteManyAsync() => throw new NotImplementedException(); + public Task IsNewAsync(T poco) => throw new NotImplementedException(); + + public Task SaveAsync(T poco) => throw new NotImplementedException(); public void InsertBulk(IEnumerable pocos) => throw new NotImplementedException(); @@ -350,6 +358,15 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public TRet FetchMultiple(Func, List, List, TRet> cb, Sql sql) => throw new NotImplementedException(); public TRet FetchMultiple(Func, List, List, List, TRet> cb, Sql sql) => throw new NotImplementedException(); + (List, List) IDatabaseQuery.FetchMultiple(string sql, params object[] args) => throw new NotImplementedException(); + + (List, List, List) IDatabaseQuery.FetchMultiple(string sql, params object[] args) => throw new NotImplementedException(); + + (List, List, List, List) IDatabaseQuery.FetchMultiple(string sql, params object[] args) => throw new NotImplementedException(); + + (List, List) IDatabaseQuery.FetchMultiple(Sql sql) => throw new NotImplementedException(); + + (List, List, List) IDatabaseQuery.FetchMultiple(Sql sql) => throw new NotImplementedException(); public Tuple, List> FetchMultiple(string sql, params object[] args) => throw new NotImplementedException(); @@ -382,6 +399,9 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public Task FirstOrDefaultAsync(string sql, params object[] args) => throw new NotImplementedException(); public Task FirstOrDefaultAsync(Sql sql) => throw new NotImplementedException(); + IAsyncEnumerable IAsyncQueryDatabase.QueryAsync(string sql, params object[] args) => throw new NotImplementedException(); + + IAsyncEnumerable IAsyncQueryDatabase.QueryAsync(Sql sql) => throw new NotImplementedException(); public Task> QueryAsync(string sql, params object[] args) => throw new NotImplementedException(); @@ -406,6 +426,29 @@ namespace Umbraco.Cms.Tests.Common.TestHelpers public Task> SkipTakeAsync(long skip, long take, string sql, params object[] args) => throw new NotImplementedException(); public Task> SkipTakeAsync(long skip, long take, Sql sql) => throw new NotImplementedException(); + public Task FetchMultipleAsync(Func, List, TRet> cb, string sql, params object[] args) => throw new NotImplementedException(); + + public Task FetchMultipleAsync(Func, List, List, TRet> cb, string sql, params object[] args) => throw new NotImplementedException(); + + public Task FetchMultipleAsync(Func, List, List, List, TRet> cb, string sql, params object[] args) => throw new NotImplementedException(); + + public Task FetchMultipleAsync(Func, List, TRet> cb, Sql sql) => throw new NotImplementedException(); + + public Task FetchMultipleAsync(Func, List, List, TRet> cb, Sql sql) => throw new NotImplementedException(); + + public Task FetchMultipleAsync(Func, List, List, List, TRet> cb, Sql sql) => throw new NotImplementedException(); + + public Task<(List, List)> FetchMultipleAsync(string sql, params object[] args) => throw new NotImplementedException(); + + public Task<(List, List, List)> FetchMultipleAsync(string sql, params object[] args) => throw new NotImplementedException(); + + public Task<(List, List, List, List)> FetchMultipleAsync(string sql, params object[] args) => throw new NotImplementedException(); + + public Task<(List, List)> FetchMultipleAsync(Sql sql) => throw new NotImplementedException(); + + public Task<(List, List, List)> FetchMultipleAsync(Sql sql) => throw new NotImplementedException(); + + public Task<(List, List, List, List)> FetchMultipleAsync(Sql sql) => throw new NotImplementedException(); public void BuildPageQueries(long skip, long take, string sql, ref object[] args, out string sqlCount, out string sqlPage) => throw new NotImplementedException(); } diff --git a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj index e8472258bd..2c05ea6bf9 100644 --- a/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj +++ b/tests/Umbraco.Tests.Integration.SqlCe/Umbraco.Tests.Integration.SqlCe.csproj @@ -17,7 +17,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs index a9bed62993..f8afe1d6ae 100644 --- a/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/tests/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -60,6 +60,11 @@ namespace Umbraco.Cms.Tests.Integration.Implementations _ipResolver = new AspNetCoreIpResolver(_httpContextAccessor); string contentRoot = Assembly.GetExecutingAssembly().GetRootDirectorySafe(); + + // The mock for IWebHostEnvironment has caused a good few issues. + // We can UseContentRoot, UseWebRoot etc on the host builder instead. + // possibly going down rabbit holes though as would need to cleanup all usages of + // GetHostingEnvironment & GetWebHostEnvironment. var hostEnvironment = new Mock(); // This must be the assembly name for the WebApplicationFactory to work. @@ -68,6 +73,7 @@ namespace Umbraco.Cms.Tests.Integration.Implementations hostEnvironment.Setup(x => x.ContentRootFileProvider).Returns(() => new PhysicalFileProvider(contentRoot)); hostEnvironment.Setup(x => x.WebRootPath).Returns(() => WorkingDirectory); hostEnvironment.Setup(x => x.WebRootFileProvider).Returns(() => new PhysicalFileProvider(WorkingDirectory)); + hostEnvironment.Setup(x => x.EnvironmentName).Returns("Tests"); // We also need to expose it as the obsolete interface since netcore's WebApplicationFactory casts it. hostEnvironment.As(); diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index 2d140e4336..bd9418bdb6 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -87,7 +87,16 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest // call startup builder.Configure(app => Configure(app)); - }).UseEnvironment(Environments.Development); + }) + .UseDefaultServiceProvider(cfg => + { + // These default to true *if* WebHostEnvironment.EnvironmentName == Development + // When running tests, EnvironmentName used to be null on the mock that we register into services. + // Enable opt in for tests so that validation occurs regardless of environment name. + // Would be nice to have this on for UmbracoIntegrationTest also but requires a lot more effort to resolve issues. + cfg.ValidateOnBuild = true; + cfg.ValidateScopes = true; + }); return builder; } diff --git a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs index 564c45d5ad..47d81e3c11 100644 --- a/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/BaseTestDatabase.cs @@ -5,10 +5,10 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; -using System.Data.SqlClient; using System.Diagnostics; using System.Linq; using System.Threading; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Moq; using Umbraco.Cms.Core; diff --git a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs b/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs index b35b6ac0d5..9daa55a1cb 100644 --- a/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs +++ b/tests/Umbraco.Tests.Integration/Testing/SqlDeveloperTestDatabase.cs @@ -3,10 +3,10 @@ using System; using System.Collections.Concurrent; -using System.Data.SqlClient; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs b/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs new file mode 100644 index 0000000000..c61cbb8a1d --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/TestConflictingRouteService.cs @@ -0,0 +1,14 @@ +using System; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.Integration.Testing +{ + public class TestConflictingRouteService : IConflictingRouteService + { + public bool HasConflictingRoutes(out string controllername) + { + controllername = string.Empty; + return false; + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs index 25f6194fde..7ec8a7fc14 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTest.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using System.Data.Common; -using System.Data.SqlClient; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -202,6 +202,9 @@ namespace Umbraco.Cms.Tests.Integration.Testing IWebHostEnvironment webHostEnvironment = TestHelper.GetWebHostEnvironment(); services.AddRequiredNetCoreServices(TestHelper, webHostEnvironment); + // We register this service because we need it for IRuntimeState, if we don't this breaks 900 tests + services.AddSingleton(); + // Add it! Core.Hosting.IHostingEnvironment hostingEnvironment = TestHelper.GetHostingEnvironment(); TypeLoader typeLoader = services.AddTypeLoader( diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs new file mode 100644 index 0000000000..6446bf542a --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Events/EventAggregatorTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Tests.Integration.TestServerTest; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Events +{ + [TestFixture] + public class EventAggregatorTests : UmbracoTestServerTestBase + { + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + services.AddScoped(); + services.AddTransient, EventAggregatorTestNotificationHandler>(); + } + + [Test] + public async Task Publish_HandlerWithScopedDependency_DoesNotThrow() + { + HttpResponseMessage result = await Client.GetAsync("/test-handler-with-scoped-services"); + Assert.AreEqual(HttpStatusCode.OK, result.StatusCode); + } + } + + public class EventAggregatorTestsController : Controller + { + private readonly IEventAggregator _eventAggregator; + + public EventAggregatorTestsController(IEventAggregator eventAggregator) => _eventAggregator = eventAggregator; + + [HttpGet("test-handler-with-scoped-services")] + public async Task Test() + { + var notification = new EventAggregatorTestNotification(); + await _eventAggregator.PublishAsync(notification); + + if (!notification.Mutated) + { + throw new ApplicationException("Doesn't work"); + } + + return Ok(); + } + } + + public class EventAggregatorTestScopedService + { + } + + public class EventAggregatorTestNotification : INotification + { + public bool Mutated { get; set; } + } + + public class EventAggregatorTestNotificationHandler : INotificationHandler + { + private readonly EventAggregatorTestScopedService _scopedService; + + public EventAggregatorTestNotificationHandler(EventAggregatorTestScopedService scopedService) => _scopedService = scopedService; + + // Mutation proves that the handler runs despite depending on scoped service. + public void Handle(EventAggregatorTestNotification notification) => notification.Mutated = true; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs new file mode 100644 index 0000000000..6a5ee88426 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Packaging/CreatedPackageSchemaTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Packaging +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class CreatedPackageSchemaTests : UmbracoIntegrationTest + { + private ICreatedPackagesRepository CreatedPackageSchemaRepository => + GetRequiredService(); + + [Test] + public void PackagesRepository_Can_Save_PackageDefinition() + { + var packageDefinition = new PackageDefinition() + { + Name = "NewPack", DocumentTypes = new List() { "Root" } + }; + var result = CreatedPackageSchemaRepository.SavePackage(packageDefinition); + Assert.IsTrue(result); + } + + [Test] + public void PackageRepository_GetAll_Returns_All_PackageDefinitions() + { + var packageDefinitionList = new List() + { + new () { Name = "PackOne" }, + new () { Name = "PackTwo" }, + new () { Name = "PackThree" } + }; + foreach (PackageDefinition packageDefinition in packageDefinitionList) + { + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + } + + var loadedPackageDefinitions = CreatedPackageSchemaRepository.GetAll().ToList(); + CollectionAssert.IsNotEmpty(loadedPackageDefinitions); + CollectionAssert.AllItemsAreUnique(loadedPackageDefinitions); + Assert.AreEqual(loadedPackageDefinitions.Count, 3); + } + + [Test] + public void PackageRepository_Can_Update_Package() + { + var packageDefinition = new PackageDefinition() { Name = "TestPackage" }; + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + + packageDefinition.Name = "UpdatedName"; + CreatedPackageSchemaRepository.SavePackage(packageDefinition); + var result = CreatedPackageSchemaRepository.GetAll().ToList(); + + Assert.AreEqual(result.Count, 1); + Assert.AreEqual(result.FirstOrDefault()?.Name, "UpdatedName"); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index fa539ef1e8..e27163bf3b 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -1,14 +1,13 @@ using System; using System.Collections.Generic; -using System.Data.SqlClient; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Data.SqlClient; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs index bfba8b32e0..498e5b10e2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/MacroRepositoryTest.cs @@ -2,8 +2,8 @@ // See LICENSE for more details. using System.Collections.Generic; -using System.Data.SqlClient; using System.Linq; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Cache; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs index 02709f7f84..4c6204ee4e 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/PartialViewRepositoryTests.cs @@ -53,7 +53,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { var repository = new PartialViewRepository(fileSystems); - var partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; + IPartialView partialView = new PartialView(PartialViewType.PartialView, "test-path-1.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("test-path-1.cshtml")); Assert.AreEqual("test-path-1.cshtml", partialView.Path); @@ -62,10 +62,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos partialView = new PartialView(PartialViewType.PartialView, "path-2/test-path-2.cshtml") { Content = "// partialView" }; repository.Save(partialView); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.cshtml")); - Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-2.cshtml"); + partialView = repository.Get("path-2/test-path-2.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-2.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-2.cshtml", partialView.VirtualPath); @@ -76,26 +76,33 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2/test-path-3.cshtml"); + partialView = repository.Get("path-2/test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = (PartialView)repository.Get("path-2\\test-path-3.cshtml"); + partialView = repository.Get("path-2\\test-path-3.cshtml"); Assert.IsNotNull(partialView); Assert.AreEqual("path-2\\test-path-3.cshtml".Replace("\\", $"{Path.DirectorySeparatorChar}"), partialView.Path); Assert.AreEqual("/Views/Partials/path-2/test-path-3.cshtml", partialView.VirtualPath); - partialView = new PartialView(PartialViewType.PartialView, "\\test-path-4.cshtml") { Content = "// partialView" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + partialView = new PartialView(PartialViewType.PartialView, "..\\test-path-4.cshtml") { Content = "// partialView" }; + Assert.Throws(() => repository.Save(partialView)); - partialView = (PartialView)repository.Get("missing.cshtml"); + partialView = new PartialView(PartialViewType.PartialView, "\\test-path-5.cshtml") { Content = "// partialView" }; + repository.Save(partialView); + + partialView = repository.Get("\\test-path-5.cshtml"); + Assert.IsNotNull(partialView); + Assert.AreEqual("test-path-5.cshtml", partialView.Path); + Assert.AreEqual("/Views/Partials/test-path-5.cshtml", partialView.VirtualPath); + + partialView = repository.Get("missing.cshtml"); Assert.IsNull(partialView); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => partialView = (PartialView)repository.Get("\\test-path-4.cshtml")); - Assert.Throws(() => partialView = (PartialView)repository.Get("../../packages.config")); + Assert.Throws(() => partialView = repository.Get("..\\test-path-4.cshtml")); + Assert.Throws(() => partialView = repository.Get("../../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs index 28f9a9eff1..4721af14e1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ScriptRepositoryTest.cs @@ -303,15 +303,22 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.js".Replace("\\", $"{Path.DirectorySeparatorChar}"), script.Path); Assert.AreEqual("/scripts/path-2/test-path-3.js", script.VirtualPath); - script = new Script("\\test-path-4.js") { Content = "// script" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + script = new Script("..\\test-path-4.js") { Content = "// script" }; + Assert.Throws(() => repository.Save(script)); + script = new Script("\\test-path-5.js") { Content = "// script" }; + repository.Save(script); + + script = repository.Get("\\test-path-5.js"); + Assert.IsNotNull(script); + Assert.AreEqual("test-path-5.js", script.Path); + Assert.AreEqual("/scripts/test-path-5.js", script.VirtualPath); + script = repository.Get("missing.js"); Assert.IsNull(script); - // fixed in 7.3 - 7.2.8 used to... - Assert.Throws(() => script = repository.Get("\\test-path-4.js")); + Assert.Throws(() => script = repository.Get("..\\test-path-4.js")); Assert.Throws(() => script = repository.Get("../packages.config")); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs index 10461dd577..ff4ced61ee 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ServerRegistrationRepositoryTest.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Data.SqlClient; using System.Linq; +using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; using NUnit.Framework; using Umbraco.Cms.Core.Cache; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs index d9bde0bca2..a32638ed4d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/StylesheetRepositoryTest.cs @@ -275,7 +275,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos repository.Save(stylesheet); Assert.IsTrue(_fileSystem.FileExists("path-2/test-path-2.css")); - Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); // fixed in 7.3 - 7.2.8 does not update the path + Assert.AreEqual("path-2\\test-path-2.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-2.css", stylesheet.VirtualPath); stylesheet = repository.Get("path-2/test-path-2.css"); @@ -300,17 +300,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual("path-2\\test-path-3.css".Replace("\\", $"{Path.DirectorySeparatorChar}"), stylesheet.Path); Assert.AreEqual("/css/path-2/test-path-3.css", stylesheet.VirtualPath); - stylesheet = new Stylesheet("\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; - Assert.Throws(() => // fixed in 7.3 - 7.2.8 used to strip the \ + stylesheet = new Stylesheet("..\\test-path-4.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + Assert.Throws(() => repository.Save(stylesheet)); - // fixed in 7.3 - 7.2.8 used to throw + stylesheet = new Stylesheet("\\test-path-5.css") { Content = "body { color:#000; } .bold {font-weight:bold;}" }; + repository.Save(stylesheet); + + stylesheet = repository.Get("\\test-path-5.css"); + Assert.IsNotNull(stylesheet); + Assert.AreEqual("test-path-5.css", stylesheet.Path); + Assert.AreEqual("/css/test-path-5.css", stylesheet.VirtualPath); + stylesheet = repository.Get("missing.css"); Assert.IsNull(stylesheet); // #7713 changes behaviour to return null when outside the filesystem // to accomodate changing the CSS path and not flooding the backoffice with errors - stylesheet = repository.Get("\\test-path-4.css"); // outside the filesystem, does not exist + stylesheet = repository.Get("..\\test-path-4.css"); // outside the filesystem, does not exist Assert.IsNull(stylesheet); stylesheet = repository.Get("../packages.config"); // outside the filesystem, exists diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs index e30d7bbf55..7ea8e65eda 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Scoping/ScopeFileSystemsTests.cs @@ -48,7 +48,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MediaFileManager_does_not_write_to_physical_file_system_when_scoped_if_scope_does_not_complete() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -77,7 +77,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MediaFileManager_writes_to_physical_file_system_when_scoped_and_scope_is_completed() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; @@ -108,7 +108,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping [Test] public void MultiThread() { - string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPath); + string rootPath = HostingEnvironment.MapPathWebRoot(GlobalSettings.UmbracoMediaPhysicalRootPath); string rootUrl = HostingEnvironment.ToAbsolute(GlobalSettings.UmbracoMediaPath); var physMediaFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, GetRequiredService>(), rootPath, rootUrl); MediaFileManager mediaFileManager = MediaFileManager; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs index ab5ed1dcfc..3b719dc53a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security { private IUserService UserService => GetRequiredService(); private IEntityService EntityService => GetRequiredService(); - private IExternalLoginService ExternalLoginService => GetRequiredService(); + private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService(); private IUmbracoMapper UmbracoMapper => GetRequiredService(); private ILocalizedTextService TextService => GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs index 985aed0597..ad7df3cee5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentTypeServiceVariantsTests.cs @@ -4,19 +4,16 @@ using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using NPoco; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -40,14 +37,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services private ILocalizationService LocalizationService => GetRequiredService(); - protected override void BeforeHostStart(IHost host) - { - base.BeforeHostStart(host); - - // Ensure that the events are bound on each test - PublishedSnapshotServiceEventHandler eventBinder = host.Services.GetRequiredService(); - eventBinder.Initialize(); - } protected override void CustomTestSetup(IUmbracoBuilder builder) { @@ -57,8 +46,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { options.NuCacheSerializerType = NuCacheSerializerType.JSON; }); - - } private void AssertJsonStartsWith(int id, string expected) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs index 09870a08e1..971379d6a9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ExternalLoginServiceTests.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { private IUserService UserService => GetRequiredService(); - private IExternalLoginService ExternalLoginService => (IExternalLoginService)GetRequiredService(); + private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService(); [Test] [Ignore("We don't support duplicates anymore, this removing on save was a breaking change work around, this needs to be ported to a migration")] @@ -38,14 +38,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // insert duplicates manuall ScopeAccessor.AmbientScope.Database.Insert(new ExternalLoginDto { - UserId = user.Id, + UserOrMemberKey = user.Key, LoginProvider = "test1", ProviderKey = providerKey, CreateDate = latest }); ScopeAccessor.AmbientScope.Database.Insert(new ExternalLoginDto { - UserId = user.Id, + UserOrMemberKey = user.Key, LoginProvider = "test1", ProviderKey = providerKey, CreateDate = oldest @@ -60,9 +60,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", providerKey) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); - var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList(); + var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList(); // duplicates will be removed, keeping the latest entries Assert.AreEqual(2, logins.Count); @@ -84,9 +84,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", providerKey) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); - var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList(); + var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList(); Assert.AreEqual(1, logins.Count); } @@ -103,16 +103,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", providerKey1, "hello"), new ExternalLogin("test2", providerKey2, "world") }; - ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Key, extLogins); extLogins = new[] { new ExternalLogin("test1", providerKey1, "123456"), new ExternalLogin("test2", providerKey2, "987654") }; - ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Key, extLogins); - var found = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var found = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(2, found.Count); Assert.AreEqual("123456", found[0].UserData); Assert.AreEqual("987654", found[1].UserData); @@ -131,7 +131,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", providerKey1, "hello"), new ExternalLogin("test2", providerKey2, "world") }; - ExternalLoginService.Save(user.Id, extLogins); + ExternalLoginService.Save(user.Key, extLogins); var found = ExternalLoginService.Find("test2", providerKey2).ToList(); Assert.AreEqual(1, found.Count); @@ -151,9 +151,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test2", Guid.NewGuid().ToString("N")) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); - var logins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var logins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(2, logins.Count); for (int i = 0; i < logins.Count; i++) { @@ -173,7 +173,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", Guid.NewGuid().ToString("N")) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); ExternalLoginToken[] externalTokens = new[] { @@ -181,9 +181,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLoginToken(externalLogins[0].LoginProvider, "hello2", "world2") }; - ExternalLoginService.Save(user.Id, externalTokens); + ExternalLoginService.Save(user.Key, externalTokens); - var tokens = ExternalLoginService.GetExternalLoginTokens(user.Id).ToList(); + var tokens = ExternalLoginService.GetExternalLoginTokens(user.Key).ToList(); Assert.AreEqual(2, tokens.Count); } @@ -201,18 +201,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test4", Guid.NewGuid().ToString("N")) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); - var logins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var logins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList(); logins.RemoveAt(0); // remove the first one logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id.ToString())); // add a new one logins[0].ProviderKey = "abcd123"; // update // save new list - ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); + ExternalLoginService.Save(user.Key, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey))); - var updatedLogins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var updatedLogins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(4, updatedLogins.Count); for (int i = 0; i < updatedLogins.Count; i++) { @@ -233,7 +233,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test2", Guid.NewGuid().ToString("N")) }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); ExternalLoginToken[] externalTokens = new[] { @@ -243,18 +243,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLoginToken(externalLogins[1].LoginProvider, "hello2a", "world2a") }; - ExternalLoginService.Save(user.Id, externalTokens); + ExternalLoginService.Save(user.Key, externalTokens); - var tokens = ExternalLoginService.GetExternalLoginTokens(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var tokens = ExternalLoginService.GetExternalLoginTokens(user.Key).OrderBy(x => x.LoginProvider).ToList(); tokens.RemoveAt(0); // remove the first one tokens.Add(new IdentityUserToken(externalLogins[1].LoginProvider, "hello2b", "world2b", user.Id.ToString())); // add a new one tokens[0].Value = "abcd123"; // update // save new list - ExternalLoginService.Save(user.Id, tokens.Select(x => new ExternalLoginToken(x.LoginProvider, x.Name, x.Value))); + ExternalLoginService.Save(user.Key, tokens.Select(x => new ExternalLoginToken(x.LoginProvider, x.Name, x.Value))); - var updatedTokens = ExternalLoginService.GetExternalLoginTokens(user.Id).OrderBy(x => x.LoginProvider).ToList(); + var updatedTokens = ExternalLoginService.GetExternalLoginTokens(user.Key).OrderBy(x => x.LoginProvider).ToList(); Assert.AreEqual(4, updatedTokens.Count); for (int i = 0; i < updatedTokens.Count; i++) { @@ -275,9 +275,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services new ExternalLogin("test1", Guid.NewGuid().ToString("N"), "hello world") }; - ExternalLoginService.Save(user.Id, externalLogins); + ExternalLoginService.Save(user.Key, externalLogins); - var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList(); + var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList(); Assert.AreEqual("hello world", logins[0].UserData); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 41d540302b..6070f468b1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -95,7 +95,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs index 023c9ad952..ca7950a7cf 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/BaseUsingSqlSyntax.cs @@ -45,7 +45,7 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers { new NullableDateMapper() }; - var pocoDataFactory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, pocoMappers).Init()); + var pocoDataFactory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, pocoMappers).Init(), pocoMappers); var sqlSyntax = new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())); SqlContext = new SqlContext(sqlSyntax, DatabaseType.SqlServer2012, pocoDataFactory, factory.GetRequiredService()); Mappers = factory.GetRequiredService(); diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index 01bfef40c6..5c7d4ea3bf 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -134,9 +134,9 @@ namespace Umbraco.Cms.Tests.UnitTests.TestHelpers /// public static string MapPathForTestFiles(string relativePath) => s_testHelperInternal.MapPathForTestFiles(relativePath); - public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath, Constants.SystemDirectories.AppPlugins }); + public static void InitializeContentDirectories() => CreateDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath, Constants.SystemDirectories.AppPlugins }); - public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPath }); + public static void CleanContentDirectories() => CleanDirectories(new[] { Constants.SystemDirectories.MvcViews, new GlobalSettings().UmbracoMediaPhysicalRootPath }); public static void CreateDirectories(string[] directories) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeHelperTests.cs index b2fade7e36..29c583dc75 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Composing/TypeHelperTests.cs @@ -6,9 +6,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data.Odbc; using System.Data.OleDb; -using System.Data.SqlClient; using System.Linq; using System.Reflection; +using Microsoft.Data.SqlClient; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Composing; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs new file mode 100644 index 0000000000..f159ecbc85 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Configuration/Models/RequestHandlerSettingsTests.cs @@ -0,0 +1,90 @@ +using System.Linq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Configuration.Models +{ + [TestFixture] + public class RequestHandlerSettingsTests + { + [Test] + public void Given_CharCollection_With_DefaultEnabled_MergesCollection() + { + var userCollection = new CharItem[] + { + new () { Char = "test", Replacement = "replace" }, + new () { Char = "test2", Replacement = "replace2" } + }; + + + var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection }; + var actual = settings.GetCharReplacements().ToList(); + + var expectedCollection = RequestHandlerSettings.DefaultCharCollection.ToList(); + expectedCollection.AddRange(userCollection); + + Assert.AreEqual(expectedCollection.Count, actual.Count); + Assert.That(actual, Is.EquivalentTo(expectedCollection)); + } + + [Test] + public void Given_CharCollection_With_DefaultDisabled_ReturnsUserCollection() + { + var userCollection = new CharItem[] + { + new () { Char = "test", Replacement = "replace" }, + new () { Char = "test2", Replacement = "replace2" } + }; + + var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection, EnableDefaultCharReplacements = false }; + var actual = settings.GetCharReplacements().ToList(); + + Assert.AreEqual(userCollection.Length, actual.Count); + Assert.That(actual, Is.EquivalentTo(userCollection)); + } + + [Test] + public void Given_CharCollection_That_OverridesDefaultValues_ReturnsReplacements() + { + var userCollection = new CharItem[] + { + new () { Char = "%", Replacement = "percent" }, + new () { Char = ".", Replacement = "dot" } + }; + + var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection }; + var actual = settings.GetCharReplacements().ToList(); + + Assert.AreEqual(RequestHandlerSettings.DefaultCharCollection.Length, actual.Count); + + Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "%" && x.Replacement == "percent")); + Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "." && x.Replacement == "dot")); + Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "%" && x.Replacement == string.Empty)); + Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "." && x.Replacement == string.Empty)); + } + + [Test] + public void Given_CharCollection_That_OverridesDefaultValues_And_ContainsNew_ReturnsMergedWithReplacements() + { + var userCollection = new CharItem[] + { + new () { Char = "%", Replacement = "percent" }, + new () { Char = ".", Replacement = "dot" }, + new () { Char = "new", Replacement = "new" } + }; + + var settings = new RequestHandlerSettings { UserDefinedCharCollection = userCollection }; + var actual = settings.GetCharReplacements().ToList(); + + // Add 1 to the length, because we're expecting to only add one new one + Assert.AreEqual(RequestHandlerSettings.DefaultCharCollection.Length + 1, actual.Count); + + Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "%" && x.Replacement == "percent")); + Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "." && x.Replacement == "dot")); + Assert.That(actual, Has.Exactly(1).Matches(x => x.Char == "new" && x.Replacement == "new")); + Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "%" && x.Replacement == string.Empty)); + Assert.That(actual, Has.Exactly(0).Matches(x => x.Char == "." && x.Replacement == string.Empty)); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs index 0ada6a20dd..f32f252633 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -3,6 +3,7 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NUnit.Framework; using Umbraco.Cms.Core.PropertyEditors; @@ -11,6 +12,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors [TestFixture] public class NestedContentPropertyComponentTests { + private static void AreEqualJson(string expected, string actual) + { + Assert.AreEqual(JToken.Parse(expected), JToken.Parse(actual)); + } + [Test] public void Invalid_Json() { @@ -27,17 +33,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors Guid GuidFactory() => guids[guidCounter++]; var json = @"[ - {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, - {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} -]"; + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} + ]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -48,29 +54,27 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors Guid GuidFactory() => guids[guidCounter++]; var json = @"[{ - ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""list"", - ""text"": ""zoot"", - ""subItems"": [{ - ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""zoot"" - } - ] - } -]"; + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"": [{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + }] + }]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) @@ -79,9 +83,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -93,7 +97,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@" + [{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -104,21 +109,21 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); var json = @"[{ - ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""list"", - ""text"": ""zoot"", - ""subItems"":" + subJsonEscaped + @" - } -]"; + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } + ]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) @@ -127,9 +132,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -141,7 +146,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -152,7 +157,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); // Complex editor such as the grid var complexEditorJsonEscaped = @"{ @@ -231,9 +236,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -252,10 +257,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); // Ensure that the original key is NOT changed/modified & still exists - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); + Assert.IsTrue(result.Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); } [Test] @@ -267,7 +272,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", ""text"": ""woot"" @@ -276,7 +281,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); var json = @"[{ ""name"": ""Item 1 was copied and has no key"", @@ -295,9 +300,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON for each item - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[1].ToString())); + Assert.IsTrue(result.Contains(guids[2].ToString())); } [Test] @@ -309,7 +314,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -319,7 +324,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); // Complex editor such as the grid var complexEditorJsonEscaped = @"{ @@ -394,8 +399,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON for each item - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[1].ToString())); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs index 6f9ee481cc..b686aee278 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/ShortStringHelper/DefaultShortStringHelperTestsWithoutSetup.cs @@ -1,6 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System; using System.Diagnostics; using System.Linq; using System.Text; @@ -19,7 +20,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper { var requestHandlerSettings = new RequestHandlerSettings() { - CharCollection = Enumerable.Empty(), + CharCollection = Array.Empty(), + EnableDefaultCharReplacements = false, ConvertUrlsToAscii = "false" }; @@ -45,7 +47,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper { var requestHandlerSettings = new RequestHandlerSettings() { - CharCollection = Enumerable.Empty(), + CharCollection = Array.Empty(), + EnableDefaultCharReplacements = false, ConvertUrlsToAscii = "false" }; @@ -339,7 +342,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper { var requestHandlerSettings = new RequestHandlerSettings() { - CharCollection = Enumerable.Empty(), + CharCollection = Array.Empty(), + EnableDefaultCharReplacements = false, ConvertUrlsToAscii = "false" }; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs index 8508281444..8ecf6870a4 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/BulkDataReaderTests.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Data.Common; -using System.Data.SqlClient; +using Microsoft.Data.SqlClient; using NUnit.Framework; using Umbraco.Cms.Infrastructure.Persistence; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs index ac880c2b3b..3eddb0fe29 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Persistence/NPocoTests/NPocoSqlTemplateTests.cs @@ -42,7 +42,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Persistence.NPocoTe public void SqlTemplateArgs() { var mappers = new NPoco.MapperCollection { new NullableDateMapper() }; - var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init()); + var factory = new FluentPocoDataFactory((type, iPocoDataFactory) => new PocoDataBuilder(type, mappers).Init(), mappers); var sqlContext = new SqlContext(new SqlServerSyntaxProvider(Options.Create(new GlobalSettings())), DatabaseType.SQLCe, factory); var sqlTemplates = new SqlTemplates(sqlContext); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs index f3edb0b8c5..5fd34dae3f 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberManagerTests.cs @@ -53,7 +53,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security new UmbracoMapper(new MapDefinitionCollection(() => mapDefinitions), scopeProvider), scopeProvider, new IdentityErrorDescriber(), - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of()); _mockIdentityOptions = new Mock>(); var idOptions = new IdentityOptions { Lockout = { AllowedForNewUsers = false } }; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs index d7c770558d..a7ad27f1a6 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Security/MemberUserStoreTests.cs @@ -37,7 +37,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security new UmbracoMapper(new MapDefinitionCollection(() => new List()), mockScopeProvider.Object), mockScopeProvider.Object, new IdentityErrorDescriber(), - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of() + ); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index 315efb5890..aaf3445908 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -617,20 +617,6 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Id = 77, Properties = new List() { - new ContentPropertyDisplay() - { - Alias = "_umb_id", - View = "idwithguid", - Value = new [] - { - "123", - "guid" - } - }, - new ContentPropertyDisplay() - { - Alias = "_umb_doctype" - }, new ContentPropertyDisplay() { Alias = "_umb_login" diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs index f00225e7b4..f5d5d7c766 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs @@ -22,13 +22,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common { private const string CropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string CropperJson2 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; - private const string CropperJson3 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; + private const string CropperJson3 = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; private const string MediaPath = "/media/1005/img_0671.jpg"; [Test] public void CanConvertImageCropperDataSetSrcToString() { - // cropperJson3 - has not crops + // cropperJson3 - has no crops ImageCropperValue cropperValue = CropperJson3.DeserializeImageCropperValue(); Attempt serialized = cropperValue.TryConvertTo(); Assert.IsTrue(serialized.Success); @@ -38,7 +38,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common [Test] public void CanConvertImageCropperDataSetJObject() { - // cropperJson3 - has not crops + // cropperJson3 - has no crops ImageCropperValue cropperValue = CropperJson3.DeserializeImageCropperValue(); Attempt serialized = cropperValue.TryConvertTo(); Assert.IsTrue(serialized.Success); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs index f553919d08..192bcaf27e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; @@ -38,7 +39,22 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security serviceCollection .AddLogging() .AddAuthentication() - .AddCookie(IdentityConstants.ApplicationScheme); + .AddCookie(IdentityConstants.ApplicationScheme) + .AddCookie(IdentityConstants.ExternalScheme, o => + { + o.Cookie.Name = IdentityConstants.ExternalScheme; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o => + { + o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + .AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o => + { + o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }); IServiceProvider serviceProvider = serviceProviderFactory.CreateServiceProvider(serviceCollection); var httpContextFactory = new DefaultHttpContextFactory(serviceProvider); IFeatureCollection features = new DefaultHttpContext().Features; @@ -56,7 +72,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security Mock.Of>(), _mockLogger.Object, Mock.Of(), - Mock.Of>()); + Mock.Of>(), + Mock.Of(), + Mock.Of() + ); } private static Mock MockMemberManager() => new Mock(